diff --git a/ext_calckey_model/src/note_model.rs b/ext_calckey_model/src/note_model.rs index ae8b40b..49910dd 100644 --- a/ext_calckey_model/src/note_model.rs +++ b/ext_calckey_model/src/note_model.rs @@ -9,7 +9,7 @@ use sea_orm::{ use serde::{Deserialize, Serialize}; use ck::{drive_file, note, note_reaction, user, user_note_pining}; -use magnetar_sdk::types::RangeFilter; +use magnetar_sdk::types::{EndFilter, RangeFilter, SpanFilter, StartFilter}; use crate::{AliasSourceExt, CalckeyDbError, CalckeyModel}; @@ -73,6 +73,12 @@ 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 { @@ -99,6 +105,35 @@ impl FromQueryResult for NoteData { }) .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 { @@ -152,7 +187,8 @@ pub trait NoteVisibilityFilterFactory: Send + Sync { pub struct NoteResolveOptions { pub ids: Option>, pub visibility_filter: Box, - pub time_range: Option, + pub time_range: Option, + pub limit: Option, pub with_user: bool, pub with_reply_target: bool, pub with_renote_target: bool, @@ -284,15 +320,60 @@ lazy_static! { 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: &RangeFilter) -> SimpleExpr { +fn range_into_expr(filter: &SpanFilter) -> Option { match filter { - RangeFilter::TimeStart(start) => note::Column::CreatedAt.gte(*start), - RangeFilter::TimeRange(range) => { - note::Column::CreatedAt.between(*range.start(), *range.end()) - } - RangeFilter::TimeEnd(end) => note::Column::CreatedAt.lt(*end), + 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, } } @@ -315,7 +396,7 @@ 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().map(range_into_expr); + let time_filter = options.time_range.as_ref().and_then(range_into_expr); let id_filter = options.ids.as_ref().map(ids_into_expr); let notes = select @@ -335,7 +416,7 @@ 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().map(range_into_expr); + let time_filter = options.time_range.as_ref().and_then(range_into_expr); let id_filter = options.ids.as_ref().map(ids_into_expr); let notes = select @@ -348,6 +429,7 @@ impl NoteResolver { user_note_pining::Column::CreatedAt, ))) }) + .apply_if(options.limit, Select::::limit) .into_model::() .all(self.db.inner()) .await?; @@ -418,6 +500,32 @@ impl NoteResolver { .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 { @@ -446,6 +554,20 @@ impl NoteResolver { 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( @@ -493,6 +615,20 @@ impl NoteResolver { 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(), + ); + } } } diff --git a/fe_calckey/frontend/magnetar-common/src/types.ts b/fe_calckey/frontend/magnetar-common/src/types.ts index 4a2340d..8ffefae 100644 --- a/fe_calckey/frontend/magnetar-common/src/types.ts +++ b/fe_calckey/frontend/magnetar-common/src/types.ts @@ -42,3 +42,8 @@ export { DriveFolderParentExt } from "./types/DriveFolderParentExt"; export { ImageMeta } from "./types/ImageMeta"; export { InstanceTicker } from "./types/InstanceTicker"; export { MovedTo } from "./types/MovedTo"; +export { RangeFilter } from "./types/RangeFilter"; +export { EndFilter } from "./types/EndFilter"; +export { StartFilter } from "./types/StartFilter"; +export { NoFilter } from "./types/NoFilter"; +export { TimelineType } from "./types/TimelineType"; diff --git a/fe_calckey/frontend/magnetar-common/src/types/EndFilter.ts b/fe_calckey/frontend/magnetar-common/src/types/EndFilter.ts new file mode 100644 index 0000000..bdf847a --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/types/EndFilter.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface EndFilter { time_end: string, id_end: string, } \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/GetTimelineReq.ts b/fe_calckey/frontend/magnetar-common/src/types/GetTimelineReq.ts index 00b3b07..e3630cf 100644 --- a/fe_calckey/frontend/magnetar-common/src/types/GetTimelineReq.ts +++ b/fe_calckey/frontend/magnetar-common/src/types/GetTimelineReq.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { NoteListFilter } from "./NoteListFilter"; +import type { SpanFilter } from "./SpanFilter"; -export interface GetTimelineReq { limit?: bigint, filter?: NoteListFilter, } \ No newline at end of file +export interface GetTimelineReq { limit?: bigint, filter?: NoteListFilter, range?: SpanFilter, } \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/NoFilter.ts b/fe_calckey/frontend/magnetar-common/src/types/NoFilter.ts new file mode 100644 index 0000000..d80c91e --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/types/NoFilter.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NoFilter = Record; \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/RangeFilter.ts b/fe_calckey/frontend/magnetar-common/src/types/RangeFilter.ts new file mode 100644 index 0000000..bdf4e84 --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/types/RangeFilter.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface RangeFilter { time_start: string, time_end: string, id_start: string, id_end: string, } \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/SpanFilter.ts b/fe_calckey/frontend/magnetar-common/src/types/SpanFilter.ts new file mode 100644 index 0000000..5f1c11c --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/types/SpanFilter.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EndFilter } from "./EndFilter"; +import type { NoFilter } from "./NoFilter"; +import type { RangeFilter } from "./RangeFilter"; +import type { StartFilter } from "./StartFilter"; + +export type SpanFilter = RangeFilter | StartFilter | EndFilter | NoFilter; \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/StartFilter.ts b/fe_calckey/frontend/magnetar-common/src/types/StartFilter.ts new file mode 100644 index 0000000..ad768f2 --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/types/StartFilter.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface StartFilter { time_start: string, id_start: string, } \ 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 new file mode 100644 index 0000000..8b2bb24 --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/types/TimelineType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TimelineType = "home" | "timeline" | "recommended" | "hybrid" | "global"; \ No newline at end of file diff --git a/fe_calckey/frontend/magnetar-common/src/types/endpoints/GetTimeline.ts b/fe_calckey/frontend/magnetar-common/src/types/endpoints/GetTimeline.ts index d2b6375..6d433db 100644 --- a/fe_calckey/frontend/magnetar-common/src/types/endpoints/GetTimeline.ts +++ b/fe_calckey/frontend/magnetar-common/src/types/endpoints/GetTimeline.ts @@ -3,8 +3,8 @@ import type { GetTimelineReq } from "../GetTimelineReq"; import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull"; export const GetTimeline = { - endpoint: "/timeline", - pathParams: [] as [], + endpoint: "/timeline/:timeline_type", + pathParams: ["timeline_type"] as ["timeline_type"], method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", request: undefined as unknown as GetTimelineReq, response: undefined as unknown as Array diff --git a/magnetar_sdk/src/endpoints/timeline.rs b/magnetar_sdk/src/endpoints/timeline.rs index d1e7c1e..8ee6203 100644 --- a/magnetar_sdk/src/endpoints/timeline.rs +++ b/magnetar_sdk/src/endpoints/timeline.rs @@ -1,5 +1,6 @@ use crate::endpoints::Endpoint; use crate::types::note::{NoteListFilter, PackNoteMaybeFull}; +use crate::types::SpanFilter; use crate::util_types::U64Range; use http::Method; use magnetar_sdk_macros::Endpoint; @@ -14,6 +15,8 @@ pub struct GetTimelineReq { pub limit: Option>, #[ts(optional)] pub filter: Option, + #[ts(optional)] + pub range: Option, } pub fn default_timeline_limit() -> U64Range { @@ -22,7 +25,7 @@ pub fn default_timeline_limit() -> U64Range>), - TimeStart(DateTime), - TimeEnd(DateTime), +#[ts(export)] +pub struct RangeFilter { + pub time_start: DateTime, + pub time_end: DateTime, + pub id_start: DateTime, + pub id_end: DateTime, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct StartFilter { + pub time_start: DateTime, + pub id_start: DateTime, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct EndFilter { + pub time_end: DateTime, + pub id_end: DateTime, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct NoFilter {} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +#[serde(untagged)] +pub enum SpanFilter { + Range(RangeFilter), + Start(StartFilter), + End(EndFilter), + None(NoFilter), } #[derive(Clone, Debug, Deserialize, Serialize, TS)] diff --git a/magnetar_sdk/src/types/note.rs b/magnetar_sdk/src/types/note.rs index 8b1c339..0652837 100644 --- a/magnetar_sdk/src/types/note.rs +++ b/magnetar_sdk/src/types/note.rs @@ -97,8 +97,8 @@ pack!( #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] pub struct NoteDetailExt { - pub parent_note: Option>, - pub renoted_note: Option>, + pub parent_note: Option>, + pub renoted_note: Option>, } pack!( diff --git a/magnetar_sdk/src/types/timeline.rs b/magnetar_sdk/src/types/timeline.rs index db653dd..44295f7 100644 --- a/magnetar_sdk/src/types/timeline.rs +++ b/magnetar_sdk/src/types/timeline.rs @@ -2,10 +2,12 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "kebab-case")] pub enum TimelineType { Home, - Timeline, Recommended, Hybrid, Global, + Local, } diff --git a/src/model/data/note.rs b/src/model/data/note.rs index 1f20442..9461fa4 100644 --- a/src/model/data/note.rs +++ b/src/model/data/note.rs @@ -1,7 +1,8 @@ use magnetar_calckey_model::ck; use magnetar_calckey_model::note_model::sub_interaction_renote; use magnetar_sdk::types::note::{ - NoteDetailExt, NoteSelfContextExt, PackNoteMaybeAttachments, PackPollBase, ReactionPair, + NoteDetailExt, NoteSelfContextExt, PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, + ReactionPair, }; use magnetar_sdk::types::user::PackUserBase; use magnetar_sdk::types::{ @@ -83,8 +84,8 @@ impl PackType<&sub_interaction_renote::Model> for NoteSelfContextExt { } pub struct NoteDetailSource<'a> { - pub parent_note: Option<&'a PackNoteMaybeAttachments>, - pub renoted_note: Option<&'a PackNoteMaybeAttachments>, + pub parent_note: Option<&'a PackNoteMaybeFull>, + pub renoted_note: Option<&'a PackNoteMaybeFull>, } impl<'a> PackType> for NoteDetailExt { diff --git a/src/model/processing/note.rs b/src/model/processing/note.rs index b0c6c77..4b06052 100644 --- a/src/model/processing/note.rs +++ b/src/model/processing/note.rs @@ -6,8 +6,8 @@ use crate::model::processing::{get_mm_token_emoji, PackError, PackResult}; use crate::model::{PackType, PackingContext, UserRelationship}; use compact_str::CompactString; use either::Either; -use futures_util::future::try_join_all; -use futures_util::{StreamExt, TryFutureExt, TryStreamExt}; +use futures_util::future::{try_join_all, BoxFuture}; +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::{ @@ -429,65 +429,62 @@ impl NoteModel { ))) } - async fn pack_full_single( - &self, - ctx: &PackingContext, - note: NoteData, - ) -> PackResult { - let drive_model = DriveModel; + fn pack_full_single<'a>( + &'a self, + ctx: &'a PackingContext, + note: &'a NoteData, + ) -> BoxFuture<'a, PackResult> { + async move { + let drive_model = DriveModel; - 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), - } - }; + let reply_target = async { + match note.reply.as_ref() { + Some(r) if self.with_context => self.pack_full_single(ctx, r).await.map(Some), + _ => Ok(None), + } + }; - 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 renote_target = async { + match note.renote.as_ref() { + Some(r) if self.with_context => self.pack_full_single(ctx, r).await.map(Some), + _ => Ok(None), + } + }; - let ( - PackNoteMaybeAttachments { + let ( + PackNoteMaybeAttachments { + id, + note, + user_context, + attachment, + }, + reply_target_pack, + renote_target_pack, + ) = try_join!( + 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.as_ref(), + renoted_note: renote_target_pack.as_ref(), + }, + ) + }); + + Ok(PackNoteMaybeFull::pack_from(( id, note, user_context, attachment, - }, - reply_target_pack, - renote_target_pack, - ) = try_join!( - 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.as_ref(), - renoted_note: renote_target_pack.as_ref(), - }, - ) - }); - - Ok(PackNoteMaybeFull::pack_from(( - id, - note, - user_context, - attachment, - Optional(detail), - ))) + Optional(detail), + ))) + } + .boxed() } pub async fn fetch_single( @@ -503,6 +500,7 @@ impl NoteModel { ctx.self_user.as_deref().map(ck::user::Model::get_id), )), time_range: None, + limit: None, with_user: self.with_context, with_reply_target: self.with_context, with_renote_target: self.with_context, @@ -522,7 +520,7 @@ impl NoteModel { return Ok(None); }; - Ok(Some(self.pack_full_single(ctx, note).await?)) + Ok(Some(self.pack_full_single(ctx, ¬e).await?)) } pub async fn fetch_pins( @@ -538,6 +536,7 @@ impl NoteModel { ctx.self_user.as_deref().map(ck::user::Model::get_id), )), time_range: None, + limit: None, with_user: self.with_context, with_reply_target: self.with_context, with_renote_target: self.with_context, @@ -555,8 +554,9 @@ impl NoteModel { .await?; let fut_iter = notes - .into_iter() - .map(|note| self.pack_full_single(ctx, note)); + .iter() + .map(|note| self.pack_full_single(ctx, note)) + .collect::>(); let processed = futures::stream::iter(fut_iter) .buffered(10)