From 5b9b813037d702c38d09e4f0a6b78883e00e669f Mon Sep 17 00:00:00 2001 From: Natty Date: Wed, 1 Nov 2023 22:55:59 +0100 Subject: [PATCH] Self reaction and renote fetching --- Cargo.lock | 2 + Cargo.toml | 10 +- ext_calckey_model/Cargo.toml | 1 + ext_calckey_model/src/lib.rs | 4 +- ext_calckey_model/src/note_model.rs | 209 ++++++++++++++++++++-- magnetar_sdk/src/types/note.rs | 23 ++- src/api_v1/note.rs | 4 +- src/model/data/note.rs | 15 +- src/model/processing/note.rs | 260 +++++++++++++--------------- 9 files changed, 349 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f533a2..136912d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1460,6 +1460,7 @@ dependencies = [ "hyper", "idna", "itertools 0.11.0", + "lazy_static", "lru", "magnetar_calckey_model", "magnetar_common", @@ -1517,6 +1518,7 @@ 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 69b9cca..e6a68d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ http = "0.2" hyper = "0.14" idna = "0.4" itertools = "0.11" +lazy_static = "1.4" lru = "0.12" miette = "5.9" nom = "7" @@ -90,20 +91,21 @@ tower-http = { workspace = true, features = ["cors", "trace", "fs"] } idna = { workspace = true } +regex = { workspace = true } + tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing = { workspace = true } cfg-if = { workspace = true } -itertools = { workspace = true } - compact_str = { workspace = true } either = { 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 } -miette = { workspace = true, features = ["fancy"] } -regex = { workspace = true } percent-encoding = { workspace = true } diff --git a/ext_calckey_model/Cargo.toml b/ext_calckey_model/Cargo.toml index 62acb6a..801f07d 100644 --- a/ext_calckey_model/Cargo.toml +++ b/ext_calckey_model/Cargo.toml @@ -16,6 +16,7 @@ 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 b7e6232..69d6b13 100644 --- a/ext_calckey_model/src/lib.rs +++ b/ext_calckey_model/src/lib.rs @@ -13,8 +13,8 @@ use redis::IntoConnectionInfo; use sea_orm::sea_query::IntoIden; use sea_orm::ActiveValue::Set; use sea_orm::{ - ColumnTrait, ConnectOptions, DatabaseConnection, DbErr, EntityTrait, QueryFilter, QueryOrder, - RelationDef, RelationTrait, TransactionTrait, + ColumnTrait, ConnectOptions, DatabaseConnection, DbErr, EntityTrait, QueryFilter, RelationDef, + RelationTrait, TransactionTrait, }; use serde::{Deserialize, Serialize}; use std::future::Future; diff --git a/ext_calckey_model/src/note_model.rs b/ext_calckey_model/src/note_model.rs index fe5dbc0..006b7e8 100644 --- a/ext_calckey_model/src/note_model.rs +++ b/ext_calckey_model/src/note_model.rs @@ -1,19 +1,52 @@ -use sea_orm::sea_query::{Alias, Expr, IntoIden, SelectExpr, SimpleExpr}; +use lazy_static::lazy_static; +use sea_orm::sea_query::{Alias, Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr}; use sea_orm::{ - ColumnTrait, DbErr, EntityTrait, FromQueryResult, Iden, Iterable, JoinType, QueryFilter, - QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, + ColumnTrait, DbErr, EntityName, EntityTrait, FromQueryResult, Iden, Iterable, JoinType, + QueryFilter, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, }; use serde::{Deserialize, Serialize}; +use tracing::info; -use ck::{drive_file, note, user}; +use ck::{drive_file, note, note_reaction, user}; use magnetar_sdk::types::RangeFilter; -use once_cell::unsync::Lazy; use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel}; +pub mod sub_interaction_renote { + use sea_orm::{DeriveColumn, EnumIter, FromQueryResult}; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] + pub struct Model { + pub renotes: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + pub enum Column { + Renotes, + } +} + +pub mod sub_interaction_reaction { + use sea_orm::{DeriveColumn, EnumIter, FromQueryResult}; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] + pub struct Model { + pub reaction_name: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + pub enum Column { + ReactionName, + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] 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, @@ -21,14 +54,20 @@ pub struct NoteData { pub renote: Option>, } +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."; @@ -39,6 +78,16 @@ impl FromQueryResult for NoteData { .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)?, @@ -52,6 +101,16 @@ impl FromQueryResult for NoteData { .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)?, @@ -63,6 +122,14 @@ impl FromQueryResult for NoteData { Ok(NoteData { note: note::Model::from_query_result(res, "")?, + interaction_user_renote: sub_interaction_renote::Model::from_query_result_optional( + res, + INTERACTION_RENOTE, + )?, + interaction_user_reaction: sub_interaction_reaction::Model::from_query_result_optional( + res, + 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)?, @@ -87,10 +154,25 @@ pub struct NoteResolveOptions { pub with_user: bool, pub with_reply_target: bool, pub with_renote_target: bool, + pub with_interactions_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>, + alias: &str, + user_id: &str, + ) -> Select; + + fn add_sub_select_renote( + self, + source_note_alias: Option<&str>, + alias: &str, + user_id: &str, + ) -> Select; } impl SelectColumnsExt for Select { @@ -116,19 +198,89 @@ impl SelectColumnsExt for Select { self } + + fn add_sub_select_reaction( + mut self: Select, + source_note_alias: Option<&str>, + prefix_alias: &str, + user_id: &str, + ) -> Select { + 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 column = sub_interaction_reaction::Column::ReactionName; + let alias = format!("{}{}", prefix_alias, column.to_string()); + + let sub_query = Query::select() + .expr(SelectExpr { + expr: Expr::col(note_reaction::Column::Reaction).into(), + alias: Some(Alias::new(alias).into_iden()), + window: None, + }) + .from(note_reaction::Entity) + .cond_where(Expr::col(note_reaction::Column::NoteId).eq(note_id_col)) + .and_where(Expr::col(note_reaction::Column::UserId).eq(user_id)) + .take() + .into_sub_query_statement(); + + QuerySelect::query(&mut self).expr(SimpleExpr::SubQuery(None, Box::new(sub_query))); + + self + } + + fn add_sub_select_renote( + mut self: Select, + source_note_alias: Option<&str>, + prefix_alias: &str, + user_id: &str, + ) -> Select { + 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 renote_note_tbl = Alias::new(format!("{}{}", prefix_alias, "note")); + + let column = sub_interaction_renote::Column::Renotes; + let alias = format!("{}{}", prefix_alias, column.to_string()); + + let sub_query = Query::select() + .expr(SelectExpr { + expr: Expr::count(Expr::col(Asterisk)), + alias: Some(Alias::new(alias).into_iden()), + window: None, + }) + .from_as(note::Entity, renote_note_tbl.clone()) + .cond_where( + Expr::col((renote_note_tbl.clone(), note::Column::RenoteId)).eq(note_id_col), + ) + .and_where(Expr::col((renote_note_tbl.clone(), note::Column::UserId)).eq(user_id)) + .take() + .into_sub_query_statement(); + + QuerySelect::query(&mut self).expr(SimpleExpr::SubQuery(None, Box::new(sub_query))); + + self + } } -const ALIAS_USER: Lazy = Lazy::new(|| Alias::new(USER)); -const ALIAS_USER_AVATAR: Lazy = Lazy::new(|| Alias::new(USER_AVATAR)); -const ALIAS_USER_BANNER: Lazy = Lazy::new(|| Alias::new(USER_BANNER)); -const ALIAS_REPLY: Lazy = Lazy::new(|| Alias::new(REPLY)); -const ALIAS_REPLY_USER: Lazy = Lazy::new(|| Alias::new(REPLY_USER)); -const ALIAS_REPLY_USER_AVATAR: Lazy = Lazy::new(|| Alias::new(REPLY_USER_AVATAR)); -const ALIAS_REPLY_USER_BANNER: Lazy = Lazy::new(|| Alias::new(REPLY_USER_BANNER)); -const ALIAS_RENOTE: Lazy = Lazy::new(|| Alias::new(RENOTE)); -const ALIAS_RENOTE_USER: Lazy = Lazy::new(|| Alias::new(RENOTE_USER)); -const ALIAS_RENOTE_USER_AVATAR: Lazy = Lazy::new(|| Alias::new(RENOTE_USER_AVATAR)); -const ALIAS_RENOTE_USER_BANNER: Lazy = Lazy::new(|| Alias::new(RENOTE_USER_BANNER)); +lazy_static! { + 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); +} impl NoteResolver { pub fn new(db: CalckeyModel) -> Self { @@ -142,11 +294,11 @@ impl NoteResolver { 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().map(|f| match f { - RangeFilter::TimeStart(start) => note::Column::CreatedAt.gte(start.clone()), + RangeFilter::TimeStart(start) => note::Column::CreatedAt.gte(*start), RangeFilter::TimeRange(range) => { - note::Column::CreatedAt.between(range.start().clone(), range.end().clone()) + note::Column::CreatedAt.between(*range.start(), *range.end()) } - RangeFilter::TimeEnd(end) => note::Column::CreatedAt.lt(end.clone()), + RangeFilter::TimeEnd(end) => note::Column::CreatedAt.lt(*end), }); let id_filter = options.ids.as_ref().map(|ids| { @@ -164,12 +316,19 @@ impl NoteResolver { .into_model::() .one(self.db.inner()) .await?; + Ok(notes) } pub fn resolve(&self, options: &NoteResolveOptions) -> Select { let mut select = note::Entity::find().add_aliased_columns(Some(USER), user::Entity); + 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) @@ -181,6 +340,12 @@ impl NoteResolver { .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) @@ -193,6 +358,12 @@ impl NoteResolver { .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) diff --git a/magnetar_sdk/src/types/note.rs b/magnetar_sdk/src/types/note.rs index bfc949d..0efbf62 100644 --- a/magnetar_sdk/src/types/note.rs +++ b/magnetar_sdk/src/types/note.rs @@ -74,6 +74,12 @@ pub struct NoteBase { pack!(PackNoteBase, Required as id & Required as note); +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct NoteSelfContextExt { + pub self_renote_count: Option, +} + #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct NoteAttachmentExt { @@ -82,19 +88,19 @@ pub struct NoteAttachmentExt { } pack!( - PackNoteWithMaybeAttachments, - Required as id & Required as note & Option as attachment + PackNoteMaybeAttachments, + Required as id & Required as note & Option as user_context & Option as attachment ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct NoteDetailExt { - pub parent_note: Option>, - pub renoted_note: Option>, + pub parent_note: Option>, + pub renoted_note: Option>, } pack!( PackNoteMaybeFull, - Required as id & Required as note & Option as attachment & Option as detail + Required as id & Required as note & Option as user_context & Option as attachment & Option as detail ); #[derive(Clone, Debug, Deserialize, Serialize, TS)] @@ -111,4 +117,9 @@ pub enum Reaction { }, } -pub type ReactionPair = (Reaction, usize); +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(untagged)] +pub enum ReactionPair { + WithoutContext(Reaction, u64), + WithContext(Reaction, u64, bool), +} diff --git a/src/api_v1/note.rs b/src/api_v1/note.rs index 8c1385b..d27d453 100644 --- a/src/api_v1/note.rs +++ b/src/api_v1/note.rs @@ -27,7 +27,7 @@ pub async fn handle_note( } .fetch_single(&ctx, self_user.as_deref(), &id) .await? - .ok_or_else(|| ObjectNotFound(id))?; + .ok_or(ObjectNotFound(id))?; - Ok(Json(note.into())) + Ok(Json(note)) } diff --git a/src/model/data/note.rs b/src/model/data/note.rs index 4b69573..d4bd760 100644 --- a/src/model/data/note.rs +++ b/src/model/data/note.rs @@ -1,6 +1,7 @@ use magnetar_calckey_model::ck; +use magnetar_calckey_model::note_model::sub_interaction_renote; use magnetar_sdk::types::note::{ - NoteDetailExt, PackNoteWithMaybeAttachments, PackPollBase, ReactionPair, + NoteDetailExt, NoteSelfContextExt, PackNoteMaybeAttachments, PackPollBase, ReactionPair, }; use magnetar_sdk::types::user::PackUserBase; use magnetar_sdk::types::{ @@ -70,9 +71,17 @@ impl PackType> for NoteBase { } } +impl PackType<&sub_interaction_renote::Model> for NoteSelfContextExt { + fn extract(_context: &PackingContext, data: &sub_interaction_renote::Model) -> Self { + NoteSelfContextExt { + self_renote_count: data.renotes.map(|v| v as u64), + } + } +} + pub struct NoteDetailSource<'a> { - pub parent_note: Option<&'a PackNoteWithMaybeAttachments>, - pub renoted_note: Option<&'a PackNoteWithMaybeAttachments>, + pub parent_note: Option<&'a PackNoteMaybeAttachments>, + pub renoted_note: Option<&'a PackNoteMaybeAttachments>, } impl<'a> PackType> for NoteDetailExt { diff --git a/src/model/processing/note.rs b/src/model/processing/note.rs index 0a4d1aa..20eaa3f 100644 --- a/src/model/processing/note.rs +++ b/src/model/processing/note.rs @@ -22,8 +22,8 @@ use magnetar_sdk::mmm::Token; use magnetar_sdk::types::drive::PackDriveFileBase; use magnetar_sdk::types::emoji::EmojiContext; use magnetar_sdk::types::note::{ - NoteAttachmentExt, NoteBase, NoteDetailExt, PackNoteBase, PackNoteMaybeFull, - PackNoteWithMaybeAttachments, Reaction, + NoteAttachmentExt, NoteBase, NoteDetailExt, NoteSelfContextExt, PackNoteBase, + PackNoteMaybeAttachments, PackNoteMaybeFull, Reaction, ReactionPair, }; use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::{mmm, Packed, Required}; @@ -151,7 +151,7 @@ impl NoteVisibilityFilterModel { return Ok(following || user.host.is_some() && note.user_host.is_some()); } - return Ok(false); + Ok(false) } pub fn new_note_visibility_filter(&self, user: Option<&str>) -> NoteVisibilityFilterSimple { @@ -225,18 +225,28 @@ impl NoteModel { let reactions_raw = serde_json::Map::::deserialize(¬e_data.note.reactions)? .into_iter() - .map(|(code, count)| { + .map(|(ref code, count)| { + let reaction = parse_reaction(code) + .map_or_else(|| Either::Left(code.to_string()), Either::Right); + ( - parse_reaction(&code).map_or_else(|| Either::Left(code), Either::Right), + reaction, count, + note_data + .interaction_user_reaction + .as_ref() + .and_then(|r| r.reaction_name.as_deref()) + .map(|r| r == code), ) }) - .map(|(code, count)| Ok((code, usize::deserialize(count)?))) + .map(|(code, count, self_reacted)| { + Ok((code, u64::deserialize(count)?, self_reacted)) + }) .collect::, serde_json::Error>>()?; // Pick out all successfully-parsed shortcode emojis let reactions_to_resolve = reactions_raw .iter() - .map(|(code, _)| code) + .map(|(code, _, _)| code) .map(Either::as_ref) .filter_map(Either::right) .filter_map(|c| match c { @@ -261,34 +271,38 @@ impl NoteModel { // Left reactions and the Right ones that didn't resolve to any emoji are turned back into Unknown let reactions = &reactions_raw .into_iter() - .map(|(raw, count)| { - ( - raw.either( - |raw| Reaction::Unknown { raw }, - |raw| match raw { - RawReaction::Unicode(text) => Reaction::Unicode(text), - RawReaction::Shortcode { shortcode, host } => reactions_fetched - .iter() - .find(|e| e.host == host && e.name == shortcode) - .map_or_else( - || Reaction::Unknown { - raw: format!( - ":{shortcode}{}:", - host.as_deref() - .map(|h| format!("@{h}")) - .unwrap_or_default() - ), - }, - |e| Reaction::Shortcode { - name: shortcode.clone(), - host: host.clone(), - url: e.public_url.clone(), - }, - ), - }, - ), - count, - ) + .map(|(raw, count, self_reaction)| { + let reaction = raw.either( + |raw| Reaction::Unknown { raw }, + |raw| match raw { + RawReaction::Unicode(text) => Reaction::Unicode(text), + RawReaction::Shortcode { shortcode, host } => reactions_fetched + .iter() + .find(|e| e.host == host && e.name == shortcode) + .map_or_else( + || Reaction::Unknown { + raw: format!( + ":{shortcode}{}:", + host.as_deref() + .map(|h| format!("@{h}")) + .unwrap_or_default() + ), + }, + |e| Reaction::Shortcode { + name: shortcode.clone(), + host: host.clone(), + url: e.public_url.clone(), + }, + ), + }, + ); + + match self_reaction { + Some(self_reaction) => { + ReactionPair::WithContext(reaction, count, self_reaction) + } + None => ReactionPair::WithoutContext(reaction, count), + } }) .collect::>(); @@ -322,54 +336,15 @@ impl NoteModel { ))) } - async fn extract_reply_target_base( + fn extract_interaction( &self, ctx: &PackingContext, note: &NoteData, - ) -> PackResult> { - match note.reply.as_ref() { - Some(r) if self.with_context => self.extract_base(ctx, r).await.map(Some), - _ => Ok(None), - } - } - - async fn extract_renote_target_base( - &self, - ctx: &PackingContext, - note: &NoteData, - ) -> PackResult> { - match note.renote.as_ref() { - Some(r) if self.with_context => self.extract_base(ctx, r).await.map(Some), - _ => Ok(None), - } - } - - async fn extract_reply_target_attachemnts( - &self, - ctx: &PackingContext, - drive_model: &DriveModel, - note: &NoteData, - ) -> PackResult>> { - match note.reply.as_ref() { - Some(r) if self.with_context => { - self.extract_attachments(ctx, drive_model, &r.note).await - } - _ => Ok(None), - } - } - - async fn extract_renote_target_attachemnts( - &self, - ctx: &PackingContext, - drive_model: &DriveModel, - note: &NoteData, - ) -> PackResult>> { - match note.renote.as_ref() { - Some(r) if self.with_context => { - self.extract_attachments(ctx, drive_model, &r.note).await - } - _ => Ok(None), - } + ) -> PackResult> { + Ok(note + .interaction_user_renote + .as_ref() + .map(|renote_info| NoteSelfContextExt::extract(ctx, renote_info))) } async fn extract_attachments( @@ -393,6 +368,33 @@ impl NoteModel { Ok(None) } + async fn pack_single_attachments( + &self, + ctx: &PackingContext, + drive_model: &DriveModel, + note_data: &NoteData, + ) -> PackResult { + let (PackNoteBase { id, note }, attachments_pack) = try_join!( + self.extract_base(ctx, note_data), + self.extract_attachments(ctx, drive_model, ¬e_data.note), + )?; + + Ok(PackNoteMaybeAttachments::pack_from(( + id, + note, + self.extract_interaction(ctx, note_data)?, + attachments_pack.map(|attachments| { + NoteAttachmentExt::extract( + ctx, + NoteAttachmentSource { + attachments: &attachments, + poll: None, // TODO + }, + ) + }), + ))) + } + pub async fn fetch_single( &self, ctx: &PackingContext, @@ -411,6 +413,10 @@ impl NoteModel { with_user: self.with_context, with_reply_target: self.with_context, with_renote_target: self.with_context, + with_interactions_from: self + .with_context + .then(|| as_user.map(ck::user::Model::get_id).map(str::to_string)) + .flatten(), }) .await? else { @@ -419,80 +425,47 @@ impl NoteModel { let drive_model = DriveModel; - let extract_reply_target = self.extract_reply_target_base(ctx, ¬e); - let extract_renote_target = self.extract_renote_target_base(ctx, ¬e); - let extract_attachments = self.extract_attachments(ctx, &drive_model, ¬e.note); - let extract_reply_attachments = - self.extract_reply_target_attachemnts(ctx, &drive_model, ¬e); - let extract_renote_attachments = - self.extract_renote_target_attachemnts(ctx, &drive_model, ¬e); + let reply_target = async { + match note.reply.as_ref() { + Some(r) if self.with_context => self + .pack_single_attachments(ctx, &drive_model, r) + .await + .map(Some), + _ => Ok(None), + } + }; - // TODO: Polls, ... + let renote_target = async { + match note.renote.as_ref() { + Some(r) if self.with_context => self + .pack_single_attachments(ctx, &drive_model, r) + .await + .map(Some), + _ => Ok(None), + } + }; let ( - PackNoteBase { id, note }, + PackNoteMaybeAttachments { + id, + note, + user_context, + attachment, + }, reply_target_pack, renote_target_pack, - attachments_pack, - reply_attachments_pack, - renote_attachments_pack, ) = try_join!( - self.extract_base(ctx, ¬e), - extract_reply_target, - extract_renote_target, - extract_attachments, - extract_reply_attachments, - extract_renote_attachments + self.pack_single_attachments(ctx, &drive_model, ¬e), + reply_target, + renote_target )?; let detail = self.with_context.then(|| { NoteDetailExt::extract( ctx, NoteDetailSource { - parent_note: reply_target_pack - .map(|base| { - PackNoteWithMaybeAttachments::pack_from(( - base.id, - base.note, - reply_attachments_pack.map(|attachments| { - NoteAttachmentExt::extract( - ctx, - NoteAttachmentSource { - attachments: &attachments, - poll: None, // TODO - }, - ) - }), - )) - }) - .as_ref(), - renoted_note: renote_target_pack - .map(|base| { - PackNoteWithMaybeAttachments::pack_from(( - base.id, - base.note, - renote_attachments_pack.map(|attachments| { - NoteAttachmentExt::extract( - ctx, - NoteAttachmentSource { - attachments: &attachments, - poll: None, // TODO - }, - ) - }), - )) - }) - .as_ref(), - }, - ) - }); - - let attachments = attachments_pack.map(|attachments| { - NoteAttachmentExt::extract( - ctx, - NoteAttachmentSource { - attachments: &attachments, - poll: None, // TODO + parent_note: reply_target_pack.as_ref(), + renoted_note: renote_target_pack.as_ref(), }, ) }); @@ -500,7 +473,8 @@ impl NoteModel { Ok(Some(PackNoteMaybeFull::pack_from(( id, note, - attachments, + user_context, + attachment, detail, )))) }