Ported MkAvatars to Magnetar
ci/woodpecker/push/ociImagePush Pipeline was successful Details

This commit is contained in:
Natty 2023-11-07 21:15:35 +01:00
parent e1859c98bd
commit 4f6e6163cc
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
21 changed files with 222 additions and 31 deletions

3
Cargo.lock generated
View File

@ -888,6 +888,7 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@ -1455,6 +1456,7 @@ dependencies = [
"compact_str", "compact_str",
"dotenvy", "dotenvy",
"either", "either",
"futures",
"futures-util", "futures-util",
"headers", "headers",
"hyper", "hyper",
@ -1590,6 +1592,7 @@ dependencies = [
"magnetar_sdk_macros", "magnetar_sdk_macros",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"ts-rs", "ts-rs",
"unicode-segmentation", "unicode-segmentation",
] ]

View File

@ -32,6 +32,7 @@ compact_str = "0.7"
dotenvy = "0.15" dotenvy = "0.15"
either = "1.9" either = "1.9"
emojis = "0.6" emojis = "0.6"
futures = "0.3"
futures-core = "0.3" futures-core = "0.3"
futures-util = "0.3" futures-util = "0.3"
headers = "0.3" headers = "0.3"
@ -53,6 +54,7 @@ sea-orm = "0.12"
sea-orm-migration = "0.12" sea-orm-migration = "0.12"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
serde_urlencoded = "0.7"
strum = "0.25" strum = "0.25"
tera = { version = "1", default-features = false } tera = { version = "1", default-features = false }
thiserror = "1" thiserror = "1"
@ -101,6 +103,7 @@ cfg-if = { workspace = true }
compact_str = { workspace = true } compact_str = { workspace = true }
either = { workspace = true } either = { workspace = true }
futures = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
lazy_static = { workspace = true } lazy_static = { workspace = true }

View File

@ -114,6 +114,16 @@ impl CalckeyModel {
.await?) .await?)
} }
pub async fn get_many_users_by_id(
&self,
id: &[String],
) -> Result<Vec<user::Model>, CalckeyDbError> {
Ok(user::Entity::find()
.filter(user::Column::Id.is_in(id))
.all(&self.0)
.await?)
}
pub async fn get_user_by_token( pub async fn get_user_by_token(
&self, &self,
token: &str, token: &str,

View File

@ -9,25 +9,37 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import * as os from "@/os"; import * as os from "@/os";
import { User } from "calckey-js/built/entities"; import { packed, endpoints } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
userIds: string[]; userIds: string[];
}>(); }>();
const users = ref<User[]>([]); const users = ref<packed.PackUserBase[]>([]);
onMounted(async () => { onMounted(async () => {
users.value = await os.api("users/show", { users.value = await os
userIds: props.userIds, .magApi(
}); endpoints.GetManyUsersById,
{
id: props.userIds,
},
{}
)
.then((p) => p.filter((u) => u !== null).map((u) => u!));
watch( watch(
() => props.userIds, () => props.userIds,
async (userIds) => { async (userIds) => {
users.value = await os.api("users/show", { users.value = await os
userIds, .magApi(
}); endpoints.GetManyUsersById,
{
id: userIds,
},
{}
)
.then((p) => p.filter((u) => u !== null).map((u) => u!));
} }
); );
}); });

View File

@ -83,7 +83,7 @@ function onClick(ev: MouseEvent) {
emit("click", ev); emit("click", ev);
} }
let color = $ref(); let color = $ref<string>();
watch( watch(
() => magTransProperty(props.user, "avatar_blurhash", "avatarBlurhash"), () => magTransProperty(props.user, "avatar_blurhash", "avatarBlurhash"),

View File

@ -23,7 +23,7 @@
}}</MkA> }}</MkA>
</div> </div>
<div class="_content"> <div class="_content">
<MkAvatars :user-ids="group.userIds" /> <MagAvatars :user-ids="group.userIds" />
</div> </div>
</div> </div>
</MkPagination> </MkPagination>
@ -35,7 +35,7 @@
<div v-for="group in items" :key="group.id" class="_card"> <div v-for="group in items" :key="group.id" class="_card">
<div class="_title">{{ group.name }}</div> <div class="_title">{{ group.name }}</div>
<div class="_content"> <div class="_content">
<MkAvatars :user-ids="group.userIds" /> <MagAvatars :user-ids="group.userIds" />
</div> </div>
<div class="_footer"> <div class="_footer">
<MkButton danger @click="leave(group)">{{ <MkButton danger @click="leave(group)">{{
@ -52,7 +52,7 @@
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import MkAvatars from "@/components/MkAvatars.vue"; import MagAvatars from "@/components/MagAvatars.vue";
import * as os from "@/os"; import * as os from "@/os";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";

View File

@ -29,7 +29,7 @@
:to="`/my/lists/${list.id}`" :to="`/my/lists/${list.id}`"
> >
<div class="name">{{ list.name }}</div> <div class="name">{{ list.name }}</div>
<MkAvatars :user-ids="list.userIds" /> <MagAvatars :user-ids="list.userIds" />
</MkA> </MkA>
<MkButton @click="deleteAll" <MkButton @click="deleteAll"
><i class="ph-trash ph-bold ph-lg"></i> ><i class="ph-trash ph-bold ph-lg"></i>
@ -45,7 +45,7 @@
import {} from "vue"; import {} from "vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import MkAvatars from "@/components/MkAvatars.vue"; import MagAvatars from "@/components/MagAvatars.vue";
import MkInfo from "@/components/MkInfo.vue"; import MkInfo from "@/components/MkInfo.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";

View File

@ -10,7 +10,7 @@
><span style="margin-left: 8px">{{ column.name }}</span> ><span style="margin-left: 8px">{{ column.name }}</span>
</template> </template>
<MkAvatars :userIds="userList" /> <MagAvatars :userIds="userList" />
</XColumn> </XColumn>
</template> </template>
@ -21,7 +21,7 @@ import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import { User } from "calckey-js/built/entities"; import { User } from "calckey-js/built/entities";
import MkAvatars from "@/components/MkAvatars.vue"; import MagAvatars from "@/components/MagAvatars.vue";
const props = defineProps<{ const props = defineProps<{
column: Column; column: Column;

View File

@ -21,7 +21,7 @@
</div> </div>
<MkLoading v-else-if="fetching" /> <MkLoading v-else-if="fetching" />
<div v-else class="users"> <div v-else class="users">
<MkAvatars :user-ids="users" class="userAvatars" /> <MagAvatars :user-ids="users" class="userAvatars" />
</div> </div>
</div> </div>
</MkContainer> </MkContainer>
@ -31,7 +31,7 @@
import { useWidgetPropsManager, Widget, WidgetComponentExpose } from "./widget"; import { useWidgetPropsManager, Widget, WidgetComponentExpose } from "./widget";
import { GetFormResultType } from "@/scripts/form"; import { GetFormResultType } from "@/scripts/form";
import MkContainer from "@/components/MkContainer.vue"; import MkContainer from "@/components/MkContainer.vue";
import MkAvatars from "@/components/MkAvatars.vue"; import MagAvatars from "@/components/MagAvatars.vue";
import * as os from "@/os"; import * as os from "@/os";
import { useInterval } from "@/scripts/use-interval"; import { useInterval } from "@/scripts/use-interval";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";

View File

@ -20,7 +20,7 @@ function nestedUrlSearchParams(data: any, topLevel: boolean = true): string {
case "boolean": case "boolean":
case "number": case "number":
case "symbol": case "symbol":
if (topLevel) return encodeURIComponent(data.toString()) + "="; if (topLevel) return encodeURIComponent(data.toString());
return data.toString(); return data.toString();
case "object": case "object":

View File

@ -3,3 +3,4 @@ export { GetTimeline } from "./types/endpoints/GetTimeline";
export { GetUserById } from "./types/endpoints/GetUserById"; export { GetUserById } from "./types/endpoints/GetUserById";
export { GetUserByAcct } from "./types/endpoints/GetUserByAcct"; export { GetUserByAcct } from "./types/endpoints/GetUserByAcct";
export { GetUserSelf } from "./types/endpoints/GetUserSelf"; export { GetUserSelf } from "./types/endpoints/GetUserSelf";
export { GetManyUsersById } from "./types/endpoints/GetManyUsersById";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ManyUsersByIdReq { id: Array<string>, }

View File

@ -0,0 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ManyUsersByIdReq } from "../ManyUsersByIdReq";
import type { PackUserBase } from "../packed/PackUserBase";
export const GetManyUsersById = {
endpoint: "/users/lookup-many",
pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as ManyUsersByIdReq,
response: undefined as unknown as Array<PackUserBase | null>
}

View File

@ -13,6 +13,7 @@ chrono = { workspace = true, features = ["serde"] }
http = { workspace = true } http = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_urlencoded = { workspace = true }
ts-rs = { workspace = true, features = ["chrono", "chrono-impl"] } ts-rs = { workspace = true, features = ["chrono", "chrono-impl"] }

View File

@ -4,7 +4,8 @@ use std::collections::HashSet;
use syn::parse::Parse; use syn::parse::Parse;
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::{ use syn::{
Expr, ExprLit, ExprPath, Ident, Lit, Meta, MetaNameValue, PathArguments, Token, Type, TypePath, Expr, ExprLit, ExprPath, GenericArgument, Ident, Lit, Meta, MetaNameValue, PathArguments,
Token, Type, TypePath,
}; };
struct Field { struct Field {
@ -311,14 +312,52 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream {
} }
}; };
let req_args_flat = request_args.clone().unwrap_or_default();
let mut res_args_flat = response_args.clone().unwrap_or_default();
let call_name_res = if let Some(ref args) = response_args { let call_name_res = if let Some(ref args) = response_args {
let args_str = args let args_nested = args
.iter() .iter()
.map(|a| a.into_token_stream().to_string()) .map(|a| {
let normal = Vec::new();
let GenericArgument::Type(inner) = a else {
return normal;
};
let Type::Path(path_inner) = inner else {
return normal;
};
let Some(seg) = path_inner.path.segments.last() else {
return normal;
};
let PathArguments::AngleBracketed(angle_inner) = &seg.arguments else {
return normal;
};
angle_inner.args.iter().cloned().collect::<Vec<_>>()
})
.collect::<Vec<_>>();
res_args_flat.extend(args_nested.iter().flatten().cloned());
let arg_tokens_nested = args_nested
.iter()
.zip(args)
.map(|(args, parent)| {
if !args.is_empty() {
let names = args.iter().map(|a| a.to_token_stream().to_string());
quote::quote! {
<#parent as TS>::name_with_type_args(vec![#( #names.to_string() ),*])
}
} else {
quote::quote! {
<#parent as TS>::name()
}
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
quote::quote! { quote::quote! {
<#struct_name as Endpoint>::Response::name_with_type_args(vec![#( #args_str.to_string() ),*]) <#struct_name as Endpoint>::Response::name_with_type_args(vec![#( #arg_tokens_nested ),*])
} }
} else { } else {
quote::quote! { quote::quote! {
@ -326,9 +365,6 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream {
} }
}; };
let req_args_flat = request_args.unwrap_or_default();
let res_args_flat = response_args.unwrap_or_default();
let expanded = quote::quote! { let expanded = quote::quote! {
impl Default for #struct_name { impl Default for #struct_name {
fn default() -> Self { fn default() -> Self {

View File

@ -1,10 +1,11 @@
use crate::endpoints::Endpoint; use crate::endpoints::Endpoint;
use crate::util_types::deserialize_array_urlenc;
use http::Method; use http::Method;
use magnetar_sdk_macros::Endpoint; use magnetar_sdk_macros::Endpoint;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::types::user::{PackUserMaybeAll, PackUserSelfMaybeAll}; use crate::types::user::{PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll};
// Get self // Get self
#[derive(Serialize, Deserialize, TS)] #[derive(Serialize, Deserialize, TS)]
@ -54,6 +55,23 @@ pub struct UserByIdReq {
)] )]
pub struct GetUserById; pub struct GetUserById;
// Get many users by an ID array
#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ManyUsersByIdReq {
#[serde(deserialize_with = "deserialize_array_urlenc")]
pub id: Vec<String>,
}
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/lookup-many",
method = Method::GET,
request = "ManyUsersByIdReq",
response = "Vec<Option<PackUserBase>>"
)]
pub struct GetManyUsersById;
// Get user by fedi tag // Get user by fedi tag
#[derive(Endpoint)] #[derive(Endpoint)]

View File

@ -1,4 +1,7 @@
use serde::Serialize; use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use std::hash::Hash;
use ts_rs::TS; use ts_rs::TS;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -76,3 +79,20 @@ impl<const MIN: u64, const MAX: u64> TryFrom<u64> for U64Range<MIN, MAX> {
Ok(U64Range(value)) Ok(U64Range(value))
} }
} }
pub(crate) fn deserialize_array_urlenc<'de, D, T: Eq + Hash>(
deserializer: D,
) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let str_raw = String::deserialize(deserializer)?;
let parts = serde_urlencoded::from_str::<Vec<(T, String)>>(&str_raw)
.map_err(serde::de::Error::custom)?
.into_iter()
.map(|(k, _)| k)
.collect::<Vec<_>>();
Ok(parts)
}

View File

@ -2,19 +2,24 @@ mod note;
mod user; mod user;
use crate::api_v1::note::handle_note; use crate::api_v1::note::handle_note;
use crate::api_v1::user::{handle_user_info, handle_user_info_by_acct, handle_user_info_self}; use crate::api_v1::user::{
handle_user_by_id_many, handle_user_info, handle_user_info_by_acct, handle_user_info_self,
};
use crate::service::MagnetarService; use crate::service::MagnetarService;
use crate::web::auth; use crate::web::auth;
use crate::web::auth::AuthState; use crate::web::auth::AuthState;
use axum::middleware::from_fn_with_state; use axum::middleware::from_fn_with_state;
use axum::routing::get; use axum::routing::get;
use axum::Router; use axum::Router;
use serde::de::{DeserializeOwned, Error};
use serde::{Deserialize, Deserializer};
use std::sync::Arc; use std::sync::Arc;
pub fn create_api_router(service: Arc<MagnetarService>) -> Router { pub fn create_api_router(service: Arc<MagnetarService>) -> Router {
Router::new() Router::new()
.route("/users/@self", get(handle_user_info_self)) .route("/users/@self", get(handle_user_info_self))
.route("/users/by-acct/:id", get(handle_user_info_by_acct)) .route("/users/by-acct/:id", get(handle_user_info_by_acct))
.route("/users/lookup-many", get(handle_user_by_id_many))
.route("/users/:id", get(handle_user_info)) .route("/users/:id", get(handle_user_info))
.route("/notes/:id", get(handle_note)) .route("/notes/:id", get(handle_note))
.layer(from_fn_with_state( .layer(from_fn_with_state(

View File

@ -1,16 +1,23 @@
use crate::model::processing::user::UserModel; use crate::model::processing::user::UserModel;
use crate::model::processing::PackError;
use crate::model::PackingContext; use crate::model::PackingContext;
use crate::service::MagnetarService; use crate::service::MagnetarService;
use crate::web::auth::{AuthenticatedUser, MaybeUser}; use crate::web::auth::{AuthenticatedUser, MaybeUser};
use crate::web::{ApiError, ObjectNotFound}; use crate::web::{ApiError, ArgumentOutOfRange, ObjectNotFound};
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use axum::Json; use axum::Json;
use futures::StreamExt;
use futures_util::TryStreamExt;
use itertools::Itertools;
use magnetar_common::util::lenient_parse_tag; use magnetar_common::util::lenient_parse_tag;
use magnetar_sdk::endpoints::user::{ use magnetar_sdk::endpoints::user::{
GetUserByAcct, GetUserById, GetUserSelf, UserByIdReq, UserSelfReq, GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, UserByIdReq,
UserSelfReq,
}; };
use magnetar_sdk::endpoints::{Req, Res}; use magnetar_sdk::endpoints::{Req, Res};
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub async fn handle_user_info_self( pub async fn handle_user_info_self(
Query(UserSelfReq { Query(UserSelfReq {
detail: _, detail: _,
@ -82,3 +89,42 @@ pub async fn handle_user_info_by_acct(
let user = UserModel.base_from_existing(&ctx, &user_model).await?; let user = UserModel.base_from_existing(&ctx, &user_model).await?;
Ok(Json(user.into())) Ok(Json(user.into()))
} }
pub async fn handle_user_by_id_many(
Query(ManyUsersByIdReq { id }): Query<Req<GetManyUsersById>>,
State(service): State<Arc<MagnetarService>>,
MaybeUser(user): MaybeUser,
) -> Result<Json<Res<GetManyUsersById>>, ApiError> {
if id.len() >= 100 {
return Err(ArgumentOutOfRange(stringify!(id).to_string()).into());
}
let users = service
.db
.get_many_users_by_id(&id.iter().cloned().sorted().dedup().collect::<Vec<_>>())
.await?;
let ctx = PackingContext::new(service, user.clone()).await?;
let user_model = UserModel;
let futures = users
.iter()
.map(|u| user_model.base_from_existing(&ctx, u))
.collect::<Vec<_>>();
let users_proc = futures::stream::iter(futures)
.buffered(20)
.err_into::<PackError>()
.try_collect::<Vec<_>>()
.await?
.into_iter()
.map(|u| (u.id.0.id.clone(), u))
.collect::<HashMap<_, _>>();
let users_ordered = id
.iter()
.map(|ident| users_proc.get(ident).cloned())
.collect::<Vec<_>>();
Ok(Json(users_ordered))
}

View File

@ -30,6 +30,8 @@ pub enum PackError {
DeserializerError(#[from] serde_json::Error), DeserializerError(#[from] serde_json::Error),
#[error("URL parse error: {0}")] #[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error("Parallel processing error: {0}")]
JoinError(#[from] tokio::task::JoinError),
} }
pub type PackResult<T> = Result<T, PackError>; pub type PackResult<T> = Result<T, PackError>;

View File

@ -134,3 +134,22 @@ impl From<ObjectNotFound> for ApiError {
} }
} }
} }
#[derive(Debug)]
pub struct ArgumentOutOfRange(pub String);
impl From<&ArgumentOutOfRange> for &str {
fn from(_: &ArgumentOutOfRange) -> Self {
"ArgumentOutOfRange"
}
}
impl From<ArgumentOutOfRange> for ApiError {
fn from(err: ArgumentOutOfRange) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
code: err.error_code(),
message: format!("Argument out of range: {}", err.0),
}
}
}