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 = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@ -1455,6 +1456,7 @@ dependencies = [
"compact_str",
"dotenvy",
"either",
"futures",
"futures-util",
"headers",
"hyper",
@ -1590,6 +1592,7 @@ dependencies = [
"magnetar_sdk_macros",
"serde",
"serde_json",
"serde_urlencoded",
"ts-rs",
"unicode-segmentation",
]

View File

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

View File

@ -114,6 +114,16 @@ impl CalckeyModel {
.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(
&self,
token: &str,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,3 +3,4 @@ export { GetTimeline } from "./types/endpoints/GetTimeline";
export { GetUserById } from "./types/endpoints/GetUserById";
export { GetUserByAcct } from "./types/endpoints/GetUserByAcct";
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 }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_urlencoded = { workspace = true }
ts-rs = { workspace = true, features = ["chrono", "chrono-impl"] }

View File

@ -4,7 +4,8 @@ use std::collections::HashSet;
use syn::parse::Parse;
use syn::punctuated::Punctuated;
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 {
@ -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 args_str = args
let args_nested = args
.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<_>>();
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 {
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! {
impl Default for #struct_name {
fn default() -> Self {

View File

@ -1,10 +1,11 @@
use crate::endpoints::Endpoint;
use crate::util_types::deserialize_array_urlenc;
use http::Method;
use magnetar_sdk_macros::Endpoint;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::types::user::{PackUserMaybeAll, PackUserSelfMaybeAll};
use crate::types::user::{PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll};
// Get self
#[derive(Serialize, Deserialize, TS)]
@ -54,6 +55,23 @@ pub struct UserByIdReq {
)]
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
#[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;
#[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))
}
}
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;
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::web::auth;
use crate::web::auth::AuthState;
use axum::middleware::from_fn_with_state;
use axum::routing::get;
use axum::Router;
use serde::de::{DeserializeOwned, Error};
use serde::{Deserialize, Deserializer};
use std::sync::Arc;
pub fn create_api_router(service: Arc<MagnetarService>) -> Router {
Router::new()
.route("/users/@self", get(handle_user_info_self))
.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("/notes/:id", get(handle_note))
.layer(from_fn_with_state(

View File

@ -1,16 +1,23 @@
use crate::model::processing::user::UserModel;
use crate::model::processing::PackError;
use crate::model::PackingContext;
use crate::service::MagnetarService;
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::Json;
use futures::StreamExt;
use futures_util::TryStreamExt;
use itertools::Itertools;
use magnetar_common::util::lenient_parse_tag;
use magnetar_sdk::endpoints::user::{
GetUserByAcct, GetUserById, GetUserSelf, UserByIdReq, UserSelfReq,
GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, UserByIdReq,
UserSelfReq,
};
use magnetar_sdk::endpoints::{Req, Res};
use std::collections::HashMap;
use std::sync::Arc;
pub async fn handle_user_info_self(
Query(UserSelfReq {
detail: _,
@ -82,3 +89,42 @@ pub async fn handle_user_info_by_acct(
let user = UserModel.base_from_existing(&ctx, &user_model).await?;
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),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Parallel processing error: {0}")]
JoinError(#[from] tokio::task::JoinError),
}
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),
}
}
}