Refactored note fetching
ci/woodpecker/push/ociImagePush Pipeline failed Details

This commit is contained in:
Natty 2024-01-07 23:28:53 +01:00
parent 074c6f999e
commit deb7f6ef5e
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
19 changed files with 701 additions and 502 deletions

2
Cargo.lock generated
View File

@ -1507,7 +1507,6 @@ dependencies = [
"hyper", "hyper",
"idna 0.5.0", "idna 0.5.0",
"itertools 0.12.0", "itertools 0.12.0",
"lazy_static",
"lru", "lru",
"magnetar_calckey_model", "magnetar_calckey_model",
"magnetar_common", "magnetar_common",
@ -1568,7 +1567,6 @@ dependencies = [
"ext_calckey_model_migration", "ext_calckey_model_migration",
"futures-core", "futures-core",
"futures-util", "futures-util",
"lazy_static",
"magnetar_common", "magnetar_common",
"magnetar_sdk", "magnetar_sdk",
"once_cell", "once_cell",

View File

@ -41,7 +41,6 @@ http = "1.0"
hyper = "1.1" hyper = "1.1"
idna = "0.5" idna = "0.5"
itertools = "0.12" itertools = "0.12"
lazy_static = "1.4"
lru = "0.12" lru = "0.12"
miette = "5.9" miette = "5.9"
nom = "7" nom = "7"
@ -108,7 +107,6 @@ either = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
lazy_static = { workspace = true }
miette = { workspace = true, features = ["fancy"] } miette = { workspace = true, features = ["fancy"] }
strum = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] }
thiserror = { workspace = true } thiserror = { workspace = true }

View File

@ -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 RUN update-ca-certificates

View File

@ -16,7 +16,6 @@ magnetar_sdk = { path = "../magnetar_sdk" }
dotenvy = { workspace = true} dotenvy = { workspace = true}
futures-core = { workspace = true } futures-core = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
lazy_static = { workspace = true }
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true} tokio-util = { workspace = true}
redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"]} redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"]}

View File

@ -1,10 +1,14 @@
pub mod emoji; pub mod emoji;
pub mod model_ext;
pub mod note_model; pub mod note_model;
pub mod paginated;
pub mod poll; pub mod poll;
pub mod user_model;
pub use ck; pub use ck;
use ck::*; use ck::*;
pub use sea_orm; pub use sea_orm;
use user_model::UserResolver;
use crate::note_model::NoteResolver; use crate::note_model::NoteResolver;
use chrono::Utc; use chrono::Utc;
@ -307,7 +311,11 @@ impl CalckeyModel {
} }
pub fn get_note_resolver(&self) -> NoteResolver { 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())
} }
} }

View File

@ -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<T: EntityTrait>(&mut self, alias: &str) -> &mut Self;
fn join_columns<I: IntoIden>(
&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<T: EntityTrait>(&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<I: IntoIden>(
&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<T: EntityTrait> 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()
}
}

View File

@ -1,16 +1,21 @@
use lazy_static::lazy_static; use ext_calckey_model_migration::SelectStatement;
use sea_orm::sea_query::{ use sea_orm::sea_query::{
Alias, Asterisk, Expr, IntoCondition, IntoIden, Query, SelectExpr, SimpleExpr, Alias, Asterisk, Expr, IntoCondition, IntoIden, Query, SelectExpr, SimpleExpr,
}; };
use sea_orm::{ use sea_orm::{
ColumnTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, JoinType, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, JoinType, QueryFilter, QueryOrder,
QueryFilter, QueryOrder, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, QueryResult, QuerySelect, QueryTrait, Select,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ck::{drive_file, note, note_reaction, user, user_note_pining}; use ck::{note, note_reaction, user, user_note_pining};
use magnetar_sdk::types::{EndFilter, RangeFilter, SpanFilter, StartFilter}; 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}; use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel};
pub mod sub_interaction_renote { pub mod sub_interaction_renote {
@ -48,9 +53,7 @@ pub struct NoteData {
pub note: note::Model, pub note: note::Model,
pub interaction_user_renote: Option<sub_interaction_renote::Model>, pub interaction_user_renote: Option<sub_interaction_renote::Model>,
pub interaction_user_reaction: Option<sub_interaction_reaction::Model>, pub interaction_user_reaction: Option<sub_interaction_reaction::Model>,
pub user: user::Model, pub user: UserData,
pub avatar: Option<drive_file::Model>,
pub banner: Option<drive_file::Model>,
pub reply: Option<Box<NoteData>>, pub reply: Option<Box<NoteData>>,
pub renote: Option<Box<NoteData>>, pub renote: Option<Box<NoteData>>,
} }
@ -59,125 +62,36 @@ const PIN: &str = "pin.";
const INTERACTION_REACTION: &str = "interaction.reaction."; const INTERACTION_REACTION: &str = "interaction.reaction.";
const INTERACTION_RENOTE: &str = "interaction.renote."; const INTERACTION_RENOTE: &str = "interaction.renote.";
const USER: &str = "user."; const USER: &str = "user.";
const USER_AVATAR: &str = "user.avatar.";
const USER_BANNER: &str = "user.banner.";
const REPLY: &str = "reply."; 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: &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 { impl FromQueryResult for NoteData {
fn from_query_result(res: &QueryResult, _pre: &str) -> Result<Self, DbErr> { fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
let reply = note::Model::from_query_result_optional(res, REPLY)? let fallback = format!("{}.", note::Entity.table_name());
.map::<Result<_, DbErr>, _>(|r| { let prefix = if prefix.is_empty() { &fallback } else { prefix };
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::<Result<_, DbErr>, _>(|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::<Result<_, DbErr>, _>(|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()?;
Ok(NoteData { 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( interaction_user_renote: sub_interaction_renote::Model::from_query_result_optional(
res, res,
INTERACTION_RENOTE, &joined_prefix_str(prefix, INTERACTION_RENOTE),
)?, )?,
interaction_user_reaction: sub_interaction_reaction::Model::from_query_result_optional( interaction_user_reaction: sub_interaction_reaction::Model::from_query_result_optional(
res, res,
INTERACTION_REACTION, &joined_prefix_str(prefix, INTERACTION_REACTION),
)?, )?,
user: user::Model::from_query_result(res, USER)?, user: UserData::from_query_result(res, &joined_prefix_str(prefix, USER))?,
avatar: drive_file::Model::from_query_result_optional(res, USER_AVATAR)?, reply: NoteData::from_query_result_optional(res, &joined_prefix_str(prefix, REPLY))?
banner: drive_file::Model::from_query_result_optional(res, USER_BANNER)?, .map(Box::new),
reply, renote: NoteData::from_query_result_optional(res, &joined_prefix_str(prefix, RENOTE))?
renote, .map(Box::new),
}) })
} }
} }
pub struct NoteResolver { pub struct NoteResolver {
db: CalckeyModel, db: CalckeyModel,
user_resolver: UserResolver,
} }
pub trait NoteVisibilityFilterFactory: Send + Sync { pub trait NoteVisibilityFilterFactory: Send + Sync {
@ -196,55 +110,29 @@ pub struct NoteResolveOptions {
pub only_pins_from: Option<String>, // User ID pub only_pins_from: Option<String>, // User ID
} }
trait SelectColumnsExt { trait SelectNoteInteractionsExt {
fn add_aliased_columns<T: EntityTrait>(self, alias: Option<&str>, entity: T) -> Self; fn add_sub_select_interaction_reaction(
&mut self,
fn add_sub_select_reaction( source_note_alias: &str,
self,
source_note_alias: Option<&str>,
alias: &str, alias: &str,
user_id: &str, user_id: &str,
) -> Select<note::Entity>; ) -> &mut Self;
fn add_sub_select_renote( fn add_sub_select_interaction_renote(
self, &mut self,
source_note_alias: Option<&str>, source_note_alias: &str,
alias: &str, alias: &str,
user_id: &str, user_id: &str,
) -> Select<note::Entity>; ) -> &mut Self;
} }
impl SelectColumnsExt for Select<note::Entity> { impl SelectNoteInteractionsExt for SelectStatement {
fn add_aliased_columns<T: EntityTrait>( fn add_sub_select_interaction_reaction(
mut self: Select<note::Entity>, &mut self,
alias: Option<&str>, iden: &str,
entity: T,
) -> Select<note::Entity> {
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<note::Entity>,
source_note_alias: Option<&str>,
prefix_alias: &str, prefix_alias: &str,
user_id: &str, user_id: &str,
) -> Select<note::Entity> { ) -> &mut Self {
let iden = source_note_alias.unwrap_or_else(|| note::Entity.table_name());
let note_id_col = Expr::col((Alias::new(iden), note::Column::Id)); let note_id_col = Expr::col((Alias::new(iden), note::Column::Id));
let column = sub_interaction_reaction::Column::ReactionName; let column = sub_interaction_reaction::Column::ReactionName;
@ -262,21 +150,19 @@ impl SelectColumnsExt for Select<note::Entity> {
.take() .take()
.into_sub_query_statement(); .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 self
} }
fn add_sub_select_renote( fn add_sub_select_interaction_renote(
mut self: Select<note::Entity>, &mut self,
source_note_alias: Option<&str>, iden: &str,
prefix_alias: &str, prefix_alias: &str,
user_id: &str, user_id: &str,
) -> Select<note::Entity> { ) -> &mut Self {
let iden = source_note_alias.unwrap_or_else(|| note::Entity.table_name());
let note_id_col = Expr::col((Alias::new(iden), note::Column::Id)); 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 column = sub_interaction_renote::Column::Renotes;
let alias = format!("{}{}", prefix_alias, column.to_string()); let alias = format!("{}{}", prefix_alias, column.to_string());
@ -295,99 +181,24 @@ impl SelectColumnsExt for Select<note::Entity> {
.take() .take()
.into_sub_query_statement(); .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 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<SimpleExpr> {
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<String>) -> SimpleExpr { fn ids_into_expr(ids: &Vec<String>) -> SimpleExpr {
let id_col = Expr::col((note::Entity.base_prefix(), note::Column::Id));
if ids.len() == 1 { if ids.len() == 1 {
note::Column::Id.eq(&ids[0]) id_col.eq(&ids[0])
} else { } else {
note::Column::Id.is_in(ids) id_col.is_in(ids)
} }
} }
impl NoteResolver { impl NoteResolver {
pub fn new(db: CalckeyModel) -> Self { pub fn new(db: CalckeyModel, user_resolver: UserResolver) -> Self {
NoteResolver { db } NoteResolver { db, user_resolver }
} }
pub async fn get_one( pub async fn get_one(
@ -396,7 +207,10 @@ impl NoteResolver {
) -> Result<Option<NoteData>, CalckeyDbError> { ) -> Result<Option<NoteData>, CalckeyDbError> {
let select = self.resolve(options); let select = self.resolve(options);
let visibility_filter = options.visibility_filter.with_note_and_user_tables(None); 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 id_filter = options.ids.as_ref().map(ids_into_expr);
let notes = select let notes = select
@ -416,7 +230,10 @@ impl NoteResolver {
) -> Result<Vec<NoteData>, CalckeyDbError> { ) -> Result<Vec<NoteData>, CalckeyDbError> {
let select = self.resolve(options); let select = self.resolve(options);
let visibility_filter = options.visibility_filter.with_note_and_user_tables(None); 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 id_filter = options.ids.as_ref().map(ids_into_expr);
let notes = select let notes = select
@ -425,7 +242,7 @@ impl NoteResolver {
.apply_if(time_filter, Select::<note::Entity>::filter) .apply_if(time_filter, Select::<note::Entity>::filter)
.apply_if(options.only_pins_from.as_deref(), |s, _| { .apply_if(options.only_pins_from.as_deref(), |s, _| {
s.order_by_desc(Expr::col(( s.order_by_desc(Expr::col((
ALIAS_PIN.clone(), joined_prefix(&note::Entity.base_prefix_str(), PIN),
user_note_pining::Column::CreatedAt, user_note_pining::Column::CreatedAt,
))) )))
}) })
@ -437,201 +254,111 @@ impl NoteResolver {
Ok(notes) Ok(notes)
} }
pub fn resolve(&self, options: &NoteResolveOptions) -> Select<note::Entity> { fn attach_note(
let mut select = note::Entity::find().add_aliased_columns(Some(USER), user::Entity); &self,
q: &mut SelectStatement,
prefix: &str,
reply_depth: usize,
renote_depth: usize,
options: &NoteResolveOptions,
user_resolver: &UserResolver,
) {
q.add_aliased_columns::<note::Entity>(prefix);
if let Some(pins_user) = options.only_pins_from.clone() { // Add the note's author
q.add_aliased_columns::<user::Entity>(&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<note::Entity> {
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 select = select
.join_as( .join_as(
JoinType::InnerJoin, JoinType::InnerJoin,
note::Relation::UserNotePining.def(), note::Relation::UserNotePining.with_alias(Alias::new(&prefix)),
ALIAS_PIN.clone(), joined_prefix(&prefix, PIN),
) )
.filter( .filter(
Expr::col((ALIAS_PIN.clone(), user_note_pining::Column::UserId)) Expr::col((
.eq(&pins_user) joined_prefix(&prefix, PIN),
.into_condition(), user_note_pining::Column::UserId,
))
.eq(pins_user)
.into_condition(),
) )
} }
if let Some(user_id) = &options.with_interactions_from { self.attach_note(
select = select QuerySelect::query(&mut select),
.add_sub_select_reaction(None, INTERACTION_REACTION, user_id) &prefix,
.add_sub_select_renote(None, INTERACTION_RENOTE, user_id); options.with_reply_target.then_some(1).unwrap_or_default(),
} options.with_renote_target.then_some(1).unwrap_or_default(),
options,
if options.with_user { &self.user_resolver,
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(),
); );
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 select
} }
} }

View File

@ -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<SimpleExpr> {
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
}
}

View File

@ -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<drive_file::Model>,
pub banner: Option<drive_file::Model>,
}
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<Self, DbErr> {
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::<user::Entity>(prefix);
if *with_avatar_and_banner {
q.add_aliased_columns::<drive_file::Entity>(&joined_prefix_str(prefix, AVATAR))
.add_aliased_columns::<drive_file::Entity>(&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<UserData> {
todo!()
}
}

View File

@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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, } export interface NoteDetailExt { parent_note: PackNoteMaybeFull | null, renoted_note: PackNoteMaybeFull | null, }

View File

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // 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"; export type TimelineType = "home" | "recommended" | "hybrid" | "global" | "local";

View File

@ -1,4 +1,4 @@
use crate::endpoints::Endpoint; use crate::endpoints::{Empty, Endpoint};
use crate::util_types::deserialize_array_urlenc; use crate::util_types::deserialize_array_urlenc;
use http::Method; use http::Method;
use magnetar_sdk_macros::Endpoint; use magnetar_sdk_macros::Endpoint;
@ -82,3 +82,48 @@ pub struct GetManyUsersById;
response = "PackUserMaybeAll" response = "PackUserMaybeAll"
)] )]
pub struct GetUserByAcct; pub struct GetUserByAcct;
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/:id/followers",
method = Method::GET,
request = "Empty",
response = "Vec<PackUserBase>"
)]
pub struct GetFollowersById;
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/:id/following",
method = Method::GET,
request = "Empty",
response = "Vec<PackUserBase>"
)]
pub struct GetFollowingById;
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/@self/followers",
method = Method::GET,
request = "Empty",
response = "Vec<PackUserBase>"
)]
pub struct GetFollowersSelf;
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/@self/following",
method = Method::GET,
request = "Empty",
response = "Vec<PackUserBase>"
)]
pub struct GetFollowingSelf;
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/@self/follow-requests",
method = Method::GET,
request = "Empty",
response = "Vec<PackUserBase>"
)]
pub struct GetFollowRequestsSelf;

View File

@ -7,7 +7,6 @@ pub mod user;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::RangeInclusive;
use ts_rs::TS; use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]

View File

@ -99,6 +99,15 @@ pub struct UserProfilePinsEx {
// pub pinned_page: Option<Page>, // pub pinned_page: Option<Page>,
} }
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum UserRelationship {
Follow,
FollowRequest,
Mute,
Block,
RenoteMute,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
pub struct UserRelationExt { pub struct UserRelationExt {

View File

@ -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::processing::PackError;
use crate::model::PackingContext; use crate::model::PackingContext;
use crate::service::MagnetarService; use crate::service::MagnetarService;
@ -11,6 +11,7 @@ use futures_util::TryStreamExt;
use itertools::Itertools; use itertools::Itertools;
use magnetar_common::util::lenient_parse_tag_decode; use magnetar_common::util::lenient_parse_tag_decode;
use magnetar_sdk::endpoints::user::{ use magnetar_sdk::endpoints::user::{
GetFollowRequestsSelf, GetFollowersById, GetFollowersSelf, GetFollowingById, GetFollowingSelf,
GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq,
}; };
use magnetar_sdk::endpoints::{Req, Res}; use magnetar_sdk::endpoints::{Req, Res};
@ -24,7 +25,15 @@ pub async fn handle_user_info_self(
) -> Result<Json<Res<GetUserSelf>>, ApiError> { ) -> Result<Json<Res<GetUserSelf>>, ApiError> {
let ctx = PackingContext::new(service, Some(user.clone())).await?; let ctx = PackingContext::new(service, Some(user.clone())).await?;
let user = UserModel 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?; .await?;
Ok(Json(user)) Ok(Json(user))
} }
@ -43,7 +52,15 @@ pub async fn handle_user_info(
.ok_or(ObjectNotFound(id))?; .ok_or(ObjectNotFound(id))?;
let user = UserModel 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?; .await?;
Ok(Json(user)) Ok(Json(user))
} }
@ -67,7 +84,15 @@ pub async fn handle_user_info_by_acct(
.ok_or(ObjectNotFound(tag_str))?; .ok_or(ObjectNotFound(tag_str))?;
let user = UserModel 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?; .await?;
Ok(Json(user)) Ok(Json(user))
} }
@ -89,9 +114,18 @@ pub async fn handle_user_by_id_many(
let ctx = PackingContext::new(service, user.clone()).await?; let ctx = PackingContext::new(service, user.clone()).await?;
let user_model = UserModel; let user_model = UserModel;
let futures = users let user_data = users
.iter() .iter()
.map(|u| user_model.base_from_existing(&ctx, u, None)) .map(|user| UserBorrowedData {
user,
avatar: None,
banner: None,
})
.collect::<Vec<_>>();
let futures = user_data
.iter()
.map(|user| user_model.base_from_existing(&ctx, user))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let users_proc = futures::stream::iter(futures) let users_proc = futures::stream::iter(futures)

View File

@ -71,6 +71,7 @@ impl PackType<NoteBaseSource<'_>> for NoteBase {
has_poll: note.has_poll, has_poll: note.has_poll,
file_ids: note.file_ids.clone(), file_ids: note.file_ids.clone(),
emojis: emoji_context.clone(), emojis: emoji_context.clone(),
is_quote: note.is_quote.unwrap_or_default(),
} }
} }
} }

View File

@ -3,6 +3,7 @@ use crate::model::processing::PackResult;
use crate::service::MagnetarService; use crate::service::MagnetarService;
use either::Either; use either::Either;
use magnetar_calckey_model::{ck, CalckeyDbError}; use magnetar_calckey_model::{ck, CalckeyDbError};
use magnetar_sdk::types::user::UserRelationship;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; 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)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
struct UserRelationshipLink { struct UserRelationshipLink {
from: String, from: String,

View File

@ -1,9 +1,11 @@
use std::sync::Arc;
use crate::model::data::id::BaseId; use crate::model::data::id::BaseId;
use crate::model::data::note::{NoteAttachmentSource, NoteBaseSource, NoteDetailSource}; use crate::model::data::note::{NoteAttachmentSource, NoteBaseSource, NoteDetailSource};
use crate::model::processing::emoji::EmojiModel; use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::user::UserModel; use crate::model::processing::user::UserModel;
use crate::model::processing::{get_mm_token_emoji, PackError, PackResult}; 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 compact_str::CompactString;
use either::Either; use either::Either;
use futures_util::future::{try_join_all, BoxFuture}; 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::ck::sea_orm_active_enums::NoteVisibilityEnum;
use magnetar_calckey_model::emoji::EmojiTag; use magnetar_calckey_model::emoji::EmojiTag;
use magnetar_calckey_model::note_model::{ 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::poll::PollResolver;
use magnetar_calckey_model::sea_orm::prelude::Expr; use magnetar_calckey_model::sea_orm::prelude::Expr;
@ -27,12 +30,14 @@ use magnetar_sdk::types::note::{
PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, Reaction, ReactionPair, PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, Reaction, ReactionPair,
ReactionShortcode, ReactionUnicode, ReactionUnknown, ReactionShortcode, ReactionUnicode, ReactionUnknown,
}; };
use magnetar_sdk::types::user::UserRelationship;
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Optional, Packed, Required}; use magnetar_sdk::{mmm, Optional, Packed, Required};
use serde::Deserialize; use serde::Deserialize;
use tokio::try_join; use tokio::try_join;
use super::drive::DriveModel; use super::drive::DriveModel;
use super::user::UserShapedData;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NoteVisibilityFilterSimple(Option<String>); pub struct NoteVisibilityFilterSimple(Option<String>);
@ -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<dyn UserShapedData<'a> + 'a>;
fn reply(&self) -> Option<Arc<dyn NoteShapedData<'a> + 'a>>;
fn renote(&self) -> Option<Arc<dyn NoteShapedData<'a> + '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<dyn UserShapedData<'a> + 'a>,
pub reply: Option<Arc<dyn NoteShapedData<'a> + 'a>>,
pub renote: Option<Arc<dyn NoteShapedData<'a> + '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<dyn UserShapedData<'a> + 'a> {
self.user.clone()
}
fn reply(&self) -> Option<Arc<dyn NoteShapedData<'a> + 'a>> {
self.reply.as_ref().cloned()
}
fn renote(&self) -> Option<Arc<dyn NoteShapedData<'a> + '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<dyn UserShapedData<'a> + 'a> {
Arc::new(&self.user)
}
fn reply(&self) -> Option<Arc<dyn NoteShapedData<'a> + 'a>> {
self.reply
.as_deref()
.map(|x| Arc::new(x) as Arc<dyn NoteShapedData<'a> + 'a>)
}
fn renote(&self) -> Option<Arc<dyn NoteShapedData<'a> + 'a>> {
self.renote
.as_deref()
.map(|x| Arc::new(x) as Arc<dyn NoteShapedData<'a> + 'a>)
}
}
struct SpeechTransformNyan; struct SpeechTransformNyan;
impl SpeechTransformNyan { impl SpeechTransformNyan {
@ -194,15 +273,17 @@ impl NoteModel {
pub async fn extract_base( pub async fn extract_base(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
note_data: &NoteData, note_data: &dyn NoteShapedData<'_>,
) -> PackResult<PackNoteBase> { ) -> PackResult<PackNoteBase> {
let note = note_data.note();
let Required(ref user) = UserModel let Required(ref user) = UserModel
.base_from_existing(ctx, &note_data.user, None) .base_from_existing(ctx, note_data.user().as_ref())
.await? .await?
.user; .user;
let cw_tok = self.tokenize_note_cw(&note_data.note); let cw_tok = self.tokenize_note_cw(note);
let mut text_tok = self.tokenize_note_text(&note_data.note); let mut text_tok = self.tokenize_note_text(note);
let mut emoji_extracted = Vec::new(); let mut emoji_extracted = Vec::new();
@ -213,7 +294,7 @@ impl NoteModel {
if let Some(text_tok) = &mut text_tok { if let Some(text_tok) = &mut text_tok {
emoji_extracted.extend_from_slice(&get_mm_token_emoji(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(); let transformer = SpeechTransformNyan::new();
text_tok.walk_speech_transform(&|text| transformer.transform(text)); 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 // 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 // Failed reaction parses -> Left, Successful ones -> Right
let reactions_raw = let reactions_raw =
serde_json::Map::<String, serde_json::Value>::deserialize(&note_data.note.reactions)? serde_json::Map::<String, serde_json::Value>::deserialize(&note.reactions)?
.into_iter() .into_iter()
.map(|(ref code, count)| { .map(|(ref code, count)| {
let reaction = parse_reaction(code) let reaction = parse_reaction(code)
@ -235,7 +316,7 @@ impl NoteModel {
reaction, reaction,
count, count,
note_data note_data
.interaction_user_reaction .interaction_user_reaction()
.as_ref() .as_ref()
.and_then(|r| r.reaction_name.as_deref()) .and_then(|r| r.reaction_name.as_deref())
.map(|r| r == code), .map(|r| r == code),
@ -266,8 +347,11 @@ impl NoteModel {
.emoji_cache .emoji_cache
.get_many_tagged(&reactions_to_resolve) .get_many_tagged(&reactions_to_resolve)
.map_err(PackError::from); .map_err(PackError::from);
let emoji_fetch = let emoji_fetch = emoji_model.fetch_many_emojis(
emoji_model.fetch_many_emojis(ctx, &shortcodes, note_data.user.host.as_deref()); ctx,
&shortcodes,
note_data.user().user().host.as_deref(),
);
let (reactions_fetched, emojis) = try_join!(reaction_fetch, emoji_fetch)?; let (reactions_fetched, emojis) = try_join!(reaction_fetch, emoji_fetch)?;
@ -318,7 +402,7 @@ impl NoteModel {
let note_base = NoteBase::extract( let note_base = NoteBase::extract(
ctx, ctx,
NoteBaseSource { NoteBaseSource {
note: &note_data.note, note,
cw_mm: cw_tok cw_mm: cw_tok
.as_ref() .as_ref()
.map(mmm::to_xml_string) .map(mmm::to_xml_string)
@ -338,7 +422,7 @@ impl NoteModel {
); );
Ok(PackNoteBase::pack_from(( Ok(PackNoteBase::pack_from((
Required(Id::from(note_data.note.id.clone())), Required(Id::from(note.id.clone())),
Required(note_base), Required(note_base),
))) )))
} }
@ -346,11 +430,10 @@ impl NoteModel {
fn extract_interaction( fn extract_interaction(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
note: &NoteData, note: &dyn NoteShapedData<'_>,
) -> PackResult<Option<NoteSelfContextExt>> { ) -> PackResult<Option<NoteSelfContextExt>> {
Ok(note Ok(note
.interaction_user_renote .interaction_user_renote()
.as_ref()
.map(|renote_info| NoteSelfContextExt::extract(ctx, renote_info))) .map(|renote_info| NoteSelfContextExt::extract(ctx, renote_info)))
} }
@ -405,12 +488,12 @@ impl NoteModel {
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
drive_model: &DriveModel, drive_model: &DriveModel,
note_data: &NoteData, note_data: &dyn NoteShapedData<'_>,
) -> PackResult<PackNoteMaybeAttachments> { ) -> PackResult<PackNoteMaybeAttachments> {
let (PackNoteBase { id, note }, attachments_pack, poll_pack) = try_join!( let (PackNoteBase { id, note }, attachments_pack, poll_pack) = try_join!(
self.extract_base(ctx, note_data), self.extract_base(ctx, note_data),
self.extract_attachments(ctx, drive_model, &note_data.note), self.extract_attachments(ctx, drive_model, note_data.note()),
self.extract_poll(ctx, &note_data.note) self.extract_poll(ctx, note_data.note())
)?; )?;
Ok(PackNoteMaybeAttachments::pack_from(( Ok(PackNoteMaybeAttachments::pack_from((

View File

@ -3,10 +3,11 @@ use crate::model::processing::drive::DriveModel;
use crate::model::processing::emoji::EmojiModel; use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::note::NoteModel; use crate::model::processing::note::NoteModel;
use crate::model::processing::{get_mm_token_emoji, PackError, PackResult}; 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 either::Either;
use futures_util::future::OptionFuture; use futures_util::future::OptionFuture;
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
use magnetar_calckey_model::user_model::UserData;
use magnetar_sdk::endpoints::user::{UserByIdReq, UserSelfReq}; use magnetar_sdk::endpoints::user::{UserByIdReq, UserSelfReq};
use magnetar_sdk::mmm::Token; use magnetar_sdk::mmm::Token;
use magnetar_sdk::types::drive::PackDriveFileBase; use magnetar_sdk::types::drive::PackDriveFileBase;
@ -15,7 +16,7 @@ use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll, MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll,
ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt,
UserProfilePinsEx, UserRelationExt, UserSecretsExt, UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt,
}; };
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Optional, Packed, Required}; use magnetar_sdk::{mmm, Optional, Packed, Required};
@ -24,6 +25,46 @@ use tokio::{join, try_join};
use tracing::warn; use tracing::warn;
use url::Url; 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileFieldRaw<'a> { pub struct ProfileFieldRaw<'a> {
name: &'a str, name: &'a str,
@ -74,14 +115,15 @@ impl UserModel {
})?) })?)
} }
pub async fn base_from_existing( pub async fn base_from_existing<'a>(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
user: &ck::user::Model, user_data: &dyn UserShapedData<'a>,
hint_avatar_file: Option<&ck::drive_file::Model>,
) -> PackResult<PackUserBase> { ) -> PackResult<PackUserBase> {
let user = user_data.user();
let drive_file_pack = DriveModel; 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)), (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, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?,
_ => None, _ => None,
@ -110,7 +152,7 @@ impl UserModel {
let base = UserBase::extract( let base = UserBase::extract(
ctx, ctx,
UserBaseSource { UserBaseSource {
user, user: &user,
username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(), username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(),
avatar_url, avatar_url,
avatar: avatar.as_ref(), avatar: avatar.as_ref(),
@ -137,17 +179,18 @@ impl UserModel {
.ok_or_else(|| PackError::DataError("Missing user profile".to_string())) .ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
} }
pub async fn profile_from_base( pub async fn profile_from_base<'a>(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
user: &ck::user::Model, user_data: &dyn UserShapedData<'a>,
profile: &ck::user_profile::Model, profile: &ck::user_profile::Model,
relation: Option<&UserRelationExt>, relation: Option<&UserRelationExt>,
emoji_out: &mut EmojiContext, emoji_out: &mut EmojiContext,
hint_banner_file: Option<&ck::drive_file::Model>,
) -> PackResult<UserProfileExt> { ) -> PackResult<UserProfileExt> {
let user = user_data.user();
let drive_file_pack = DriveModel; 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)), (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, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?,
_ => None, _ => None,
@ -244,22 +287,20 @@ impl UserModel {
Ok(UserSecretsExt::extract(ctx, (profile, &secrets))) Ok(UserSecretsExt::extract(ctx, (profile, &secrets)))
} }
pub async fn self_full_from_base( pub async fn self_full_from_base<'a>(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
user: &ck::user::Model, user_data: &impl UserShapedData<'a>,
req: &UserSelfReq, req: &UserSelfReq,
hint_avatar_file: Option<&ck::drive_file::Model>,
hint_banner_file: Option<&ck::drive_file::Model>,
) -> PackResult<PackUserSelfMaybeAll> { ) -> PackResult<PackUserSelfMaybeAll> {
let user = user_data.user();
let should_fetch_profile = let should_fetch_profile =
req.profile.unwrap_or_default() || req.secrets.unwrap_or_default(); req.profile.unwrap_or_default() || req.secrets.unwrap_or_default();
let profile_raw_promise = let profile_raw_promise =
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user))); OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
let (base_res, profile_res) = join!( let (base_res, profile_res) =
self.base_from_existing(ctx, user, hint_avatar_file), join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
profile_raw_promise
);
let mut base = base_res?; let mut base = base_res?;
let profile_raw = profile_res.transpose()?; let profile_raw = profile_res.transpose()?;
@ -271,11 +312,10 @@ impl UserModel {
let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| { let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| {
self.profile_from_base( self.profile_from_base(
ctx, ctx,
user, user_data,
profile_raw.as_ref().unwrap(), profile_raw.as_ref().unwrap(),
None, None,
&mut base.user.0.emojis, &mut base.user.0.emojis,
hint_banner_file,
) )
})); }));
@ -380,18 +420,15 @@ impl UserModel {
pub async fn foreign_full_from_base( pub async fn foreign_full_from_base(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
user: &ck::user::Model, user_data: &dyn UserShapedData<'_>,
req: &UserByIdReq, req: &UserByIdReq,
hint_avatar_file: Option<&ck::drive_file::Model>,
hint_banner_file: Option<&ck::drive_file::Model>,
) -> PackResult<PackUserMaybeAll> { ) -> PackResult<PackUserMaybeAll> {
let user = user_data.user();
let should_fetch_profile = req.profile.unwrap_or_default() || req.auth.unwrap_or_default(); let should_fetch_profile = req.profile.unwrap_or_default() || req.auth.unwrap_or_default();
let profile_raw_promise = let profile_raw_promise =
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user))); OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
let (base, profile) = join!( let (base, profile) = join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
self.base_from_existing(ctx, user, hint_avatar_file),
profile_raw_promise
);
let mut base = base?; let mut base = base?;
let profile_raw = profile.transpose()?; let profile_raw = profile.transpose()?;
@ -403,11 +440,10 @@ impl UserModel {
let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| { let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| {
self.profile_from_base( self.profile_from_base(
ctx, ctx,
user, user_data,
profile_raw.as_ref().unwrap(), profile_raw.as_ref().unwrap(),
None, None,
&mut base.user.0.emojis, &mut base.user.0.emojis,
hint_banner_file,
) )
})); }));