From deb7f6ef5efa29cab790d2caabec4dab2f273a39 Mon Sep 17 00:00:00 2001 From: Natty Date: Sun, 7 Jan 2024 23:28:53 +0100 Subject: [PATCH] Refactored note fetching --- Cargo.lock | 2 - Cargo.toml | 2 - Dockerfile | 2 +- ext_calckey_model/Cargo.toml | 1 - ext_calckey_model/src/lib.rs | 10 +- ext_calckey_model/src/model_ext.rs | 92 +++ ext_calckey_model/src/note_model.rs | 579 +++++------------- ext_calckey_model/src/paginated.rs | 95 +++ ext_calckey_model/src/user_model.rs | 83 +++ .../src/types/NoteDetailExt.ts | 4 +- .../magnetar-common/src/types/TimelineType.ts | 2 +- magnetar_sdk/src/endpoints/user.rs | 47 +- magnetar_sdk/src/types/mod.rs | 1 - magnetar_sdk/src/types/user.rs | 9 + src/api_v1/user.rs | 46 +- src/model/data/note.rs | 1 + src/model/mod.rs | 10 +- src/model/processing/note.rs | 121 +++- src/model/processing/user.rs | 96 ++- 19 files changed, 701 insertions(+), 502 deletions(-) create mode 100644 ext_calckey_model/src/model_ext.rs create mode 100644 ext_calckey_model/src/paginated.rs create mode 100644 ext_calckey_model/src/user_model.rs diff --git a/Cargo.lock b/Cargo.lock index 9529d86..48c1fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1507,7 +1507,6 @@ dependencies = [ "hyper", "idna 0.5.0", "itertools 0.12.0", - "lazy_static", "lru", "magnetar_calckey_model", "magnetar_common", @@ -1568,7 +1567,6 @@ dependencies = [ "ext_calckey_model_migration", "futures-core", "futures-util", - "lazy_static", "magnetar_common", "magnetar_sdk", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index c4037c0..050d458 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,6 @@ http = "1.0" hyper = "1.1" idna = "0.5" itertools = "0.12" -lazy_static = "1.4" lru = "0.12" miette = "5.9" nom = "7" @@ -108,7 +107,6 @@ either = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } itertools = { workspace = true } -lazy_static = { workspace = true } miette = { workspace = true, features = ["fancy"] } strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } diff --git a/Dockerfile b/Dockerfile index 4414e12..64fa696 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/rust:1.74-bookworm as build +FROM docker.io/rust:1.75-bookworm as build RUN update-ca-certificates diff --git a/ext_calckey_model/Cargo.toml b/ext_calckey_model/Cargo.toml index 801f07d..62acb6a 100644 --- a/ext_calckey_model/Cargo.toml +++ b/ext_calckey_model/Cargo.toml @@ -16,7 +16,6 @@ magnetar_sdk = { path = "../magnetar_sdk" } dotenvy = { workspace = true} futures-core = { workspace = true } futures-util = { workspace = true } -lazy_static = { workspace = true } tokio = { workspace = true, features = ["full"] } tokio-util = { workspace = true} redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"]} diff --git a/ext_calckey_model/src/lib.rs b/ext_calckey_model/src/lib.rs index dd000ad..b8f267d 100644 --- a/ext_calckey_model/src/lib.rs +++ b/ext_calckey_model/src/lib.rs @@ -1,10 +1,14 @@ pub mod emoji; +pub mod model_ext; pub mod note_model; +pub mod paginated; pub mod poll; +pub mod user_model; pub use ck; use ck::*; pub use sea_orm; +use user_model::UserResolver; use crate::note_model::NoteResolver; use chrono::Utc; @@ -307,7 +311,11 @@ impl CalckeyModel { } pub fn get_note_resolver(&self) -> NoteResolver { - NoteResolver::new(self.clone()) + NoteResolver::new(self.clone(), self.get_user_resolver()) + } + + pub fn get_user_resolver(&self) -> UserResolver { + UserResolver::new(self.clone()) } } diff --git a/ext_calckey_model/src/model_ext.rs b/ext_calckey_model/src/model_ext.rs new file mode 100644 index 0000000..d9ce5dc --- /dev/null +++ b/ext_calckey_model/src/model_ext.rs @@ -0,0 +1,92 @@ +use ext_calckey_model_migration::{Alias, Expr, IntoIden, SelectExpr, SelectStatement, TableRef}; +use sea_orm::{ColumnTrait, Condition, EntityTrait, Iden, Iterable, JoinType, RelationDef}; + +pub(crate) trait SelectColumnsExt { + fn add_aliased_columns(&mut self, alias: &str) -> &mut Self; + + fn join_columns( + &mut self, + join: JoinType, + rel: RelationDef, + alias: I, + ) -> &mut Self; +} + +pub(crate) fn join_columns_default(rel: RelationDef) -> Condition { + let tbl_id = |tbl: &TableRef| { + match tbl { + TableRef::Table(id) => id, + TableRef::TableAlias(_, id) => id, + _ => unreachable!(), + } + .clone() + }; + + let mut cond = Condition::all(); + for (owner_key, foreign_key) in rel.from_col.into_iter().zip(rel.to_col.into_iter()) { + cond = cond.add( + Expr::col((tbl_id(&rel.from_tbl), owner_key)) + .equals((tbl_id(&rel.to_tbl), foreign_key)), + ); + } + cond +} + +impl SelectColumnsExt for SelectStatement { + fn add_aliased_columns(&mut self, iden: &str) -> &mut Self { + for col in T::Column::iter() { + let column: &T::Column = &col; + + let alias = format!("{}{}", iden, col.to_string()); + + let column_ref = Expr::col((Alias::new(iden), column.as_column_ref().1)); + + self.expr(SelectExpr { + expr: col.select_as(column_ref), + alias: Some(Alias::new(&alias).into_iden()), + window: None, + }); + } + + self + } + + fn join_columns( + &mut self, + join: JoinType, + mut rel: RelationDef, + alias: I, + ) -> &mut Self { + let alias = alias.into_iden(); + rel.to_tbl = rel.to_tbl.alias(alias); + self.join(join, rel.to_tbl.clone(), join_columns_default(rel)); + self + } +} + +pub(crate) fn joined_prefix_str(prefix: &str, suffix: &str) -> String { + format!("{prefix}{suffix}") +} + +pub(crate) fn joined_prefix_alias(prefix: &str, suffix: &str) -> Alias { + Alias::new(joined_prefix_str(prefix, suffix)) +} + +pub(crate) fn joined_prefix(prefix: &str, suffix: &str) -> impl IntoIden { + joined_prefix_alias(prefix, suffix).into_iden() +} + +pub(crate) trait EntityPrefixExt { + fn base_prefix_str(&self) -> String; + fn base_prefix(&self) -> impl IntoIden; +} + +impl EntityPrefixExt for T { + fn base_prefix_str(&self) -> String { + format!("{}.", self.table_name()) + } + + fn base_prefix(&self) -> impl IntoIden { + Alias::new(self.base_prefix_str()).into_iden() + } +} diff --git a/ext_calckey_model/src/note_model.rs b/ext_calckey_model/src/note_model.rs index dbe3c0a..aa21c89 100644 --- a/ext_calckey_model/src/note_model.rs +++ b/ext_calckey_model/src/note_model.rs @@ -1,16 +1,21 @@ -use lazy_static::lazy_static; +use ext_calckey_model_migration::SelectStatement; use sea_orm::sea_query::{ Alias, Asterisk, Expr, IntoCondition, IntoIden, Query, SelectExpr, SimpleExpr, }; use sea_orm::{ - ColumnTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, JoinType, - QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, + DbErr, EntityName, EntityTrait, FromQueryResult, Iden, JoinType, QueryFilter, QueryOrder, + QueryResult, QuerySelect, QueryTrait, Select, }; use serde::{Deserialize, Serialize}; -use ck::{drive_file, note, note_reaction, user, user_note_pining}; -use magnetar_sdk::types::{EndFilter, RangeFilter, SpanFilter, StartFilter}; +use ck::{note, note_reaction, user, user_note_pining}; +use magnetar_sdk::types::SpanFilter; +use crate::model_ext::{ + joined_prefix, joined_prefix_alias, joined_prefix_str, EntityPrefixExt, SelectColumnsExt, +}; +use crate::paginated::PaginatedModel; +use crate::user_model::{UserData, UserResolver}; use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel}; pub mod sub_interaction_renote { @@ -48,9 +53,7 @@ pub struct NoteData { pub note: note::Model, pub interaction_user_renote: Option, pub interaction_user_reaction: Option, - pub user: user::Model, - pub avatar: Option, - pub banner: Option, + pub user: UserData, pub reply: Option>, pub renote: Option>, } @@ -59,125 +62,36 @@ const PIN: &str = "pin."; const INTERACTION_REACTION: &str = "interaction.reaction."; const INTERACTION_RENOTE: &str = "interaction.renote."; const USER: &str = "user."; -const USER_AVATAR: &str = "user.avatar."; -const USER_BANNER: &str = "user.banner."; const REPLY: &str = "reply."; -const REPLY_INTERACTION_REACTION: &str = "reply.interaction.reaction."; -const REPLY_INTERACTION_RENOTE: &str = "reply.interaction.renote."; -const REPLY_USER: &str = "reply.user."; -const REPLY_USER_AVATAR: &str = "reply.user.avatar."; -const REPLY_USER_BANNER: &str = "reply.user.banner."; const RENOTE: &str = "renote."; -const RENOTE_INTERACTION_REACTION: &str = "renote.interaction.reaction."; -const RENOTE_INTERACTION_RENOTE: &str = "renote.interaction.renote."; -const RENOTE_USER: &str = "renote.user."; -const RENOTE_USER_AVATAR: &str = "renote.user.avatar."; -const RENOTE_USER_BANNER: &str = "renote.user.banner."; -const RENOTE_REPLY: &str = "renote.reply."; -const RENOTE_REPLY_INTERACTION_REACTION: &str = "renote.reply.interaction.reaction."; -const RENOTE_REPLY_INTERACTION_RENOTE: &str = "renote.reply.interaction.renote."; -const RENOTE_REPLY_USER: &str = "renote.reply.user."; -const RENOTE_REPLY_USER_AVATAR: &str = "renote.reply.user.avatar."; -const RENOTE_REPLY_USER_BANNER: &str = "renote.reply.user.banner."; impl FromQueryResult for NoteData { - fn from_query_result(res: &QueryResult, _pre: &str) -> Result { - let reply = note::Model::from_query_result_optional(res, REPLY)? - .map::, _>(|r| { - Ok(Box::new(NoteData { - note: r, - interaction_user_renote: - sub_interaction_renote::Model::from_query_result_optional( - res, - REPLY_INTERACTION_RENOTE, - )?, - interaction_user_reaction: - sub_interaction_reaction::Model::from_query_result_optional( - res, - REPLY_INTERACTION_REACTION, - )?, - user: user::Model::from_query_result(res, REPLY_USER)?, - avatar: drive_file::Model::from_query_result_optional(res, REPLY_USER_AVATAR)?, - banner: drive_file::Model::from_query_result_optional(res, REPLY_USER_BANNER)?, - reply: None, - renote: None, - })) - }) - .transpose()?; - - let renote_reply = note::Model::from_query_result_optional(res, RENOTE_REPLY)? - .map::, _>(|r| { - Ok(Box::new(NoteData { - note: r, - interaction_user_renote: - sub_interaction_renote::Model::from_query_result_optional( - res, - RENOTE_REPLY_INTERACTION_RENOTE, - )?, - interaction_user_reaction: - sub_interaction_reaction::Model::from_query_result_optional( - res, - RENOTE_REPLY_INTERACTION_REACTION, - )?, - user: user::Model::from_query_result(res, RENOTE_REPLY_USER)?, - avatar: drive_file::Model::from_query_result_optional( - res, - RENOTE_REPLY_USER_AVATAR, - )?, - banner: drive_file::Model::from_query_result_optional( - res, - RENOTE_REPLY_USER_BANNER, - )?, - reply: None, - renote: None, - })) - }) - .transpose()?; - - let renote = note::Model::from_query_result_optional(res, RENOTE)? - .map::, _>(|r| { - Ok(Box::new(NoteData { - note: r, - interaction_user_renote: - sub_interaction_renote::Model::from_query_result_optional( - res, - RENOTE_INTERACTION_RENOTE, - )?, - interaction_user_reaction: - sub_interaction_reaction::Model::from_query_result_optional( - res, - RENOTE_INTERACTION_REACTION, - )?, - user: user::Model::from_query_result(res, RENOTE_USER)?, - avatar: drive_file::Model::from_query_result_optional(res, RENOTE_USER_AVATAR)?, - banner: drive_file::Model::from_query_result_optional(res, RENOTE_USER_BANNER)?, - reply: renote_reply, - renote: None, - })) - }) - .transpose()?; + fn from_query_result(res: &QueryResult, prefix: &str) -> Result { + let fallback = format!("{}.", note::Entity.table_name()); + let prefix = if prefix.is_empty() { &fallback } else { prefix }; Ok(NoteData { - note: note::Model::from_query_result(res, "")?, + note: note::Model::from_query_result(res, prefix)?, interaction_user_renote: sub_interaction_renote::Model::from_query_result_optional( res, - INTERACTION_RENOTE, + &joined_prefix_str(prefix, INTERACTION_RENOTE), )?, interaction_user_reaction: sub_interaction_reaction::Model::from_query_result_optional( res, - INTERACTION_REACTION, + &joined_prefix_str(prefix, INTERACTION_REACTION), )?, - user: user::Model::from_query_result(res, USER)?, - avatar: drive_file::Model::from_query_result_optional(res, USER_AVATAR)?, - banner: drive_file::Model::from_query_result_optional(res, USER_BANNER)?, - reply, - renote, + user: UserData::from_query_result(res, &joined_prefix_str(prefix, USER))?, + reply: NoteData::from_query_result_optional(res, &joined_prefix_str(prefix, REPLY))? + .map(Box::new), + renote: NoteData::from_query_result_optional(res, &joined_prefix_str(prefix, RENOTE))? + .map(Box::new), }) } } pub struct NoteResolver { db: CalckeyModel, + user_resolver: UserResolver, } pub trait NoteVisibilityFilterFactory: Send + Sync { @@ -196,55 +110,29 @@ pub struct NoteResolveOptions { pub only_pins_from: Option, // User ID } -trait SelectColumnsExt { - fn add_aliased_columns(self, alias: Option<&str>, entity: T) -> Self; - - fn add_sub_select_reaction( - self, - source_note_alias: Option<&str>, +trait SelectNoteInteractionsExt { + fn add_sub_select_interaction_reaction( + &mut self, + source_note_alias: &str, alias: &str, user_id: &str, - ) -> Select; + ) -> &mut Self; - fn add_sub_select_renote( - self, - source_note_alias: Option<&str>, + fn add_sub_select_interaction_renote( + &mut self, + source_note_alias: &str, alias: &str, user_id: &str, - ) -> Select; + ) -> &mut Self; } -impl SelectColumnsExt for Select { - fn add_aliased_columns( - mut self: Select, - alias: Option<&str>, - entity: T, - ) -> Select { - for col in T::Column::iter() { - let column: &T::Column = &col; - - let iden = alias.unwrap_or_else(|| entity.table_name()); - let alias = format!("{}{}", iden, col.to_string()); - - let column_ref = Expr::col((Alias::new(iden), column.as_column_ref().1)); - - QuerySelect::query(&mut self).expr(SelectExpr { - expr: col.select_as(column_ref), - alias: Some(Alias::new(&alias).into_iden()), - window: None, - }); - } - - self - } - - fn add_sub_select_reaction( - mut self: Select, - source_note_alias: Option<&str>, +impl SelectNoteInteractionsExt for SelectStatement { + fn add_sub_select_interaction_reaction( + &mut self, + iden: &str, prefix_alias: &str, user_id: &str, - ) -> Select { - let iden = source_note_alias.unwrap_or_else(|| note::Entity.table_name()); + ) -> &mut Self { let note_id_col = Expr::col((Alias::new(iden), note::Column::Id)); let column = sub_interaction_reaction::Column::ReactionName; @@ -262,21 +150,19 @@ impl SelectColumnsExt for Select { .take() .into_sub_query_statement(); - QuerySelect::query(&mut self).expr(SimpleExpr::SubQuery(None, Box::new(sub_query))); - + self.expr(SimpleExpr::SubQuery(None, Box::new(sub_query))); self } - fn add_sub_select_renote( - mut self: Select, - source_note_alias: Option<&str>, + fn add_sub_select_interaction_renote( + &mut self, + iden: &str, prefix_alias: &str, user_id: &str, - ) -> Select { - let iden = source_note_alias.unwrap_or_else(|| note::Entity.table_name()); + ) -> &mut Self { let note_id_col = Expr::col((Alias::new(iden), note::Column::Id)); - let renote_note_tbl = Alias::new(format!("{}{}", prefix_alias, "note")); + let renote_note_tbl = joined_prefix_alias(prefix_alias, "note"); let column = sub_interaction_renote::Column::Renotes; let alias = format!("{}{}", prefix_alias, column.to_string()); @@ -295,99 +181,24 @@ impl SelectColumnsExt for Select { .take() .into_sub_query_statement(); - QuerySelect::query(&mut self).expr(SimpleExpr::SubQuery(None, Box::new(sub_query))); - + self.expr(SimpleExpr::SubQuery(None, Box::new(sub_query))); self } } -lazy_static! { - static ref ALIAS_PIN: Alias = Alias::new(PIN); - static ref ALIAS_INTERACTION_RENOTE: Alias = Alias::new(INTERACTION_RENOTE); - static ref ALIAS_INTERACTION_REACTION: Alias = Alias::new(INTERACTION_REACTION); - static ref ALIAS_USER: Alias = Alias::new(USER); - static ref ALIAS_USER_AVATAR: Alias = Alias::new(USER_AVATAR); - static ref ALIAS_USER_BANNER: Alias = Alias::new(USER_BANNER); - static ref ALIAS_REPLY: Alias = Alias::new(REPLY); - static ref ALIAS_REPLY_INTERACTION_RENOTE: Alias = Alias::new(REPLY_INTERACTION_RENOTE); - static ref ALIAS_REPLY_INTERACTION_REACTION: Alias = Alias::new(REPLY_INTERACTION_REACTION); - static ref ALIAS_REPLY_USER: Alias = Alias::new(REPLY_USER); - static ref ALIAS_REPLY_USER_AVATAR: Alias = Alias::new(REPLY_USER_AVATAR); - static ref ALIAS_REPLY_USER_BANNER: Alias = Alias::new(REPLY_USER_BANNER); - static ref ALIAS_RENOTE: Alias = Alias::new(RENOTE); - static ref ALIAS_RENOTE_INTERACTION_RENOTE: Alias = Alias::new(RENOTE_INTERACTION_RENOTE); - static ref ALIAS_RENOTE_INTERACTION_REACTION: Alias = Alias::new(RENOTE_INTERACTION_REACTION); - static ref ALIAS_RENOTE_USER: Alias = Alias::new(RENOTE_USER); - static ref ALIAS_RENOTE_USER_AVATAR: Alias = Alias::new(RENOTE_USER_AVATAR); - static ref ALIAS_RENOTE_USER_BANNER: Alias = Alias::new(RENOTE_USER_BANNER); - static ref ALIAS_RENOTE_REPLY: Alias = Alias::new(RENOTE_REPLY); - static ref ALIAS_RENOTE_REPLY_INTERACTION_RENOTE: Alias = - Alias::new(RENOTE_REPLY_INTERACTION_RENOTE); - static ref ALIAS_RENOTE_REPLY_INTERACTION_REACTION: Alias = - Alias::new(RENOTE_REPLY_INTERACTION_REACTION); - static ref ALIAS_RENOTE_REPLY_USER: Alias = Alias::new(RENOTE_REPLY_USER); - static ref ALIAS_RENOTE_REPLY_USER_AVATAR: Alias = Alias::new(RENOTE_REPLY_USER_AVATAR); - static ref ALIAS_RENOTE_REPLY_USER_BANNER: Alias = Alias::new(RENOTE_REPLY_USER_BANNER); -} - -fn range_into_expr(filter: &SpanFilter) -> Option { - match filter { - SpanFilter::Range(RangeFilter { - time_start, - time_end, - id_start, - id_end, - }) => Some( - Expr::tuple([ - Expr::col(note::Column::CreatedAt).into(), - Expr::col(note::Column::Id).into(), - ]) - .between( - Expr::tuple([ - Expr::value(time_start.clone()), - Expr::value(id_start.clone()), - ]), - Expr::tuple([Expr::value(time_end.clone()), Expr::value(id_end.clone())]), - ), - ), - SpanFilter::Start(StartFilter { - id_start, - time_start, - }) => Some( - Expr::tuple([ - Expr::col(note::Column::CreatedAt).into(), - Expr::col(note::Column::Id).into(), - ]) - .gt(Expr::tuple([ - Expr::value(time_start.clone()), - Expr::value(id_start.clone()), - ])), - ), - SpanFilter::End(EndFilter { id_end, time_end }) => Some( - Expr::tuple([ - Expr::col(note::Column::CreatedAt).into(), - Expr::col(note::Column::Id).into(), - ]) - .lt(Expr::tuple([ - Expr::value(time_end.clone()), - Expr::value(id_end.clone()), - ])), - ), - SpanFilter::None(_) => None, - } -} - fn ids_into_expr(ids: &Vec) -> SimpleExpr { + let id_col = Expr::col((note::Entity.base_prefix(), note::Column::Id)); + if ids.len() == 1 { - note::Column::Id.eq(&ids[0]) + id_col.eq(&ids[0]) } else { - note::Column::Id.is_in(ids) + id_col.is_in(ids) } } impl NoteResolver { - pub fn new(db: CalckeyModel) -> Self { - NoteResolver { db } + pub fn new(db: CalckeyModel, user_resolver: UserResolver) -> Self { + NoteResolver { db, user_resolver } } pub async fn get_one( @@ -396,7 +207,10 @@ impl NoteResolver { ) -> Result, CalckeyDbError> { let select = self.resolve(options); let visibility_filter = options.visibility_filter.with_note_and_user_tables(None); - let time_filter = options.time_range.as_ref().and_then(range_into_expr); + let time_filter = options + .time_range + .as_ref() + .and_then(note::Model::range_into_expr); let id_filter = options.ids.as_ref().map(ids_into_expr); let notes = select @@ -416,7 +230,10 @@ impl NoteResolver { ) -> Result, CalckeyDbError> { let select = self.resolve(options); let visibility_filter = options.visibility_filter.with_note_and_user_tables(None); - let time_filter = options.time_range.as_ref().and_then(range_into_expr); + let time_filter = options + .time_range + .as_ref() + .and_then(note::Model::range_into_expr); let id_filter = options.ids.as_ref().map(ids_into_expr); let notes = select @@ -425,7 +242,7 @@ impl NoteResolver { .apply_if(time_filter, Select::::filter) .apply_if(options.only_pins_from.as_deref(), |s, _| { s.order_by_desc(Expr::col(( - ALIAS_PIN.clone(), + joined_prefix(¬e::Entity.base_prefix_str(), PIN), user_note_pining::Column::CreatedAt, ))) }) @@ -437,201 +254,111 @@ impl NoteResolver { Ok(notes) } - pub fn resolve(&self, options: &NoteResolveOptions) -> Select { - let mut select = note::Entity::find().add_aliased_columns(Some(USER), user::Entity); + fn attach_note( + &self, + q: &mut SelectStatement, + prefix: &str, + reply_depth: usize, + renote_depth: usize, + options: &NoteResolveOptions, + user_resolver: &UserResolver, + ) { + q.add_aliased_columns::(prefix); - if let Some(pins_user) = options.only_pins_from.clone() { + // Add the note's author + q.add_aliased_columns::(&joined_prefix_str(prefix, USER)); + q.join_columns( + JoinType::LeftJoin, + note::Relation::User.with_alias(Alias::new(prefix).into_iden()), + joined_prefix(prefix, USER), + ); + + // Interactions like renotes or reactions from the specified user + if let Some(user_id) = &options.with_interactions_from { + q.add_sub_select_interaction_reaction( + prefix, + &joined_prefix_str(prefix, INTERACTION_REACTION), + user_id, + ); + q.add_sub_select_interaction_renote( + prefix, + &joined_prefix_str(prefix, INTERACTION_RENOTE), + user_id, + ); + } + + // Recursively attach reply parents + if reply_depth > 0 { + q.join_columns( + JoinType::LeftJoin, + note::Relation::SelfRef2.with_alias(Alias::new(prefix).into_iden()), + joined_prefix(prefix, REPLY), + ); + + self.attach_note( + q, + &joined_prefix_str(prefix, REPLY), + reply_depth - 1, + renote_depth, + options, + user_resolver, + ); + } + + // Recursively attach renote/quote targets + if renote_depth > 0 { + q.join_columns( + JoinType::LeftJoin, + note::Relation::SelfRef1.with_alias(Alias::new(prefix).into_iden()), + joined_prefix(prefix, RENOTE), + ); + + self.attach_note( + q, + &joined_prefix_str(prefix, RENOTE), + reply_depth, + renote_depth - 1, + options, + user_resolver, + ); + } + } + + pub fn resolve(&self, options: &NoteResolveOptions) -> Select { + let mut select = note::Entity::find(); + + let prefix = note::Entity.base_prefix_str(); + + let query = QuerySelect::query(&mut select); + query.clear_selects(); + query.from_as(note::Entity, Alias::new(&prefix)); + + if let Some(pins_user) = &options.only_pins_from { select = select .join_as( JoinType::InnerJoin, - note::Relation::UserNotePining.def(), - ALIAS_PIN.clone(), + note::Relation::UserNotePining.with_alias(Alias::new(&prefix)), + joined_prefix(&prefix, PIN), ) .filter( - Expr::col((ALIAS_PIN.clone(), user_note_pining::Column::UserId)) - .eq(&pins_user) - .into_condition(), + Expr::col(( + joined_prefix(&prefix, PIN), + user_note_pining::Column::UserId, + )) + .eq(pins_user) + .into_condition(), ) } - if let Some(user_id) = &options.with_interactions_from { - select = select - .add_sub_select_reaction(None, INTERACTION_REACTION, user_id) - .add_sub_select_renote(None, INTERACTION_RENOTE, user_id); - } - - if options.with_user { - select = select - .add_aliased_columns(Some(USER_AVATAR), drive_file::Entity) - .add_aliased_columns(Some(USER_BANNER), drive_file::Entity); - } - - if options.with_reply_target { - select = select - .add_aliased_columns(Some(REPLY), note::Entity) - .add_aliased_columns(Some(REPLY_USER), user::Entity); - - if let Some(user_id) = &options.with_interactions_from { - select = select - .add_sub_select_reaction(Some(REPLY), REPLY_INTERACTION_REACTION, user_id) - .add_sub_select_renote(Some(REPLY), REPLY_INTERACTION_RENOTE, user_id); - } - - if options.with_user { - select = select - .add_aliased_columns(Some(REPLY_USER_AVATAR), drive_file::Entity) - .add_aliased_columns(Some(REPLY_USER_BANNER), drive_file::Entity); - } - } - - if options.with_renote_target { - select = select - .add_aliased_columns(Some(RENOTE), note::Entity) - .add_aliased_columns(Some(RENOTE_USER), user::Entity); - - if let Some(user_id) = &options.with_interactions_from { - select = select - .add_sub_select_reaction(Some(RENOTE), RENOTE_INTERACTION_REACTION, user_id) - .add_sub_select_renote(Some(RENOTE), RENOTE_INTERACTION_RENOTE, user_id); - } - - if options.with_user { - select = select - .add_aliased_columns(Some(RENOTE_USER_AVATAR), drive_file::Entity) - .add_aliased_columns(Some(RENOTE_USER_BANNER), drive_file::Entity) - } - - if options.with_reply_target { - select = select - .add_aliased_columns(Some(RENOTE_REPLY), note::Entity) - .add_aliased_columns(Some(RENOTE_REPLY_USER), user::Entity); - - if let Some(user_id) = &options.with_interactions_from { - select = select - .add_sub_select_reaction( - Some(RENOTE_REPLY), - RENOTE_REPLY_INTERACTION_REACTION, - user_id, - ) - .add_sub_select_renote( - Some(RENOTE_REPLY), - RENOTE_REPLY_INTERACTION_RENOTE, - user_id, - ); - } - - if options.with_user { - select = select - .add_aliased_columns(Some(RENOTE_REPLY_USER_AVATAR), drive_file::Entity) - .add_aliased_columns(Some(RENOTE_REPLY_USER_BANNER), drive_file::Entity); - } - } - } - - if options.with_reply_target { - select = select - .join_as( - JoinType::LeftJoin, - note::Relation::SelfRef2.def(), - ALIAS_REPLY.clone(), - ) - .join_as( - JoinType::LeftJoin, - note::Relation::User.with_alias(ALIAS_REPLY.clone()), - ALIAS_REPLY_USER.clone(), - ); - } - - if options.with_renote_target { - select = select - .join_as( - JoinType::LeftJoin, - note::Relation::SelfRef1.def(), - ALIAS_RENOTE.clone(), - ) - .join_as( - JoinType::LeftJoin, - note::Relation::User.with_alias(ALIAS_RENOTE.clone()), - ALIAS_RENOTE_USER.clone(), - ); - - if options.with_reply_target { - select = select - .join_as( - JoinType::LeftJoin, - note::Relation::SelfRef2.with_alias(ALIAS_RENOTE.clone()), - ALIAS_RENOTE_REPLY.clone(), - ) - .join_as( - JoinType::LeftJoin, - note::Relation::User.with_alias(ALIAS_RENOTE_REPLY.clone()), - ALIAS_RENOTE_REPLY_USER.clone(), - ); - } - } - - select = select.join_as( - JoinType::InnerJoin, - note::Relation::User.def(), - ALIAS_USER.clone(), + self.attach_note( + QuerySelect::query(&mut select), + &prefix, + options.with_reply_target.then_some(1).unwrap_or_default(), + options.with_renote_target.then_some(1).unwrap_or_default(), + options, + &self.user_resolver, ); - if options.with_user { - select = select - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile2.with_alias(ALIAS_USER.clone()), - ALIAS_USER_AVATAR.clone(), - ) - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile1.with_alias(ALIAS_USER.clone()), - ALIAS_USER_BANNER.clone(), - ); - - if options.with_reply_target { - select = select - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile2.with_alias(ALIAS_REPLY_USER.clone()), - ALIAS_REPLY_USER_AVATAR.clone(), - ) - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile1.with_alias(ALIAS_REPLY_USER.clone()), - ALIAS_REPLY_USER_BANNER.clone(), - ); - } - - if options.with_renote_target { - select = select - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile2.with_alias(ALIAS_RENOTE_USER.clone()), - ALIAS_RENOTE_USER_AVATAR.clone(), - ) - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile1.with_alias(ALIAS_RENOTE_USER.clone()), - ALIAS_RENOTE_USER_BANNER.clone(), - ); - - if options.with_reply_target { - select = select - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile2.with_alias(ALIAS_RENOTE_REPLY_USER.clone()), - ALIAS_RENOTE_REPLY_USER_AVATAR.clone(), - ) - .join_as( - JoinType::LeftJoin, - user::Relation::DriveFile1.with_alias(ALIAS_RENOTE_REPLY_USER.clone()), - ALIAS_RENOTE_REPLY_USER_BANNER.clone(), - ); - } - } - } - select } } diff --git a/ext_calckey_model/src/paginated.rs b/ext_calckey_model/src/paginated.rs new file mode 100644 index 0000000..bb92354 --- /dev/null +++ b/ext_calckey_model/src/paginated.rs @@ -0,0 +1,95 @@ +use ext_calckey_model_migration::{Expr, SimpleExpr}; +use magnetar_sdk::types::{EndFilter, RangeFilter, SpanFilter, StartFilter}; +use sea_orm::Iden; + +pub trait PaginatedModel: 'static { + fn time_column() -> impl Iden; + fn id_column() -> impl Iden; + + fn range_into_expr(filter: &SpanFilter) -> Option { + match filter { + SpanFilter::Range(RangeFilter { + time_start, + time_end, + id_start, + id_end, + }) => Some( + Expr::tuple([ + Expr::col(Self::time_column()).into(), + Expr::col(Self::id_column()).into(), + ]) + .between( + Expr::tuple([ + Expr::value(time_start.clone()), + Expr::value(id_start.clone()), + ]), + Expr::tuple([Expr::value(time_end.clone()), Expr::value(id_end.clone())]), + ), + ), + SpanFilter::Start(StartFilter { + id_start, + time_start, + }) => Some( + Expr::tuple([ + Expr::col(Self::time_column()).into(), + Expr::col(Self::id_column()).into(), + ]) + .gt(Expr::tuple([ + Expr::value(time_start.clone()), + Expr::value(id_start.clone()), + ])), + ), + SpanFilter::End(EndFilter { id_end, time_end }) => Some( + Expr::tuple([ + Expr::col(Self::time_column()).into(), + Expr::col(Self::id_column()).into(), + ]) + .lt(Expr::tuple([ + Expr::value(time_end.clone()), + Expr::value(id_end.clone()), + ])), + ), + SpanFilter::None(_) => None, + } + } +} + +impl PaginatedModel for ck::note::Model { + fn time_column() -> impl Iden { + ck::note::Column::CreatedAt + } + + fn id_column() -> impl Iden { + ck::note::Column::Id + } +} + +impl PaginatedModel for ck::user::Model { + fn time_column() -> impl Iden { + ck::user::Column::CreatedAt + } + + fn id_column() -> impl Iden { + ck::user::Column::Id + } +} + +impl PaginatedModel for ck::following::Model { + fn time_column() -> impl Iden { + ck::following::Column::CreatedAt + } + + fn id_column() -> impl Iden { + ck::following::Column::Id + } +} + +impl PaginatedModel for ck::follow_request::Model { + fn time_column() -> impl Iden { + ck::follow_request::Column::CreatedAt + } + + fn id_column() -> impl Iden { + ck::follow_request::Column::Id + } +} diff --git a/ext_calckey_model/src/user_model.rs b/ext_calckey_model/src/user_model.rs new file mode 100644 index 0000000..7ad85a0 --- /dev/null +++ b/ext_calckey_model/src/user_model.rs @@ -0,0 +1,83 @@ +use crate::model_ext::EntityPrefixExt; +use crate::{ + model_ext::{joined_prefix, joined_prefix_str, SelectColumnsExt}, + AliasSourceExt, CalckeyModel, +}; +use ck::{drive_file, follow_request, following, user}; +use ext_calckey_model_migration::{Alias, SelectStatement}; +use sea_orm::{DbErr, EntityName, FromQueryResult, JoinType, QueryResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserData { + pub user: user::Model, + pub avatar: Option, + pub banner: Option, +} + +pub struct UserResolveOptions { + pub with_avatar_and_banner: bool, +} + +const AVATAR: &str = "avatar."; +const BANNER: &str = "banner."; + +impl FromQueryResult for UserData { + fn from_query_result(res: &QueryResult, prefix: &str) -> Result { + let fallback = user::Entity.base_prefix_str(); + let prefix = if prefix.is_empty() { &fallback } else { prefix }; + + Ok(UserData { + user: user::Model::from_query_result(res, prefix)?, + avatar: drive_file::Model::from_query_result_optional( + res, + &joined_prefix_str(prefix, AVATAR), + )?, + banner: drive_file::Model::from_query_result_optional( + res, + &joined_prefix_str(prefix, BANNER), + )?, + }) + } +} + +pub struct UserResolver { + db: CalckeyModel, +} + +impl UserResolver { + pub fn new(db: CalckeyModel) -> Self { + Self { db } + } + + pub fn resolve( + &self, + q: &mut SelectStatement, + prefix: &str, + UserResolveOptions { + with_avatar_and_banner, + }: &UserResolveOptions, + ) { + q.add_aliased_columns::(prefix); + + if *with_avatar_and_banner { + q.add_aliased_columns::(&joined_prefix_str(prefix, AVATAR)) + .add_aliased_columns::(&joined_prefix_str(prefix, BANNER)); + + q.join_columns( + JoinType::LeftJoin, + user::Relation::DriveFile2.with_alias(Alias::new(prefix)), + joined_prefix(prefix, AVATAR), + ) + .join_columns( + JoinType::LeftJoin, + user::Relation::DriveFile1.with_alias(Alias::new(prefix)), + joined_prefix(prefix, BANNER), + ); + } + } + + pub async fn get_followers(&self, user_id: &str) -> Vec { + todo!() + } +} diff --git a/fe_calckey/frontend/magnetar-common/src/types/NoteDetailExt.ts b/fe_calckey/frontend/magnetar-common/src/types/NoteDetailExt.ts index 3482627..60fcaa3 100644 --- a/fe_calckey/frontend/magnetar-common/src/types/NoteDetailExt.ts +++ b/fe_calckey/frontend/magnetar-common/src/types/NoteDetailExt.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PackNoteMaybeAttachments } from "./packed/PackNoteMaybeAttachments"; +import type { PackNoteMaybeFull } from "./packed/PackNoteMaybeFull"; -export interface NoteDetailExt { parent_note: PackNoteMaybeAttachments | null, renoted_note: PackNoteMaybeAttachments | null, } \ No newline at end of file +export interface NoteDetailExt { parent_note: PackNoteMaybeFull | null, renoted_note: PackNoteMaybeFull | null, } \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/TimelineType.ts b/fe_calckey/frontend/magnetar-common/src/types/TimelineType.ts index 8b2bb24..f0f19ef 100644 --- a/fe_calckey/frontend/magnetar-common/src/types/TimelineType.ts +++ b/fe_calckey/frontend/magnetar-common/src/types/TimelineType.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type TimelineType = "home" | "timeline" | "recommended" | "hybrid" | "global"; \ No newline at end of file +export type TimelineType = "home" | "recommended" | "hybrid" | "global" | "local"; \ No newline at end of file diff --git a/magnetar_sdk/src/endpoints/user.rs b/magnetar_sdk/src/endpoints/user.rs index be98737..34af5c4 100644 --- a/magnetar_sdk/src/endpoints/user.rs +++ b/magnetar_sdk/src/endpoints/user.rs @@ -1,4 +1,4 @@ -use crate::endpoints::Endpoint; +use crate::endpoints::{Empty, Endpoint}; use crate::util_types::deserialize_array_urlenc; use http::Method; use magnetar_sdk_macros::Endpoint; @@ -82,3 +82,48 @@ pub struct GetManyUsersById; response = "PackUserMaybeAll" )] pub struct GetUserByAcct; + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/users/:id/followers", + method = Method::GET, + request = "Empty", + response = "Vec" +)] +pub struct GetFollowersById; + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/users/:id/following", + method = Method::GET, + request = "Empty", + response = "Vec" +)] +pub struct GetFollowingById; + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/users/@self/followers", + method = Method::GET, + request = "Empty", + response = "Vec" +)] +pub struct GetFollowersSelf; + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/users/@self/following", + method = Method::GET, + request = "Empty", + response = "Vec" +)] +pub struct GetFollowingSelf; + +#[derive(Endpoint)] +#[endpoint( + endpoint = "/users/@self/follow-requests", + method = Method::GET, + request = "Empty", + response = "Vec" +)] +pub struct GetFollowRequestsSelf; diff --git a/magnetar_sdk/src/types/mod.rs b/magnetar_sdk/src/types/mod.rs index 46e899b..e787b45 100644 --- a/magnetar_sdk/src/types/mod.rs +++ b/magnetar_sdk/src/types/mod.rs @@ -7,7 +7,6 @@ pub mod user; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use std::ops::RangeInclusive; use ts_rs::TS; #[derive(Clone, Debug, Deserialize, Serialize, TS)] diff --git a/magnetar_sdk/src/types/user.rs b/magnetar_sdk/src/types/user.rs index c6fdc9b..09acb32 100644 --- a/magnetar_sdk/src/types/user.rs +++ b/magnetar_sdk/src/types/user.rs @@ -99,6 +99,15 @@ pub struct UserProfilePinsEx { // pub pinned_page: Option, } +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub enum UserRelationship { + Follow, + FollowRequest, + Mute, + Block, + RenoteMute, +} + #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct UserRelationExt { diff --git a/src/api_v1/user.rs b/src/api_v1/user.rs index a0526f1..67dafb8 100644 --- a/src/api_v1/user.rs +++ b/src/api_v1/user.rs @@ -1,4 +1,4 @@ -use crate::model::processing::user::UserModel; +use crate::model::processing::user::{UserBorrowedData, UserModel}; use crate::model::processing::PackError; use crate::model::PackingContext; use crate::service::MagnetarService; @@ -11,6 +11,7 @@ use futures_util::TryStreamExt; use itertools::Itertools; use magnetar_common::util::lenient_parse_tag_decode; use magnetar_sdk::endpoints::user::{ + GetFollowRequestsSelf, GetFollowersById, GetFollowersSelf, GetFollowingById, GetFollowingSelf, GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, }; use magnetar_sdk::endpoints::{Req, Res}; @@ -24,7 +25,15 @@ pub async fn handle_user_info_self( ) -> Result>, ApiError> { let ctx = PackingContext::new(service, Some(user.clone())).await?; let user = UserModel - .self_full_from_base(&ctx, user.as_ref(), &req, None, None) + .self_full_from_base( + &ctx, + &UserBorrowedData { + user: user.as_ref(), + avatar: None, + banner: None, + }, + &req, + ) .await?; Ok(Json(user)) } @@ -43,7 +52,15 @@ pub async fn handle_user_info( .ok_or(ObjectNotFound(id))?; let user = UserModel - .foreign_full_from_base(&ctx, &user_model, &req, None, None) + .foreign_full_from_base( + &ctx, + &UserBorrowedData { + user: &user_model, + avatar: None, + banner: None, + }, + &req, + ) .await?; Ok(Json(user)) } @@ -67,7 +84,15 @@ pub async fn handle_user_info_by_acct( .ok_or(ObjectNotFound(tag_str))?; let user = UserModel - .foreign_full_from_base(&ctx, &user_model, &req, None, None) + .foreign_full_from_base( + &ctx, + &UserBorrowedData { + user: &user_model, + avatar: None, + banner: None, + }, + &req, + ) .await?; Ok(Json(user)) } @@ -89,9 +114,18 @@ pub async fn handle_user_by_id_many( let ctx = PackingContext::new(service, user.clone()).await?; let user_model = UserModel; - let futures = users + let user_data = users .iter() - .map(|u| user_model.base_from_existing(&ctx, u, None)) + .map(|user| UserBorrowedData { + user, + avatar: None, + banner: None, + }) + .collect::>(); + + let futures = user_data + .iter() + .map(|user| user_model.base_from_existing(&ctx, user)) .collect::>(); let users_proc = futures::stream::iter(futures) diff --git a/src/model/data/note.rs b/src/model/data/note.rs index 9461fa4..9dd518f 100644 --- a/src/model/data/note.rs +++ b/src/model/data/note.rs @@ -71,6 +71,7 @@ impl PackType> for NoteBase { has_poll: note.has_poll, file_ids: note.file_ids.clone(), emojis: emoji_context.clone(), + is_quote: note.is_quote.unwrap_or_default(), } } } diff --git a/src/model/mod.rs b/src/model/mod.rs index 239ac5b..ed63689 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -3,6 +3,7 @@ use crate::model::processing::PackResult; use crate::service::MagnetarService; use either::Either; use magnetar_calckey_model::{ck, CalckeyDbError}; +use magnetar_sdk::types::user::UserRelationship; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; @@ -21,15 +22,6 @@ impl Default for ProcessingLimits { } } -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -enum UserRelationship { - Follow, - FollowRequest, - Mute, - Block, - RenoteMute, -} - #[derive(Clone, Debug, Hash, PartialEq, Eq)] struct UserRelationshipLink { from: String, diff --git a/src/model/processing/note.rs b/src/model/processing/note.rs index 4b06052..28213d4 100644 --- a/src/model/processing/note.rs +++ b/src/model/processing/note.rs @@ -1,9 +1,11 @@ +use std::sync::Arc; + use crate::model::data::id::BaseId; use crate::model::data::note::{NoteAttachmentSource, NoteBaseSource, NoteDetailSource}; use crate::model::processing::emoji::EmojiModel; use crate::model::processing::user::UserModel; use crate::model::processing::{get_mm_token_emoji, PackError, PackResult}; -use crate::model::{PackType, PackingContext, UserRelationship}; +use crate::model::{PackType, PackingContext}; use compact_str::CompactString; use either::Either; use futures_util::future::{try_join_all, BoxFuture}; @@ -11,7 +13,8 @@ use futures_util::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use magnetar_calckey_model::ck::sea_orm_active_enums::NoteVisibilityEnum; use magnetar_calckey_model::emoji::EmojiTag; use magnetar_calckey_model::note_model::{ - NoteData, NoteResolveOptions, NoteVisibilityFilterFactory, + sub_interaction_reaction, sub_interaction_renote, NoteData, NoteResolveOptions, + NoteVisibilityFilterFactory, }; use magnetar_calckey_model::poll::PollResolver; use magnetar_calckey_model::sea_orm::prelude::Expr; @@ -27,12 +30,14 @@ use magnetar_sdk::types::note::{ PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, Reaction, ReactionPair, ReactionShortcode, ReactionUnicode, ReactionUnknown, }; +use magnetar_sdk::types::user::UserRelationship; use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::{mmm, Optional, Packed, Required}; use serde::Deserialize; use tokio::try_join; use super::drive::DriveModel; +use super::user::UserShapedData; #[derive(Debug, Clone)] pub struct NoteVisibilityFilterSimple(Option); @@ -161,6 +166,80 @@ impl NoteVisibilityFilterModel { } } +pub trait NoteShapedData<'a>: Send + Sync { + fn note(&self) -> &'a ck::note::Model; + fn interaction_user_renote(&self) -> Option<&'a sub_interaction_renote::Model>; + fn interaction_user_reaction(&self) -> Option<&'a sub_interaction_reaction::Model>; + fn user(&self) -> Arc + 'a>; + fn reply(&self) -> Option + 'a>>; + fn renote(&self) -> Option + 'a>>; +} + +pub struct NoteBorrowedData<'a> { + pub note: &'a ck::note::Model, + pub interaction_user_renote: Option<&'a sub_interaction_renote::Model>, + pub interaction_user_reaction: Option<&'a sub_interaction_reaction::Model>, + pub user: Arc + 'a>, + pub reply: Option + 'a>>, + pub renote: Option + 'a>>, +} + +impl<'a> NoteShapedData<'a> for NoteBorrowedData<'a> { + fn note(&self) -> &'a ck::note::Model { + self.note + } + + fn interaction_user_renote(&self) -> Option<&'a sub_interaction_renote::Model> { + self.interaction_user_renote + } + + fn interaction_user_reaction(&self) -> Option<&'a sub_interaction_reaction::Model> { + self.interaction_user_reaction + } + + fn user(&self) -> Arc + 'a> { + self.user.clone() + } + + fn reply(&self) -> Option + 'a>> { + self.reply.as_ref().cloned() + } + + fn renote(&self) -> Option + 'a>> { + self.renote.as_ref().cloned() + } +} + +impl<'a> NoteShapedData<'a> for &'a NoteData { + fn note(&self) -> &'a ck::note::Model { + &self.note + } + + fn interaction_user_renote(&self) -> Option<&'a sub_interaction_renote::Model> { + self.interaction_user_renote.as_ref() + } + + fn interaction_user_reaction(&self) -> Option<&'a sub_interaction_reaction::Model> { + self.interaction_user_reaction.as_ref() + } + + fn user(&self) -> Arc + 'a> { + Arc::new(&self.user) + } + + fn reply(&self) -> Option + 'a>> { + self.reply + .as_deref() + .map(|x| Arc::new(x) as Arc + 'a>) + } + + fn renote(&self) -> Option + 'a>> { + self.renote + .as_deref() + .map(|x| Arc::new(x) as Arc + 'a>) + } +} + struct SpeechTransformNyan; impl SpeechTransformNyan { @@ -194,15 +273,17 @@ impl NoteModel { pub async fn extract_base( &self, ctx: &PackingContext, - note_data: &NoteData, + note_data: &dyn NoteShapedData<'_>, ) -> PackResult { + let note = note_data.note(); + let Required(ref user) = UserModel - .base_from_existing(ctx, ¬e_data.user, None) + .base_from_existing(ctx, note_data.user().as_ref()) .await? .user; - let cw_tok = self.tokenize_note_cw(¬e_data.note); - let mut text_tok = self.tokenize_note_text(¬e_data.note); + let cw_tok = self.tokenize_note_cw(note); + let mut text_tok = self.tokenize_note_text(note); let mut emoji_extracted = Vec::new(); @@ -213,7 +294,7 @@ impl NoteModel { if let Some(text_tok) = &mut text_tok { emoji_extracted.extend_from_slice(&get_mm_token_emoji(text_tok)); - if note_data.user.is_cat && note_data.user.speak_as_cat { + if note_data.user().user().is_cat && note_data.user().user().speak_as_cat { let transformer = SpeechTransformNyan::new(); text_tok.walk_speech_transform(&|text| transformer.transform(text)); } @@ -225,7 +306,7 @@ impl NoteModel { // Parse the JSON into an ordered map and turn it into a Vec of pairs, parsing the reaction codes // Failed reaction parses -> Left, Successful ones -> Right let reactions_raw = - serde_json::Map::::deserialize(¬e_data.note.reactions)? + serde_json::Map::::deserialize(¬e.reactions)? .into_iter() .map(|(ref code, count)| { let reaction = parse_reaction(code) @@ -235,7 +316,7 @@ impl NoteModel { reaction, count, note_data - .interaction_user_reaction + .interaction_user_reaction() .as_ref() .and_then(|r| r.reaction_name.as_deref()) .map(|r| r == code), @@ -266,8 +347,11 @@ impl NoteModel { .emoji_cache .get_many_tagged(&reactions_to_resolve) .map_err(PackError::from); - let emoji_fetch = - emoji_model.fetch_many_emojis(ctx, &shortcodes, note_data.user.host.as_deref()); + let emoji_fetch = emoji_model.fetch_many_emojis( + ctx, + &shortcodes, + note_data.user().user().host.as_deref(), + ); let (reactions_fetched, emojis) = try_join!(reaction_fetch, emoji_fetch)?; @@ -318,7 +402,7 @@ impl NoteModel { let note_base = NoteBase::extract( ctx, NoteBaseSource { - note: ¬e_data.note, + note, cw_mm: cw_tok .as_ref() .map(mmm::to_xml_string) @@ -338,7 +422,7 @@ impl NoteModel { ); Ok(PackNoteBase::pack_from(( - Required(Id::from(note_data.note.id.clone())), + Required(Id::from(note.id.clone())), Required(note_base), ))) } @@ -346,11 +430,10 @@ impl NoteModel { fn extract_interaction( &self, ctx: &PackingContext, - note: &NoteData, + note: &dyn NoteShapedData<'_>, ) -> PackResult> { Ok(note - .interaction_user_renote - .as_ref() + .interaction_user_renote() .map(|renote_info| NoteSelfContextExt::extract(ctx, renote_info))) } @@ -405,12 +488,12 @@ impl NoteModel { &self, ctx: &PackingContext, drive_model: &DriveModel, - note_data: &NoteData, + note_data: &dyn NoteShapedData<'_>, ) -> PackResult { let (PackNoteBase { id, note }, attachments_pack, poll_pack) = try_join!( self.extract_base(ctx, note_data), - self.extract_attachments(ctx, drive_model, ¬e_data.note), - self.extract_poll(ctx, ¬e_data.note) + self.extract_attachments(ctx, drive_model, note_data.note()), + self.extract_poll(ctx, note_data.note()) )?; Ok(PackNoteMaybeAttachments::pack_from(( diff --git a/src/model/processing/user.rs b/src/model/processing/user.rs index 7d9e748..d73673b 100644 --- a/src/model/processing/user.rs +++ b/src/model/processing/user.rs @@ -3,10 +3,11 @@ use crate::model::processing::drive::DriveModel; use crate::model::processing::emoji::EmojiModel; use crate::model::processing::note::NoteModel; use crate::model::processing::{get_mm_token_emoji, PackError, PackResult}; -use crate::model::{PackType, PackingContext, UserRelationship}; +use crate::model::{PackType, PackingContext}; use either::Either; use futures_util::future::OptionFuture; use magnetar_calckey_model::ck; +use magnetar_calckey_model::user_model::UserData; use magnetar_sdk::endpoints::user::{UserByIdReq, UserSelfReq}; use magnetar_sdk::mmm::Token; use magnetar_sdk::types::drive::PackDriveFileBase; @@ -15,7 +16,7 @@ use magnetar_sdk::types::instance::InstanceTicker; use magnetar_sdk::types::user::{ MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll, ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, - UserProfilePinsEx, UserRelationExt, UserSecretsExt, + UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt, }; use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::{mmm, Optional, Packed, Required}; @@ -24,6 +25,46 @@ use tokio::{join, try_join}; use tracing::warn; use url::Url; +pub trait UserShapedData<'a>: Send + Sync { + fn user(&self) -> &'a ck::user::Model; + fn avatar(&self) -> Option<&'a ck::drive_file::Model>; + fn banner(&self) -> Option<&'a ck::drive_file::Model>; +} + +pub struct UserBorrowedData<'a> { + pub user: &'a ck::user::Model, + pub avatar: Option<&'a ck::drive_file::Model>, + pub banner: Option<&'a ck::drive_file::Model>, +} + +impl<'a> UserShapedData<'a> for &'a UserData { + fn user(&self) -> &'a ck::user::Model { + &self.user + } + + fn avatar(&self) -> Option<&'a ck::drive_file::Model> { + self.avatar.as_ref() + } + + fn banner(&self) -> Option<&'a ck::drive_file::Model> { + self.banner.as_ref() + } +} + +impl<'a> UserShapedData<'a> for UserBorrowedData<'a> { + fn user(&self) -> &'a ck::user::Model { + self.user + } + + fn avatar(&self) -> Option<&'a ck::drive_file::Model> { + self.avatar + } + + fn banner(&self) -> Option<&'a ck::drive_file::Model> { + self.banner + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProfileFieldRaw<'a> { name: &'a str, @@ -74,14 +115,15 @@ impl UserModel { })?) } - pub async fn base_from_existing( + pub async fn base_from_existing<'a>( &self, ctx: &PackingContext, - user: &ck::user::Model, - hint_avatar_file: Option<&ck::drive_file::Model>, + user_data: &dyn UserShapedData<'a>, ) -> PackResult { + let user = user_data.user(); + let drive_file_pack = DriveModel; - let avatar = match (hint_avatar_file, &user.avatar_id) { + let avatar = match (user_data.avatar(), &user.avatar_id) { (Some(avatar_file), _) => Some(drive_file_pack.pack_existing(ctx, avatar_file)), (None, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?, _ => None, @@ -110,7 +152,7 @@ impl UserModel { let base = UserBase::extract( ctx, UserBaseSource { - user, + user: &user, username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(), avatar_url, avatar: avatar.as_ref(), @@ -137,17 +179,18 @@ impl UserModel { .ok_or_else(|| PackError::DataError("Missing user profile".to_string())) } - pub async fn profile_from_base( + pub async fn profile_from_base<'a>( &self, ctx: &PackingContext, - user: &ck::user::Model, + user_data: &dyn UserShapedData<'a>, profile: &ck::user_profile::Model, relation: Option<&UserRelationExt>, emoji_out: &mut EmojiContext, - hint_banner_file: Option<&ck::drive_file::Model>, ) -> PackResult { + let user = user_data.user(); + let drive_file_pack = DriveModel; - let banner = match (hint_banner_file, &user.banner_id) { + let banner = match (user_data.banner(), &user.banner_id) { (Some(banner_file), _) => Some(drive_file_pack.pack_existing(ctx, banner_file)), (None, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?, _ => None, @@ -244,22 +287,20 @@ impl UserModel { Ok(UserSecretsExt::extract(ctx, (profile, &secrets))) } - pub async fn self_full_from_base( + pub async fn self_full_from_base<'a>( &self, ctx: &PackingContext, - user: &ck::user::Model, + user_data: &impl UserShapedData<'a>, req: &UserSelfReq, - hint_avatar_file: Option<&ck::drive_file::Model>, - hint_banner_file: Option<&ck::drive_file::Model>, ) -> PackResult { + let user = user_data.user(); + let should_fetch_profile = req.profile.unwrap_or_default() || req.secrets.unwrap_or_default(); let profile_raw_promise = OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user))); - let (base_res, profile_res) = join!( - self.base_from_existing(ctx, user, hint_avatar_file), - profile_raw_promise - ); + let (base_res, profile_res) = + join!(self.base_from_existing(ctx, user_data), profile_raw_promise); let mut base = base_res?; let profile_raw = profile_res.transpose()?; @@ -271,11 +312,10 @@ impl UserModel { let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| { self.profile_from_base( ctx, - user, + user_data, profile_raw.as_ref().unwrap(), None, &mut base.user.0.emojis, - hint_banner_file, ) })); @@ -380,18 +420,15 @@ impl UserModel { pub async fn foreign_full_from_base( &self, ctx: &PackingContext, - user: &ck::user::Model, + user_data: &dyn UserShapedData<'_>, req: &UserByIdReq, - hint_avatar_file: Option<&ck::drive_file::Model>, - hint_banner_file: Option<&ck::drive_file::Model>, ) -> PackResult { + let user = user_data.user(); + let should_fetch_profile = req.profile.unwrap_or_default() || req.auth.unwrap_or_default(); let profile_raw_promise = OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user))); - let (base, profile) = join!( - self.base_from_existing(ctx, user, hint_avatar_file), - profile_raw_promise - ); + let (base, profile) = join!(self.base_from_existing(ctx, user_data), profile_raw_promise); let mut base = base?; let profile_raw = profile.transpose()?; @@ -403,11 +440,10 @@ impl UserModel { let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| { self.profile_from_base( ctx, - user, + user_data, profile_raw.as_ref().unwrap(), None, &mut base.user.0.emojis, - hint_banner_file, ) }));