Add note processing for parents of renote posts and introduced better timeline filtering
ci/woodpecker/push/ociImagePush Pipeline is pending Details

This commit is contained in:
Natty 2024-01-04 23:52:24 +01:00
parent 152c4e6fc6
commit bbc4f84ceb
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
16 changed files with 281 additions and 81 deletions

View File

@ -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<Self, DbErr> {
@ -99,6 +105,35 @@ impl FromQueryResult for NoteData {
})
.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 {
@ -152,7 +187,8 @@ pub trait NoteVisibilityFilterFactory: Send + Sync {
pub struct NoteResolveOptions {
pub ids: Option<Vec<String>>,
pub visibility_filter: Box<dyn NoteVisibilityFilterFactory>,
pub time_range: Option<RangeFilter>,
pub time_range: Option<SpanFilter>,
pub limit: Option<u64>,
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<SimpleExpr> {
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<Option<NoteData>, 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<Vec<NoteData>, 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::<note::Entity>::limit)
.into_model::<NoteData>()
.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(),
);
}
}
}

View File

@ -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";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface EndFilter { time_end: string, id_end: string, }

View File

@ -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, }
export interface GetTimelineReq { limit?: bigint, filter?: NoteListFilter, range?: SpanFilter, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NoFilter = Record<string, never>;

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface RangeFilter { time_start: string, time_end: string, id_start: string, id_end: string, }

View File

@ -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;

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface StartFilter { time_start: string, id_start: string, }

View File

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

View File

@ -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<PackNoteMaybeFull>

View File

@ -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<U64Range<1, 100>>,
#[ts(optional)]
pub filter: Option<NoteListFilter>,
#[ts(optional)]
pub range: Option<SpanFilter>,
}
pub fn default_timeline_limit<const MIN: u64, const MAX: u64>() -> U64Range<MIN, MAX> {
@ -22,7 +25,7 @@ pub fn default_timeline_limit<const MIN: u64, const MAX: u64>() -> U64Range<MIN,
#[derive(Endpoint)]
#[endpoint(
endpoint = "/timeline",
endpoint = "/timeline/:timeline_type",
method = Method::GET,
request = "GetTimelineReq",
response = "Vec<PackNoteMaybeFull>",

View File

@ -11,10 +11,40 @@ use std::ops::RangeInclusive;
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub enum RangeFilter {
TimeRange(RangeInclusive<DateTime<Utc>>),
TimeStart(DateTime<Utc>),
TimeEnd(DateTime<Utc>),
#[ts(export)]
pub struct RangeFilter {
pub time_start: DateTime<Utc>,
pub time_end: DateTime<Utc>,
pub id_start: DateTime<Utc>,
pub id_end: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct StartFilter {
pub time_start: DateTime<Utc>,
pub id_start: DateTime<Utc>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct EndFilter {
pub time_end: DateTime<Utc>,
pub id_end: DateTime<Utc>,
}
#[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)]

View File

@ -97,8 +97,8 @@ pack!(
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct NoteDetailExt {
pub parent_note: Option<Box<PackNoteMaybeAttachments>>,
pub renoted_note: Option<Box<PackNoteMaybeAttachments>>,
pub parent_note: Option<Box<PackNoteMaybeFull>>,
pub renoted_note: Option<Box<PackNoteMaybeFull>>,
}
pack!(

View File

@ -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,
}

View File

@ -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<NoteDetailSource<'a>> for NoteDetailExt {

View File

@ -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,29 +429,24 @@ impl NoteModel {
)))
}
async fn pack_full_single(
&self,
ctx: &PackingContext,
note: NoteData,
) -> PackResult<PackNoteMaybeFull> {
fn pack_full_single<'a>(
&'a self,
ctx: &'a PackingContext,
note: &'a NoteData,
) -> BoxFuture<'a, PackResult<PackNoteMaybeFull>> {
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),
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),
Some(r) if self.with_context => self.pack_full_single(ctx, r).await.map(Some),
_ => Ok(None),
}
};
@ -489,6 +484,8 @@ impl NoteModel {
Optional(detail),
)))
}
.boxed()
}
pub async fn fetch_single(
&self,
@ -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, &note).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::<Vec<_>>();
let processed = futures::stream::iter(fut_iter)
.buffered(10)