Basic notification fetching via Magnetar

This commit is contained in:
Natty 2024-01-16 18:10:56 +01:00
parent 98fb2ef0d8
commit ad3528055f
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
37 changed files with 857 additions and 266 deletions

View File

@ -98,14 +98,10 @@ pub enum NotificationTypeEnum {
Follow, Follow,
#[sea_orm(string_value = "followRequestAccepted")] #[sea_orm(string_value = "followRequestAccepted")]
FollowRequestAccepted, FollowRequestAccepted,
#[sea_orm(string_value = "groupInvited")]
GroupInvited,
#[sea_orm(string_value = "mention")] #[sea_orm(string_value = "mention")]
Mention, Mention,
#[sea_orm(string_value = "pollEnded")] #[sea_orm(string_value = "pollEnded")]
PollEnded, PollEnded,
#[sea_orm(string_value = "pollVote")]
PollVote,
#[sea_orm(string_value = "quote")] #[sea_orm(string_value = "quote")]
Quote, Quote,
#[sea_orm(string_value = "reaction")] #[sea_orm(string_value = "reaction")]

View File

@ -9,6 +9,7 @@ mod m20240107_220523_generated_is_quote;
mod m20240107_224446_generated_is_renote; mod m20240107_224446_generated_is_renote;
mod m20240112_215106_remove_pages; mod m20240112_215106_remove_pages;
mod m20240112_234759_remove_gallery; mod m20240112_234759_remove_gallery;
mod m20240115_212109_remove_poll_vote_notification;
pub struct Migrator; pub struct Migrator;
@ -25,6 +26,7 @@ impl MigratorTrait for Migrator {
Box::new(m20240107_224446_generated_is_renote::Migration), Box::new(m20240107_224446_generated_is_renote::Migration),
Box::new(m20240112_215106_remove_pages::Migration), Box::new(m20240112_215106_remove_pages::Migration),
Box::new(m20240112_234759_remove_gallery::Migration), Box::new(m20240112_234759_remove_gallery::Migration),
Box::new(m20240115_212109_remove_poll_vote_notification::Migration),
] ]
} }
} }

View File

@ -0,0 +1,45 @@
use sea_orm_migration::prelude::*;
use sea_orm_migration::sea_orm::TransactionTrait;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
DELETE FROM "notification" WHERE "type" = 'pollVote' OR "type" = 'groupInvited';
ALTER TYPE "notification_type_enum" RENAME TO "notification_type_enum_old";
CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'app');
ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::text::notification_type_enum;
DROP TYPE "notification_type_enum_old";
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
let txn = db.begin().await?;
db.execute_unprepared(
r#"
ALTER TYPE "notification_type_enum" RENAME TO "notification_type_enum_old";
CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app');
ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::text::notification_type_enum;
DROP TYPE "notification_type_enum_old";
"#,
)
.await?;
txn.commit().await?;
Ok(())
}
}

View File

@ -5,8 +5,8 @@ use ext_calckey_model_migration::{
use magnetar_sdk::types::SpanFilter; use magnetar_sdk::types::SpanFilter;
use sea_orm::{ use sea_orm::{
ColumnTrait, Condition, ConnectionTrait, Cursor, DbErr, DynIden, EntityTrait, FromQueryResult, ColumnTrait, Condition, ConnectionTrait, Cursor, DbErr, DynIden, EntityTrait, FromQueryResult,
Iden, IntoIdentity, Iterable, JoinType, RelationDef, RelationTrait, Select, SelectModel, Iden, IntoIdentity, Iterable, JoinType, QueryTrait, RelationDef, RelationTrait, Select,
SelectorTrait, SelectModel, SelectorTrait,
}; };
use std::fmt::Write; use std::fmt::Write;
@ -224,6 +224,7 @@ pub trait CursorPaginationExt<E> {
fn cursor_by_columns_and_span<C>( fn cursor_by_columns_and_span<C>(
self, self,
cursor_prefix_alias: Option<MagIden>,
order_columns: C, order_columns: C,
pagination: &SpanFilter, pagination: &SpanFilter,
limit: Option<u64>, limit: Option<u64>,
@ -234,6 +235,7 @@ pub trait CursorPaginationExt<E> {
async fn get_paginated_model<M, C, T>( async fn get_paginated_model<M, C, T>(
self, self,
db: &T, db: &T,
cursor_prefix_alias: Option<MagIden>,
columns: C, columns: C,
curr: &SpanFilter, curr: &SpanFilter,
prev: &mut Option<SpanFilter>, prev: &mut Option<SpanFilter>,
@ -255,6 +257,7 @@ where
fn cursor_by_columns_and_span<C>( fn cursor_by_columns_and_span<C>(
self, self,
cursor_prefix_alias: Option<MagIden>,
order_columns: C, order_columns: C,
pagination: &SpanFilter, pagination: &SpanFilter,
limit: Option<u64>, limit: Option<u64>,
@ -262,7 +265,11 @@ where
where where
C: IntoIdentity, C: IntoIdentity,
{ {
let mut cursor = self.cursor_by(order_columns); let mut cursor = Cursor::new(
self.into_query(),
cursor_prefix_alias.map_or_else(|| E::default().into_iden(), MagIden::into_iden),
order_columns,
);
if let Some(start) = pagination.start() { if let Some(start) = pagination.start() {
cursor.after(start); cursor.after(start);
@ -286,6 +293,7 @@ where
async fn get_paginated_model<Q, C, T>( async fn get_paginated_model<Q, C, T>(
self, self,
db: &T, db: &T,
cursor_prefix_alias: Option<MagIden>,
columns: C, columns: C,
curr: &SpanFilter, curr: &SpanFilter,
prev: &mut Option<SpanFilter>, prev: &mut Option<SpanFilter>,
@ -298,7 +306,7 @@ where
T: ConnectionTrait, T: ConnectionTrait,
{ {
let mut result = self let mut result = self
.cursor_by_columns_and_span(columns, curr, Some(limit + 1)) .cursor_by_columns_and_span(cursor_prefix_alias, columns, curr, Some(limit + 1))
.into_model::<Q>() .into_model::<Q>()
.all(db) .all(db)
.await?; .await?;

View File

@ -3,8 +3,8 @@ pub mod data;
use ext_calckey_model_migration::SelectStatement; use ext_calckey_model_migration::SelectStatement;
use sea_orm::sea_query::{Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr}; use sea_orm::sea_query::{Asterisk, Expr, IntoIden, Query, SelectExpr, SimpleExpr};
use sea_orm::{ use sea_orm::{
Condition, EntityTrait, Iden, IntoSimpleExpr, JoinType, QueryFilter, QueryOrder, QuerySelect, Condition, EntityTrait, Iden, JoinType, QueryFilter, QueryOrder, QuerySelect, QueryTrait,
QueryTrait, Select, Select,
}; };
use std::sync::Arc; use std::sync::Arc;
@ -177,6 +177,7 @@ impl NoteResolver {
notes_select notes_select
.get_paginated_model::<NoteData, _, _>( .get_paginated_model::<NoteData, _, _>(
self.db.inner(), self.db.inner(),
Some(note::Entity.base_prefix()),
(note::Column::CreatedAt, note::Column::Id), (note::Column::CreatedAt, note::Column::Id),
pagination, pagination,
&mut None, &mut None,

View File

@ -7,10 +7,13 @@ use crate::note_model::{NoteResolveOptions, NoteResolver};
use crate::user_model::{UserData, UserResolveOptions, UserResolver}; use crate::user_model::{UserData, UserResolveOptions, UserResolver};
use crate::{CalckeyDbError, CalckeyModel}; use crate::{CalckeyDbError, CalckeyModel};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ck::sea_orm_active_enums::NotificationTypeEnum;
use ck::{access_token, notification, user}; use ck::{access_token, notification, user};
use ext_calckey_model_migration::{JoinType, SelectStatement}; use ext_calckey_model_migration::{JoinType, SelectStatement};
use magnetar_sdk::types::SpanFilter; use magnetar_sdk::types::SpanFilter;
use sea_orm::Iden; use sea_orm::prelude::Expr;
use sea_orm::sea_query::{IntoCondition, Query};
use sea_orm::{ActiveEnum, Iden, IntoSimpleExpr, QueryTrait};
use sea_orm::{DbErr, EntityTrait, FromQueryResult, QueryFilter, QueryResult, QuerySelect}; use sea_orm::{DbErr, EntityTrait, FromQueryResult, QueryFilter, QueryResult, QuerySelect};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -55,12 +58,14 @@ impl ModelPagination for NotificationData {
} }
} }
const NOTIFICATION: &str = "notification.";
const NOTIFIER: &str = "notifier."; const NOTIFIER: &str = "notifier.";
const NOTIFICATION_NOTE: &str = "note."; const NOTIFICATION_NOTE: &str = "note.";
const ACCESS_TOKEN: &str = "access_token."; const ACCESS_TOKEN: &str = "access_token.";
pub struct NotificationResolveOptions {} pub struct NotificationResolveOptions {
pub note_options: NoteResolveOptions,
pub user_options: UserResolveOptions,
}
#[derive(Clone)] #[derive(Clone)]
pub struct NotificationResolver { pub struct NotificationResolver {
@ -86,11 +91,12 @@ impl NotificationResolver {
&self, &self,
q: &mut SelectStatement, q: &mut SelectStatement,
notification_tbl: &MagIden, notification_tbl: &MagIden,
note_options: &NoteResolveOptions, resolve_options: &NotificationResolveOptions,
user_options: &UserResolveOptions,
note_resolver: &NoteResolver, note_resolver: &NoteResolver,
user_resolver: &UserResolver, user_resolver: &UserResolver,
) { ) {
q.add_aliased_columns::<notification::Entity>(&notification_tbl);
let notifier_tbl = notification_tbl.join_str(NOTIFIER); let notifier_tbl = notification_tbl.join_str(NOTIFIER);
q.add_aliased_columns::<user::Entity>(&notifier_tbl); q.add_aliased_columns::<user::Entity>(&notifier_tbl);
q.join_columns( q.join_columns(
@ -98,7 +104,7 @@ impl NotificationResolver {
notification::Relation::User2.with_from_alias(notification_tbl), notification::Relation::User2.with_from_alias(notification_tbl),
&notifier_tbl, &notifier_tbl,
); );
user_resolver.resolve(q, &notifier_tbl, &user_options); user_resolver.resolve(q, &notifier_tbl, &resolve_options.user_options);
let token_tbl = notification_tbl.join_str(ACCESS_TOKEN); let token_tbl = notification_tbl.join_str(ACCESS_TOKEN);
q.add_aliased_columns::<access_token::Entity>(&token_tbl); q.add_aliased_columns::<access_token::Entity>(&token_tbl);
@ -114,14 +120,22 @@ impl NotificationResolver {
notification::Relation::Note.with_from_alias(notification_tbl), notification::Relation::Note.with_from_alias(notification_tbl),
&note_tbl, &note_tbl,
); );
note_resolver.attach_note(q, &note_tbl, 1, 1, note_options, &self.user_resolver); note_resolver.attach_note(
q,
&note_tbl,
1,
1,
&resolve_options.note_options,
&self.user_resolver,
);
} }
pub async fn get( pub async fn get(
&self, &self,
note_options: &NoteResolveOptions, resolve_options: &NotificationResolveOptions,
user_options: &UserResolveOptions,
user_id: &str, user_id: &str,
notification_types: &[NotificationTypeEnum],
unread_only: bool,
pagination: &SpanFilter, pagination: &SpanFilter,
prev: &mut Option<SpanFilter>, prev: &mut Option<SpanFilter>,
next: &mut Option<SpanFilter>, next: &mut Option<SpanFilter>,
@ -129,26 +143,46 @@ impl NotificationResolver {
) -> Result<Vec<NotificationData>, CalckeyDbError> { ) -> Result<Vec<NotificationData>, CalckeyDbError> {
let notification_tbl = notification::Entity.base_prefix(); let notification_tbl = notification::Entity.base_prefix();
let mut select = notification::Entity::find(); let mut query = Query::select();
query.from_as(notification::Entity, notification_tbl.clone());
let query = QuerySelect::query(&mut select);
self.resolve( self.resolve(
query, &mut query,
&notification_tbl, &notification_tbl,
note_options, &resolve_options,
user_options,
&self.note_resolver, &self.note_resolver,
&self.user_resolver, &self.user_resolver,
); );
let mut select = notification::Entity::find();
*QuerySelect::query(&mut select) = query;
let notifications = select let notifications = select
.filter( .filter(
notification_tbl notification_tbl
.col(notification::Column::NotifieeId) .col(notification::Column::NotifieeId)
.eq(user_id), .eq(user_id)
.and(
notification_tbl.col(notification::Column::Type).is_in(
notification_types
.iter()
.copied()
.map(Expr::val)
.map(|e| e.cast_as(NotificationTypeEnum::name())),
),
),
) )
.apply_if(unread_only.then_some(()), |s, _| {
s.filter(
notification_tbl
.col(notification::Column::IsRead)
.not()
.into_condition(),
)
})
.get_paginated_model::<NotificationData, _, _>( .get_paginated_model::<NotificationData, _, _>(
&self.db.0, &self.db.0,
Some(notification_tbl),
(notification::Column::CreatedAt, notification::Column::Id), (notification::Column::CreatedAt, notification::Column::Id),
pagination, pagination,
prev, prev,

View File

@ -200,6 +200,7 @@ impl UserResolver {
.filter(follow_request::Column::FolloweeId.eq(followee)) .filter(follow_request::Column::FolloweeId.eq(followee))
.get_paginated_model::<UserFollowRequestData, _, _>( .get_paginated_model::<UserFollowRequestData, _, _>(
&self.db.0, &self.db.0,
None,
( (
follow_request::Column::CreatedAt, follow_request::Column::CreatedAt,
follow_request::Column::Id, follow_request::Column::Id,
@ -241,6 +242,7 @@ impl UserResolver {
.filter(following::Column::FollowerId.eq(follower)) .filter(following::Column::FollowerId.eq(follower))
.get_paginated_model::<UserFollowData, _, _>( .get_paginated_model::<UserFollowData, _, _>(
&self.db.0, &self.db.0,
None,
(following::Column::CreatedAt, following::Column::Id), (following::Column::CreatedAt, following::Column::Id),
pagination, pagination,
prev, prev,
@ -279,6 +281,7 @@ impl UserResolver {
.filter(following::Column::FolloweeId.eq(followee)) .filter(following::Column::FolloweeId.eq(followee))
.get_paginated_model::<UserFollowData, _, _>( .get_paginated_model::<UserFollowData, _, _>(
&self.db.0, &self.db.0,
None,
(following::Column::CreatedAt, following::Column::Id), (following::Column::CreatedAt, following::Column::Id),
pagination, pagination,
prev, prev,

View File

@ -286,7 +286,7 @@ const reactButton = ref<HTMLElement | null>(null);
let appearNote = $computed( let appearNote = $computed(
() => magEffectiveNote(note) as packed.PackNoteMaybeFull () => magEffectiveNote(note) as packed.PackNoteMaybeFull
); );
const isMyRenote = $i && $i.id === note.user.id; const isMyRenote = $i && $i.id === appearNote.user.id;
const showContent = ref(false); const showContent = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords)); const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));

View File

@ -6,13 +6,24 @@
:class="notification.type" :class="notification.type"
> >
<div class="head"> <div class="head">
<MagAvatarResolvingProxy <MagAvatar
v-if="notification.type === 'pollEnded'" v-if="
notification.type === 'Renote' ||
notification.type === 'Reply' ||
notification.type === 'Mention' ||
notification.type === 'Quote' ||
notification.type === 'PollEnd'
"
class="icon" class="icon"
:user="notification.note.user" :user="notification.note.user"
/> />
<MagAvatarResolvingProxy <MagAvatar
v-else-if="notification.user" v-else-if="
notification.type === 'Reaction' ||
notification.type === 'FollowRequestAccepted' ||
notification.type === 'Follow' ||
notification.type === 'FollowRequestReceived'
"
class="icon" class="icon"
:user="notification.user" :user="notification.user"
/> />
@ -24,55 +35,51 @@
/> />
<div class="sub-icon" :class="notification.type"> <div class="sub-icon" :class="notification.type">
<i <i
v-if="notification.type === 'follow'" v-if="notification.type === 'Follow'"
class="ph-hand-waving ph-bold" class="ph-hand-waving ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'receiveFollowRequest'" v-else-if="notification.type === 'FollowRequestReceived'"
class="ph-clock ph-bold" class="ph-clock ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'followRequestAccepted'" v-else-if="notification.type === 'FollowRequestAccepted'"
class="ph-check ph-bold" class="ph-check ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'renote'" v-else-if="notification.type === 'Renote'"
class="ph-repeat ph-bold" class="ph-repeat ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'reply'" v-else-if="notification.type === 'Reply'"
class="ph-arrow-bend-up-left ph-bold" class="ph-arrow-bend-up-left ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'mention'" v-else-if="notification.type === 'Mention'"
class="ph-at ph-bold" class="ph-at ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'quote'" v-else-if="notification.type === 'Quote'"
class="ph-quotes ph-bold" class="ph-quotes ph-bold"
></i> ></i>
<i <i
v-else-if="notification.type === 'pollVote'" v-else-if="notification.type === 'PollEnd'"
class="ph-microphone-stage ph-bold"
></i>
<i
v-else-if="notification.type === 'pollEnded'"
class="ph-microphone-stage ph-bold" class="ph-microphone-stage ph-bold"
></i> ></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MagEmoji <MagEmoji
v-else-if=" v-else-if="
showEmojiReactions && notification.type === 'reaction' showEmojiReactions && notification.type === 'Reaction'
" "
ref="reactionRef" ref="reactionRef"
:emoji="normalizeNotifReaction(notification)" :emoji="notification.reaction"
:is-reaction="true" :is-reaction="true"
:normal="true" :normal="true"
:no-style="true" :no-style="true"
/> />
<MagEmoji <MagEmoji
v-else-if=" v-else-if="
!showEmojiReactions && notification.type === 'reaction' !showEmojiReactions && notification.type === 'Reaction'
" "
:emoji="defaultReaction" :emoji="defaultReaction"
:is-reaction="true" :is-reaction="true"
@ -83,11 +90,29 @@
</div> </div>
<div class="tail"> <div class="tail">
<header> <header>
<span v-if="notification.type === 'pollEnded'">{{ <span v-if="notification.type === 'PollEnd'">{{
i18n.ts._notification.pollEnded i18n.ts._notification.pollEnded
}}</span> }}</span>
<MkA <MkA
v-else-if="notification.user" v-if="
notification.type === 'Renote' ||
notification.type === 'Reply' ||
notification.type === 'Mention' ||
notification.type === 'Quote' ||
notification.type === 'PollEnd'
"
v-user-preview="notification.note.user.id"
class="name"
:to="userPage(notification.note.user)"
><MkUserName :user="notification.note.user"
/></MkA>
<MkA
v-else-if="
notification.type === 'Reaction' ||
notification.type === 'FollowRequestAccepted' ||
notification.type === 'Follow' ||
notification.type === 'FollowRequestReceived'
"
v-user-preview="notification.user.id" v-user-preview="notification.user.id"
class="name" class="name"
:to="userPage(notification.user)" :to="userPage(notification.user)"
@ -96,12 +121,12 @@
<span v-else>{{ notification.header }}</span> <span v-else>{{ notification.header }}</span>
<MkTime <MkTime
v-if="withTime" v-if="withTime"
:time="notification.createdAt" :time="notification.created_at"
class="time" class="time"
/> />
</header> </header>
<MkA <MkA
v-if="notification.type === 'reaction'" v-if="notification.type === 'Reaction'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="getNoteSummary(notification.note)" :title="getNoteSummary(notification.note)"
@ -117,23 +142,23 @@
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
</MkA> </MkA>
<MkA <MkA
v-if="notification.type === 'renote'" v-if="notification.type === 'Renote'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note.renoted_note!)"
:title="getNoteSummary(notification.note.renote!)" :title="getNoteSummary(notification.note.renoted_note!)"
> >
<span>{{ i18n.ts._notification.renoted }}</span> <span>{{ i18n.ts._notification.renoted }}</span>
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
<Mfm <Mfm
:text="getNoteSummary(notification.note.renote!)" :text="getNoteSummary(notification.note.renoted_note!)"
:plain="true" :plain="true"
:nowrap="!full" :nowrap="!full"
:custom-emojis="notification.note.renote.emojis" :custom-emojis="notification.note.renoted_note!.emojis"
/> />
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
</MkA> </MkA>
<MkA <MkA
v-if="notification.type === 'reply'" v-if="notification.type === 'Reply'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="getNoteSummary(notification.note)" :title="getNoteSummary(notification.note)"
@ -146,7 +171,7 @@
/> />
</MkA> </MkA>
<MkA <MkA
v-if="notification.type === 'mention'" v-if="notification.type === 'Mention'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="getNoteSummary(notification.note)" :title="getNoteSummary(notification.note)"
@ -159,7 +184,7 @@
/> />
</MkA> </MkA>
<MkA <MkA
v-if="notification.type === 'quote'" v-if="notification.type === 'Quote'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="getNoteSummary(notification.note)" :title="getNoteSummary(notification.note)"
@ -172,23 +197,7 @@
/> />
</MkA> </MkA>
<MkA <MkA
v-if="notification.type === 'pollVote'" v-if="notification.type === 'PollEnd'"
class="text"
:to="notePage(notification.note)"
:title="getNoteSummary(notification.note)"
>
<span>{{ i18n.ts._notification.voted }}</span>
<i class="ph-quotes ph-fill ph-lg"></i>
<Mfm
:text="getNoteSummary(notification.note)"
:plain="true"
:nowrap="!full"
:custom-emojis="notification.note?.emojis"
/>
<i class="ph-quotes ph-fill ph-lg"></i>
</MkA>
<MkA
v-if="notification.type === 'pollEnded'"
class="text" class="text"
:to="notePage(notification.note)" :to="notePage(notification.note)"
:title="getNoteSummary(notification.note)" :title="getNoteSummary(notification.note)"
@ -203,7 +212,7 @@
<i class="ph-quotes ph-fill ph-lg"></i> <i class="ph-quotes ph-fill ph-lg"></i>
</MkA> </MkA>
<span <span
v-if="notification.type === 'follow'" v-if="notification.type === 'Follow'"
class="text" class="text"
style="opacity: 0.7" style="opacity: 0.7"
>{{ i18n.ts.youGotNewFollower }} >{{ i18n.ts.youGotNewFollower }}
@ -216,13 +225,13 @@
</div> </div>
</span> </span>
<span <span
v-if="notification.type === 'followRequestAccepted'" v-if="notification.type === 'FollowRequestAccepted'"
class="text" class="text"
style="opacity: 0.7" style="opacity: 0.7"
>{{ i18n.ts.followRequestAccepted }}</span >{{ i18n.ts.followRequestAccepted }}</span
> >
<span <span
v-if="notification.type === 'receiveFollowRequest'" v-if="notification.type === 'FollowRequestReceived'"
class="text" class="text"
style="opacity: 0.7" style="opacity: 0.7"
>{{ i18n.ts.receiveFollowRequest }} >{{ i18n.ts.receiveFollowRequest }}
@ -233,7 +242,7 @@
/> />
</div> </div>
</span> </span>
<span v-if="notification.type === 'app'" class="text"> <span v-if="notification.type === 'App'" class="text">
<Mfm :text="notification.body" :nowrap="!full" /> <Mfm :text="notification.body" :nowrap="!full" />
</span> </span>
</div> </div>
@ -242,25 +251,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref, watch } from "vue"; import { onMounted, onUnmounted, ref, watch } from "vue";
import * as misskey from "calckey-js";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import XReactionTooltip from "@/components/MkReactionTooltip.vue"; import XReactionTooltip from "@/components/MkReactionTooltip.vue";
import { getNoteSummary } from "@/scripts/get-note-summary"; import { getNoteSummary } from "@/scripts/get-note-summary";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { i18n } from "@/i18n";
import * as os from "@/os"; import * as os from "@/os";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { useTooltip } from "@/scripts/use-tooltip"; import { useTooltip } from "@/scripts/use-tooltip";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { instance } from "@/instance"; import { instance } from "@/instance";
import MkFollowApproveButton from "@/components/MkFollowApproveButton.vue"; import MkFollowApproveButton from "@/components/MkFollowApproveButton.vue";
import { magConvertReaction, magIsCustomEmoji } from "@/scripts-mag/mag-util"; import { packed } from "magnetar-common";
import { types } from "magnetar-common"; import { i18n } from "@/i18n";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
notification: misskey.entities.Notification; notification: packed.PackNotification;
withTime?: boolean; withTime?: boolean;
full?: boolean; full?: boolean;
}>(), }>(),
@ -281,32 +288,11 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
? instance.defaultReaction ? instance.defaultReaction
: "⭐"; : "⭐";
function normalizeNotifReaction(
notification: misskey.entities.Notification & { type: "reaction" }
): types.Reaction {
return notification.reaction
? magConvertReaction(
notification.reaction,
(name, host) =>
notification.note.emojis.find((e) => {
const parsed = magConvertReaction(`:${e.name}:`, e.url);
if (!magIsCustomEmoji(parsed)) return false;
return (
parsed.name === name &&
(parsed.host ?? null) === (host ?? null)
);
})?.url!
)
: notification.reaction;
}
let readObserver: IntersectionObserver | undefined; let readObserver: IntersectionObserver | undefined;
let connection; let connection;
onMounted(() => { onMounted(() => {
if (!props.notification.isRead) { if (!props.notification.is_read) {
readObserver = new IntersectionObserver((entries, observer) => { readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some((entry) => entry.isIntersecting)) return; if (!entries.some((entry) => entry.isIntersecting)) return;
stream.send("readNotification", { stream.send("readNotification", {
@ -318,11 +304,14 @@ onMounted(() => {
readObserver.observe(elRef.value); readObserver.observe(elRef.value);
connection = stream.useChannel("main"); connection = stream.useChannel("main");
connection.on("readAllNotifications", () => readObserver.disconnect()); connection.on("readAllNotifications", () => readObserver!.disconnect());
watch(props.notification.isRead, () => { watch(
readObserver.disconnect(); () => props.notification.is_read,
}); () => {
readObserver!.disconnect();
}
);
} }
}); });
@ -334,13 +323,13 @@ onUnmounted(() => {
const followRequestDone = ref(false); const followRequestDone = ref(false);
useTooltip(reactionRef, (showing) => { useTooltip(reactionRef, (showing) => {
if (props.notification.type !== "reaction") return; if (props.notification.type !== "Reaction") return;
os.popup( os.popup(
XReactionTooltip, XReactionTooltip,
{ {
showing, showing,
reaction: normalizeNotifReaction(props.notification), reaction: props.notification.reaction,
targetElement: reactionRef.value.$el, targetElement: reactionRef.value.$el,
}, },
{}, {},
@ -406,46 +395,39 @@ useTooltip(reactionRef, (showing) => {
height: 100%; height: 100%;
} }
&.follow, &.Follow,
&.followRequestAccepted, &.FollowRequestAccepted,
&.receiveFollowRequest, &.FollowRequestReceived {
&.groupInvited {
padding: 3px; padding: 3px;
background: #31748f; background: #31748f;
pointer-events: none; pointer-events: none;
} }
&.renote { &.Renote {
padding: 3px; padding: 3px;
background: #31748f; background: #31748f;
pointer-events: none; pointer-events: none;
} }
&.quote { &.Quote {
padding: 3px; padding: 3px;
background: #31748f; background: #31748f;
pointer-events: none; pointer-events: none;
} }
&.reply { &.Reply {
padding: 3px; padding: 3px;
background: #c4a7e7; background: #c4a7e7;
pointer-events: none; pointer-events: none;
} }
&.mention { &.Mention {
padding: 3px; padding: 3px;
background: #908caa; background: #908caa;
pointer-events: none; pointer-events: none;
} }
&.pollVote { &.PollEnd {
padding: 3px;
background: #908caa;
pointer-events: none;
}
&.pollEnded {
padding: 3px; padding: 3px;
background: #908caa; background: #908caa;
pointer-events: none; pointer-events: none;

View File

@ -1,5 +1,5 @@
<template> <template>
<MkPagination ref="pagingComponent" :pagination="pagination"> <MagPagination ref="pagingComponent" :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img <img
@ -11,26 +11,23 @@
</div> </div>
</template> </template>
<template #default="{ items: notifications }"> <template #items="{ items: notifications }">
<XList <XList
v-slot="{ item: notification }" v-slot="{ item: notification }"
class="elsfgstc" class="elsfgstc"
:items="notifications" :items="notifications"
:no-gap="true" :no-gap="true"
> >
<XNoteResolvingProxy <XNote
v-if=" v-if="
['reply', 'quote', 'mention'].includes( notification.note &&
notification.type (notification.type === 'Quote' ||
) notification.type === 'Mention' ||
notification.type === 'Reply')
" "
:key="notification.id" :key="notification.id"
:note="notification.note.id" :note="notification.note"
:collapsedReply=" :collapsedReply="!!notification.note.parent_note"
notification.type === 'reply' ||
(notification.type === 'mention' &&
notification.note.replyId != null)
"
/> />
<XNotification <XNotification
v-else v-else
@ -42,37 +39,36 @@
/> />
</XList> </XList>
</template> </template>
</MkPagination> </MagPagination>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue"; import { onMounted, onUnmounted, ref } from "vue";
import { notificationTypes } from "calckey-js";
import MkPagination, { Paging } from "@/components/MkPagination.vue";
import XNotification from "@/components/MkNotification.vue"; import XNotification from "@/components/MkNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNoteResolvingProxy from "@/components/MagNoteResolvingProxy.vue"; import XNote from "@/components/MagNote.vue";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import MagPagination, { Paging } from "@/components/MagPagination.vue";
import { endpoints, types } from "magnetar-common";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
const props = defineProps<{ const props = defineProps<{
includeTypes?: (typeof notificationTypes)[number][]; includeTypes?: types.NotificationType[];
unreadOnly?: boolean; unreadOnly?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = ref<InstanceType<typeof MagPagination>>();
const pagination: Paging = { const pagination: Paging = {
endpoint: "i/notifications" as const, endpoint: endpoints.GetNotifications,
limit: 10, params: {
params: computed(() => ({ include_types: props.includeTypes ?? undefined,
includeTypes: props.includeTypes ?? undefined, exclude_types: props.includeTypes
excludeTypes: props.includeTypes
? undefined ? undefined
: $i?.mutingNotificationTypes, : $i?.mutingNotificationTypes,
unreadOnly: props.unreadOnly, unread_only: props.unreadOnly,
})), } as types.NotificationsReq,
}; };
const onNotification = (notification) => { const onNotification = (notification) => {

View File

@ -9,3 +9,4 @@ export { GetFollowersSelf } from "./types/endpoints/GetFollowersSelf";
export { GetFollowingById } from "./types/endpoints/GetFollowingById"; export { GetFollowingById } from "./types/endpoints/GetFollowingById";
export { GetFollowingSelf } from "./types/endpoints/GetFollowingSelf"; export { GetFollowingSelf } from "./types/endpoints/GetFollowingSelf";
export { GetFollowRequestsSelf } from "./types/endpoints/GetFollowRequestsSelf"; export { GetFollowRequestsSelf } from "./types/endpoints/GetFollowRequestsSelf";
export { GetNotifications } from "./types/endpoints/GetNotifications";

View File

@ -54,3 +54,4 @@ export { NotificationAppExt } from "./types/NotificationAppExt";
export { NotificationNoteExt } from "./types/NotificationNoteExt"; export { NotificationNoteExt } from "./types/NotificationNoteExt";
export { NotificationReactionExt } from "./types/NotificationReactionExt"; export { NotificationReactionExt } from "./types/NotificationReactionExt";
export { NotificationUserExt } from "./types/NotificationUserExt"; export { NotificationUserExt } from "./types/NotificationUserExt";
export { NotificationsReq } from "./types/NotificationsReq";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NotificationType } from "./NotificationType";
export interface NotificationsReq { include_types?: Array<NotificationType>, exclude_types?: Array<NotificationType>, unread_only?: boolean, }

View File

@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NotificationsReq } from "../NotificationsReq";
import type { PackNotification } from "../PackNotification";
export const GetNotifications = {
endpoint: "/users/@self/notifications",
pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as NotificationsReq,
response: undefined as unknown as Array<PackNotification>,
paginated: true as true
}

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationAppExt } from "../NotificationAppExt"; import type { NotificationAppExt } from "../NotificationAppExt";
import type { NotificationBase } from "../NotificationBase";
export type PackNotificationApp = Id & NotificationAppExt; export type PackNotificationApp = Id & NotificationBase & NotificationAppExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationUserExt } from "../NotificationUserExt"; import type { NotificationUserExt } from "../NotificationUserExt";
export type PackNotificationFollow = Id & NotificationUserExt; export type PackNotificationFollow = Id & NotificationBase & NotificationUserExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationUserExt } from "../NotificationUserExt"; import type { NotificationUserExt } from "../NotificationUserExt";
export type PackNotificationFollowRequestAccepted = Id & NotificationUserExt; export type PackNotificationFollowRequestAccepted = Id & NotificationBase & NotificationUserExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationUserExt } from "../NotificationUserExt"; import type { NotificationUserExt } from "../NotificationUserExt";
export type PackNotificationFollowRequestReceived = Id & NotificationUserExt; export type PackNotificationFollowRequestReceived = Id & NotificationBase & NotificationUserExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationNoteExt } from "../NotificationNoteExt"; import type { NotificationNoteExt } from "../NotificationNoteExt";
export type PackNotificationMention = Id & NotificationNoteExt; export type PackNotificationMention = Id & NotificationBase & NotificationNoteExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationNoteExt } from "../NotificationNoteExt"; import type { NotificationNoteExt } from "../NotificationNoteExt";
export type PackNotificationPollEnd = Id & NotificationNoteExt; export type PackNotificationPollEnd = Id & NotificationBase & NotificationNoteExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationNoteExt } from "../NotificationNoteExt"; import type { NotificationNoteExt } from "../NotificationNoteExt";
export type PackNotificationQuote = Id & NotificationNoteExt; export type PackNotificationQuote = Id & NotificationBase & NotificationNoteExt;

View File

@ -1,6 +1,8 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationNoteExt } from "../NotificationNoteExt";
import type { NotificationReactionExt } from "../NotificationReactionExt"; import type { NotificationReactionExt } from "../NotificationReactionExt";
import type { NotificationUserExt } from "../NotificationUserExt"; import type { NotificationUserExt } from "../NotificationUserExt";
export type PackNotificationReaction = Id & NotificationReactionExt & NotificationUserExt; export type PackNotificationReaction = Id & NotificationBase & NotificationNoteExt & NotificationReactionExt & NotificationUserExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationNoteExt } from "../NotificationNoteExt"; import type { NotificationNoteExt } from "../NotificationNoteExt";
export type PackNotificationRenote = Id & NotificationNoteExt; export type PackNotificationRenote = Id & NotificationBase & NotificationNoteExt;

View File

@ -1,5 +1,6 @@
// 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 { Id } from "../Id"; import type { Id } from "../Id";
import type { NotificationBase } from "../NotificationBase";
import type { NotificationNoteExt } from "../NotificationNoteExt"; import type { NotificationNoteExt } from "../NotificationNoteExt";
export type PackNotificationReply = Id & NotificationNoteExt; export type PackNotificationReply = Id & NotificationBase & NotificationNoteExt;

View File

@ -1,5 +1,6 @@
use crate::endpoints::{Empty, Endpoint}; use crate::endpoints::{Empty, Endpoint};
use crate::util_types::deserialize_array_urlenc; use crate::types::notification::{NotificationType, PackNotification};
use crate::util_types::{deserialize_array_urlenc, deserialize_opt_array_urlenc};
use http::Method; use http::Method;
use magnetar_sdk_macros::Endpoint; use magnetar_sdk_macros::Endpoint;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -83,6 +84,31 @@ pub struct GetManyUsersById;
)] )]
pub struct GetUserByAcct; pub struct GetUserByAcct;
#[derive(Serialize, Deserialize, TS)]
#[ts(export)]
pub struct NotificationsReq {
#[ts(optional)]
#[serde(default)]
#[serde(deserialize_with = "deserialize_opt_array_urlenc")]
pub include_types: Option<Vec<NotificationType>>,
#[ts(optional)]
#[serde(default)]
#[serde(deserialize_with = "deserialize_opt_array_urlenc")]
pub exclude_types: Option<Vec<NotificationType>>,
#[ts(optional)]
pub unread_only: Option<bool>,
}
#[derive(Endpoint)]
#[endpoint(
endpoint = "/users/@self/notifications",
method = Method::GET,
request = "NotificationsReq",
response = "Vec<PackNotification>",
paginated = true
)]
pub struct GetNotifications;
#[derive(Endpoint)] #[derive(Endpoint)]
#[endpoint( #[endpoint(
endpoint = "/users/:id/followers", endpoint = "/users/:id/followers",

View File

@ -29,7 +29,7 @@ pub(crate) mod packed_time {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
Ok(DateTime::<Utc>::from_utc( Ok(DateTime::<Utc>::from_naive_utc_and_offset(
NaiveDateTime::from_timestamp_millis( NaiveDateTime::from_timestamp_millis(
String::deserialize(deserializer)? String::deserialize(deserializer)?
.parse::<i64>() .parse::<i64>()

View File

@ -1,13 +1,11 @@
use crate::types::note::{ use crate::types::note::{PackNoteMaybeFull, Reaction};
PackNoteMaybeFull, Reaction, ReactionShortcode, ReactionUnicode, ReactionUnknown,
};
use crate::types::user::PackUserBase; use crate::types::user::PackUserBase;
use crate::types::Id; use crate::types::Id;
use crate::{Packed, Required}; use crate::{Packed, Required};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use magnetar_sdk_macros::pack; use magnetar_sdk_macros::pack;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::EnumDiscriminants; use strum::{EnumDiscriminants, EnumIter};
use ts_rs::TS; use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
@ -44,21 +42,21 @@ pub struct NotificationAppExt {
pub icon: String, pub icon: String,
} }
pack!(PackNotificationFollow, Required<Id> as id & Required<NotificationUserExt> as follower); pack!(PackNotificationFollow, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationUserExt> as follower);
pack!(PackNotificationFollowRequestReceived, Required<Id> as id & Required<NotificationUserExt> as follower); pack!(PackNotificationFollowRequestReceived, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationUserExt> as follower);
pack!(PackNotificationFollowRequestAccepted, Required<Id> as id & Required<NotificationUserExt> as follower); pack!(PackNotificationFollowRequestAccepted, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationUserExt> as follower);
pack!(PackNotificationMention, Required<Id> as id & Required<NotificationNoteExt> as note); pack!(PackNotificationMention, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationNoteExt> as note);
pack!(PackNotificationReply, Required<Id> as id & Required<NotificationNoteExt> as note); pack!(PackNotificationReply, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationNoteExt> as note);
pack!(PackNotificationRenote, Required<Id> as id & Required<NotificationNoteExt> as note); pack!(PackNotificationRenote, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationNoteExt> as note);
pack!(PackNotificationReaction, Required<Id> as id & Required<NotificationReactionExt> as reaction & Required<NotificationUserExt> as user); pack!(PackNotificationReaction, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationNoteExt> as note & Required<NotificationReactionExt> as reaction & Required<NotificationUserExt> as user);
pack!(PackNotificationQuote, Required<Id> as id & Required<NotificationNoteExt> as note); pack!(PackNotificationQuote, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationNoteExt> as note);
pack!(PackNotificationPollEnd, Required<Id> as id & Required<NotificationNoteExt> as note); pack!(PackNotificationPollEnd, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationNoteExt> as note);
pack!(PackNotificationApp, Required<Id> as id & Required<NotificationAppExt> as custom); pack!(PackNotificationApp, Required<Id> as id & Required<NotificationBase> as notification & Required<NotificationAppExt> as custom);
#[derive(Clone, Debug, Deserialize, Serialize, TS, EnumDiscriminants)] #[derive(Clone, Debug, Deserialize, Serialize, TS, EnumDiscriminants)]
#[strum_discriminants(name(NotificationType))] #[strum_discriminants(name(NotificationType))]
#[strum_discriminants(ts(export))] #[strum_discriminants(ts(export))]
#[strum_discriminants(derive(Deserialize, Serialize, TS))] #[strum_discriminants(derive(Deserialize, Serialize, Hash, TS, EnumIter))]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum PackNotification { pub enum PackNotification {

View File

@ -95,3 +95,23 @@ where
Ok(parts) Ok(parts)
} }
pub(crate) fn deserialize_opt_array_urlenc<'de, D, T: Eq + Hash>(
deserializer: D,
) -> Result<Option<Vec<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: DeserializeOwned,
{
let Some(str_raw) = Option::<String>::deserialize(deserializer)? else {
return Ok(None);
};
let parts = serde_urlencoded::from_str::<Vec<(T, String)>>(&str_raw)
.map_err(serde::de::Error::custom)?
.into_iter()
.map(|(k, _)| k)
.collect::<Vec<_>>();
Ok(Some(parts))
}

View File

@ -4,8 +4,8 @@ mod user;
use crate::api_v1::note::handle_note; use crate::api_v1::note::handle_note;
use crate::api_v1::user::{ use crate::api_v1::user::{
handle_follow_requests_self, handle_followers, handle_followers_self, handle_following, handle_follow_requests_self, handle_followers, handle_followers_self, handle_following,
handle_following_self, handle_user_by_id_many, handle_user_info, handle_user_info_by_acct, handle_following_self, handle_notifications, handle_user_by_id_many, handle_user_info,
handle_user_info_self, handle_user_info_by_acct, handle_user_info_self,
}; };
use crate::service::MagnetarService; use crate::service::MagnetarService;
use crate::web::auth; use crate::web::auth;
@ -21,6 +21,7 @@ pub fn create_api_router(service: Arc<MagnetarService>) -> Router {
.route("/users/by-acct/:id", get(handle_user_info_by_acct)) .route("/users/by-acct/:id", get(handle_user_info_by_acct))
.route("/users/lookup-many", get(handle_user_by_id_many)) .route("/users/lookup-many", get(handle_user_by_id_many))
.route("/users/:id", get(handle_user_info)) .route("/users/:id", get(handle_user_info))
.route("/users/@self/notifications", get(handle_notifications))
.route( .route(
"/users/@self/follow-requests", "/users/@self/follow-requests",
get(handle_follow_requests_self), get(handle_follow_requests_self),

View File

@ -1,3 +1,4 @@
use crate::model::processing::notification::NotificationModel;
use crate::model::processing::user::{UserBorrowedData, UserModel, UserShapedData}; use crate::model::processing::user::{UserBorrowedData, UserModel, UserShapedData};
use crate::model::PackingContext; use crate::model::PackingContext;
use crate::service::MagnetarService; use crate::service::MagnetarService;
@ -10,11 +11,14 @@ 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, GetFollowRequestsSelf, GetFollowersById, GetFollowersSelf, GetFollowingById, GetFollowingSelf,
GetManyUsersById, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq, GetManyUsersById, GetNotifications, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq,
NotificationsReq,
}; };
use magnetar_sdk::endpoints::{Req, Res}; use magnetar_sdk::endpoints::{Req, Res};
use magnetar_sdk::types::notification::NotificationType;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use strum::IntoEnumIterator;
pub async fn handle_user_info_self( pub async fn handle_user_info_self(
Query(req): Query<Req<GetUserSelf>>, Query(req): Query<Req<GetUserSelf>>,
@ -141,6 +145,40 @@ pub async fn handle_user_by_id_many(
Ok(Json(users_ordered)) Ok(Json(users_ordered))
} }
pub async fn handle_notifications(
Query(NotificationsReq {
ref exclude_types,
include_types,
unread_only,
}): Query<Req<GetNotifications>>,
State(service): State<Arc<MagnetarService>>,
AuthenticatedUser(user): AuthenticatedUser,
mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetNotifications>>), ApiError> {
let notification_types = include_types
.unwrap_or_else(|| NotificationType::iter().collect::<Vec<_>>())
.iter()
.filter(|t| {
exclude_types.is_none() || !exclude_types.as_ref().is_some_and(|tt| tt.contains(t))
})
.copied()
.collect::<Vec<_>>();
let ctx = PackingContext::new(service, Some(user.clone())).await?;
let notification_model = NotificationModel;
let notifications = notification_model
.get_notifications(
&ctx,
&user.id,
&notification_types,
unread_only.unwrap_or_default(),
&mut pagination,
)
.await?;
Ok((pagination, Json(notifications)))
}
pub async fn handle_following_self( pub async fn handle_following_self(
Query(_): Query<Req<GetFollowingSelf>>, Query(_): Query<Req<GetFollowingSelf>>,
State(service): State<Arc<MagnetarService>>, State(service): State<Arc<MagnetarService>>,

View File

@ -17,6 +17,7 @@ macro_rules! impl_id {
impl_id!(ck::emoji::Model); impl_id!(ck::emoji::Model);
impl_id!(ck::user::Model); impl_id!(ck::user::Model);
impl_id!(ck::note::Model); impl_id!(ck::note::Model);
impl_id!(ck::notification::Model);
impl BaseId for ck::poll::Model { impl BaseId for ck::poll::Model {
fn get_id(&self) -> &str { fn get_id(&self) -> &str {

View File

@ -3,5 +3,6 @@ pub mod emoji;
pub mod id; pub mod id;
pub mod instance; pub mod instance;
pub mod note; pub mod note;
pub mod notification;
pub mod poll; pub mod poll;
pub mod user; pub mod user;

View File

@ -0,0 +1,65 @@
use crate::model::{PackType, PackingContext};
use magnetar_calckey_model::ck;
use magnetar_sdk::types::note::{PackNoteMaybeFull, Reaction};
use magnetar_sdk::types::notification::{
NotificationAppExt, NotificationBase, NotificationNoteExt, NotificationReactionExt,
NotificationUserExt,
};
use magnetar_sdk::types::user::PackUserBase;
impl<'a> PackType<&'a ck::notification::Model> for NotificationBase {
fn extract(_: &PackingContext, data: &'a ck::notification::Model) -> Self {
NotificationBase {
id: data.id.clone(),
created_at: data.created_at.into(),
is_read: data.is_read,
}
}
}
impl<'a> PackType<&'a PackNoteMaybeFull> for NotificationNoteExt {
fn extract(_: &PackingContext, data: &'a PackNoteMaybeFull) -> Self {
NotificationNoteExt { note: data.clone() }
}
}
impl<'a> PackType<&'a PackUserBase> for NotificationUserExt {
fn extract(_: &PackingContext, data: &'a PackUserBase) -> Self {
NotificationUserExt { user: data.clone() }
}
}
impl<'a> PackType<&'a Reaction> for NotificationReactionExt {
fn extract(_: &PackingContext, data: &'a Reaction) -> Self {
NotificationReactionExt {
reaction: data.clone(),
}
}
}
impl<'a> PackType<(&'a ck::notification::Model, &'a ck::access_token::Model)>
for NotificationAppExt
{
fn extract(
_: &PackingContext,
(notification, access_token): (&'a ck::notification::Model, &'a ck::access_token::Model),
) -> Self {
NotificationAppExt {
body: notification.custom_body.clone().unwrap_or_default(),
header: notification
.custom_header
.as_ref()
.or(access_token.name.as_ref())
.map(String::to_string)
.clone()
.unwrap_or_default(),
icon: notification
.custom_icon
.as_ref()
.or(access_token.icon_url.as_ref())
.map(String::to_string)
.clone()
.unwrap_or_default(),
}
}
}

View File

@ -1,11 +1,57 @@
use crate::model::processing::PackResult; use crate::model::processing::{PackError, PackResult};
use crate::model::{PackType, PackingContext}; use crate::model::{PackType, PackingContext};
use either::Either;
use futures_util::TryFutureExt;
use itertools::Itertools; use itertools::Itertools;
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
use magnetar_calckey_model::emoji::EmojiTag; use magnetar_calckey_model::emoji::EmojiTag;
use magnetar_common::util::{parse_reaction, RawReaction};
use magnetar_sdk::types::emoji::{EmojiBase, PackEmojiBase}; use magnetar_sdk::types::emoji::{EmojiBase, PackEmojiBase};
use magnetar_sdk::types::note::{Reaction, ReactionShortcode, ReactionUnicode, ReactionUnknown};
use magnetar_sdk::types::Id; use magnetar_sdk::types::Id;
use magnetar_sdk::{Packed, Required}; use magnetar_sdk::{Packed, Required};
use std::sync::Arc;
pub fn parse_emoji_or_raw(tag: &str) -> Either<String, RawReaction> {
parse_reaction(tag).map_or_else(|| Either::Left(tag.to_string()), Either::Right)
}
pub fn shortcode_tag_or_none(value: &RawReaction) -> Option<EmojiTag<'_>> {
match value {
RawReaction::Shortcode { shortcode, host } => Some(EmojiTag {
name: shortcode.as_str(),
host: host.as_deref(),
}),
_ => None,
}
}
pub fn resolve_reaction<'a>(
value: RawReaction,
code_lookup: &impl Fn(&str, Option<&str>) -> Option<&'a ck::emoji::Model>,
) -> Reaction {
match value {
RawReaction::Unicode(text) => Reaction::Unicode(ReactionUnicode(text)),
RawReaction::Shortcode { shortcode, host } => code_lookup(&shortcode, host.as_deref())
.map_or_else(
|| {
Reaction::Unknown(ReactionUnknown {
raw: format!(
":{shortcode}{}:",
host.as_deref().map(|h| format!("@{h}")).unwrap_or_default()
),
})
},
|e| {
Reaction::Shortcode(ReactionShortcode {
name: shortcode.clone(),
host: host.clone(),
url: e.public_url.clone(),
})
},
),
}
}
pub struct EmojiModel; pub struct EmojiModel;
@ -48,4 +94,78 @@ impl EmojiModel {
.take(ctx.limits.max_emojis) .take(ctx.limits.max_emojis)
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub async fn resolve_reaction(
&self,
ctx: &PackingContext,
reaction: &str,
) -> PackResult<Reaction> {
let parsed = parse_emoji_or_raw(reaction);
Ok(match parsed {
Either::Left(raw) => Reaction::Unknown(ReactionUnknown { raw }),
Either::Right(raw) => {
let reaction_fetched = match shortcode_tag_or_none(&raw) {
Some(tag) => {
ctx.service
.emoji_cache
.get(tag.name, tag.host)
.map_err(PackError::from)
.await?
}
None => None,
};
let reaction_ref = reaction_fetched.as_ref().map(Arc::as_ref);
resolve_reaction(raw, &move |_, _| reaction_ref)
}
})
}
pub async fn resolve_reactions_many(
&self,
ctx: &PackingContext,
reactions_raw: &[String],
) -> PackResult<Vec<Reaction>> {
let reactions_parsed = reactions_raw
.iter()
.map(String::as_ref)
.map(parse_emoji_or_raw)
.collect::<Vec<_>>();
// Pick out all successfully-parsed shortcode emojis
let reactions_to_resolve = reactions_parsed
.iter()
.map(Either::as_ref)
.filter_map(Either::right)
.filter_map(shortcode_tag_or_none)
.collect::<Vec<_>>();
let reactions_fetched = ctx
.service
.emoji_cache
.get_many_tagged(&reactions_to_resolve)
.map_err(PackError::from)
.await?;
// Left reactions and the Right ones that didn't resolve to any emoji are turned back into Unknown
let reactions_resolved = reactions_parsed
.into_iter()
.map(|val| {
val.either(
|raw| Reaction::Unknown(ReactionUnknown { raw }),
|raw| {
resolve_reaction(raw, &|shortcode, host| {
reactions_fetched
.iter()
.find(|e| e.host.as_deref() == host && e.name == shortcode)
.map(Arc::as_ref)
})
},
)
})
.collect::<Vec<_>>();
Ok(reactions_resolved)
}
} }

View File

@ -10,6 +10,7 @@ use thiserror::Error;
pub mod drive; pub mod drive;
pub mod emoji; pub mod emoji;
pub mod note; pub mod note;
pub mod notification;
pub mod user; pub mod user;
#[derive(Debug, Error, strum::IntoStaticStr)] #[derive(Debug, Error, strum::IntoStaticStr)]
@ -18,7 +19,7 @@ pub enum PackError {
DbError(#[from] DbErr), DbError(#[from] DbErr),
#[error("Calckey database wrapper error: {0}")] #[error("Calckey database wrapper error: {0}")]
CalckeyDbError(#[from] CalckeyDbError), CalckeyDbError(#[from] CalckeyDbError),
#[error("Emoji cache error: {0}")] #[error("Data error: {0}")]
DataError(String), DataError(String),
#[error("Emoji cache error: {0}")] #[error("Emoji cache error: {0}")]
EmojiCacheError(#[from] EmojiCacheError), EmojiCacheError(#[from] EmojiCacheError),

View File

@ -7,9 +7,8 @@ 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};
use futures_util::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use futures_util::{FutureExt, StreamExt, 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::model_ext::AliasColumnExt; use magnetar_calckey_model::model_ext::AliasColumnExt;
use magnetar_calckey_model::note_model::data::{ use magnetar_calckey_model::note_model::data::{
sub_interaction_reaction, sub_interaction_renote, NoteData, sub_interaction_reaction, sub_interaction_renote, NoteData,
@ -20,14 +19,12 @@ use magnetar_calckey_model::sea_orm::sea_query::{PgFunc, Query, SimpleExpr};
use magnetar_calckey_model::sea_orm::{ActiveEnum, ColumnTrait, Iden, IntoSimpleExpr}; use magnetar_calckey_model::sea_orm::{ActiveEnum, ColumnTrait, Iden, IntoSimpleExpr};
use magnetar_calckey_model::user_model::UserResolveOptions; use magnetar_calckey_model::user_model::UserResolveOptions;
use magnetar_calckey_model::{ck, CalckeyDbError}; use magnetar_calckey_model::{ck, CalckeyDbError};
use magnetar_common::util::{parse_reaction, RawReaction};
use magnetar_sdk::mmm::Token; use magnetar_sdk::mmm::Token;
use magnetar_sdk::types::drive::PackDriveFileBase; use magnetar_sdk::types::drive::PackDriveFileBase;
use magnetar_sdk::types::emoji::EmojiContext; use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::note::{ use magnetar_sdk::types::note::{
NoteAttachmentExt, NoteBase, NoteDetailExt, NoteSelfContextExt, PackNoteBase, NoteAttachmentExt, NoteBase, NoteDetailExt, NoteSelfContextExt, PackNoteBase,
PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, Reaction, ReactionPair, PackNoteMaybeAttachments, PackNoteMaybeFull, PackPollBase, PollBase, ReactionPair,
ReactionShortcode, ReactionUnicode, ReactionUnknown,
}; };
use magnetar_sdk::types::user::UserRelationship; use magnetar_sdk::types::user::UserRelationship;
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
@ -299,95 +296,38 @@ impl NoteModel {
let shortcodes = emoji_model.deduplicate_emoji(ctx, emoji_extracted); let shortcodes = emoji_model.deduplicate_emoji(ctx, emoji_extracted);
// 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, reactions_counts): (Vec<_>, Vec<_>) =
serde_json::Map::<String, serde_json::Value>::deserialize(&note.reactions)? serde_json::Map::<String, serde_json::Value>::deserialize(&note.reactions)?
.into_iter() .into_iter()
.map(|(ref code, count)| { .filter_map(|(k, v)| Some((k, usize::deserialize(v).ok()?)))
let reaction = parse_reaction(code) .filter(|(_, count)| *count > 0)
.map_or_else(|| Either::Left(code.to_string()), Either::Right); .unzip();
let self_reaction_name = note_data
( .interaction_user_reaction()
reaction, .as_ref()
count, .and_then(|r| r.reaction_name.as_deref());
note_data let reactions_has_self_reacted = reactions_raw
.interaction_user_reaction()
.as_ref()
.and_then(|r| r.reaction_name.as_deref())
.map(|r| r == code),
)
})
.map(|(code, count, self_reacted)| {
Ok((code, usize::deserialize(count)?, self_reacted))
})
.filter(|v| !v.as_ref().is_ok_and(|(_, count, _)| *count == 0))
.collect::<Result<Vec<_>, serde_json::Error>>()?;
// Pick out all successfully-parsed shortcode emojis
let reactions_to_resolve = reactions_raw
.iter() .iter()
.map(|(code, _, _)| code) .map(|r| self_reaction_name.map(|srn| srn == r))
.map(Either::as_ref)
.filter_map(Either::right)
.filter_map(|c| match c {
RawReaction::Shortcode { shortcode, host } => Some(EmojiTag {
name: shortcode,
host: host.as_deref(),
}),
_ => None,
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let reaction_fetch = ctx
.service
.emoji_cache
.get_many_tagged(&reactions_to_resolve)
.map_err(PackError::from);
let emoji_fetch = emoji_model.fetch_many_emojis( let emoji_fetch = emoji_model.fetch_many_emojis(
ctx, ctx,
&shortcodes, &shortcodes,
note_data.user().user().host.as_deref(), note_data.user().user().host.as_deref(),
); );
let reaction_fetch = emoji_model.resolve_reactions_many(ctx, &reactions_raw);
let (reactions_fetched, emojis) = try_join!(reaction_fetch, emoji_fetch)?; let (reactions_fetched, emojis) = try_join!(reaction_fetch, emoji_fetch)?;
// Left reactions and the Right ones that didn't resolve to any emoji are turned back into Unknown let reactions = reactions_fetched
let reactions = &reactions_raw
.into_iter() .into_iter()
.map(|(raw, count, self_reaction)| { .zip(reactions_counts.into_iter())
let reaction = raw.either( .zip(reactions_has_self_reacted.into_iter())
|raw| Reaction::Unknown(ReactionUnknown { raw }), .map(|((reaction, count), self_reaction)| match self_reaction {
|raw| match raw { Some(self_reaction) => ReactionPair::WithContext(reaction, count, self_reaction),
RawReaction::Unicode(text) => Reaction::Unicode(ReactionUnicode(text)), None => ReactionPair::WithoutContext(reaction, count),
RawReaction::Shortcode { shortcode, host } => reactions_fetched
.iter()
.find(|e| e.host == host && e.name == shortcode)
.map_or_else(
|| {
Reaction::Unknown(ReactionUnknown {
raw: format!(
":{shortcode}{}:",
host.as_deref()
.map(|h| format!("@{h}"))
.unwrap_or_default()
),
})
},
|e| {
Reaction::Shortcode(ReactionShortcode {
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::<Vec<_>>(); .collect::<Vec<_>>();
@ -409,7 +349,7 @@ impl NoteModel {
.and_then(Result::ok) .and_then(Result::ok)
.map(MmXml) .map(MmXml)
.as_ref(), .as_ref(),
reactions, reactions: &reactions,
user, user,
emoji_context, emoji_context,
}, },
@ -506,7 +446,7 @@ impl NoteModel {
))) )))
} }
fn pack_full_single<'b, 'a: 'b>( pub fn pack_full_single<'b, 'a: 'b>(
&'a self, &'a self,
ctx: &'a PackingContext, ctx: &'a PackingContext,
note: &'b (dyn NoteShapedData<'a> + 'b), note: &'b (dyn NoteShapedData<'a> + 'b),

View File

@ -0,0 +1,283 @@
use crate::model::data::id::BaseId;
use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::note::{NoteModel, NoteVisibilityFilterModel};
use crate::model::processing::user::UserModel;
use crate::model::processing::{PackError, PackResult};
use crate::model::{PackType, PackingContext};
use crate::web::pagination::Pagination;
use futures_util::{StreamExt, TryStreamExt};
use magnetar_calckey_model::ck;
use magnetar_calckey_model::ck::sea_orm_active_enums::NotificationTypeEnum;
use magnetar_calckey_model::note_model::NoteResolveOptions;
use magnetar_calckey_model::notification_model::{NotificationData, NotificationResolveOptions};
use magnetar_calckey_model::user_model::UserResolveOptions;
use magnetar_sdk::types::notification::{
NotificationAppExt, NotificationBase, NotificationNoteExt, NotificationReactionExt,
NotificationType, NotificationUserExt, PackNotification, PackNotificationApp,
PackNotificationFollow, PackNotificationFollowRequestAccepted,
PackNotificationFollowRequestReceived, PackNotificationMention, PackNotificationPollEnd,
PackNotificationQuote, PackNotificationReaction, PackNotificationRenote, PackNotificationReply,
};
use magnetar_sdk::types::Id;
use magnetar_sdk::{Packed, Required};
use std::sync::Arc;
impl PackType<&NotificationType> for NotificationTypeEnum {
fn extract(_: &PackingContext, value: &NotificationType) -> Self {
use NotificationType as NT;
use NotificationTypeEnum as NTE;
match value {
NT::Follow => NTE::Follow,
NT::FollowRequestReceived => NTE::ReceiveFollowRequest,
NT::FollowRequestAccepted => NTE::FollowRequestAccepted,
NT::Mention => NTE::Mention,
NT::Reply => NTE::Reply,
NT::Renote => NTE::Renote,
NT::Reaction => NTE::Reaction,
NT::Quote => NTE::Quote,
NT::PollEnd => NTE::PollEnded,
NT::App => NTE::App,
}
}
}
impl PackType<&NotificationTypeEnum> for NotificationType {
fn extract(_: &PackingContext, value: &NotificationTypeEnum) -> Self {
use NotificationType as NT;
use NotificationTypeEnum as NTE;
match value {
NTE::Follow => NT::Follow,
NTE::ReceiveFollowRequest => NT::FollowRequestReceived,
NTE::FollowRequestAccepted => NT::FollowRequestAccepted,
NTE::Mention => NT::Mention,
NTE::Reply => NT::Reply,
NTE::Renote => NT::Renote,
NTE::Reaction => NT::Reaction,
NTE::Quote => NT::Quote,
NTE::PollEnded => NT::PollEnd,
NTE::App => NT::App,
}
}
}
pub struct NotificationModel;
impl NotificationModel {
async fn pack_notification_single(
&self,
ctx: &PackingContext,
notification_data: &NotificationData,
note_model: &NoteModel,
user_model: &UserModel,
emoji_model: &EmojiModel,
) -> PackResult<PackNotification> {
let notification_type =
NotificationType::extract(ctx, &notification_data.notification.r#type);
let id = Required(Id::from(&notification_data.notification.id));
let base = Required(NotificationBase::extract(
ctx,
&notification_data.notification,
));
let notifier = notification_data
.notifier
.as_ref()
.ok_or_else(|| PackError::DataError("Missing notification user".to_string()));
let note = notification_data
.notification_note
.as_ref()
.ok_or_else(|| PackError::DataError("Missing notification note".to_string()));
let access_token = notification_data
.access_token
.as_ref()
.ok_or_else(|| PackError::DataError("Missing notification access token".to_string()));
Ok(match notification_type {
NotificationType::Follow => {
PackNotification::Follow(PackNotificationFollow::pack_from((
id,
base,
Required(NotificationUserExt::extract(
ctx,
&user_model.base_from_existing(ctx, &notifier?).await?,
)),
)))
}
NotificationType::FollowRequestReceived => PackNotification::FollowRequestReceived(
PackNotificationFollowRequestReceived::pack_from((
id,
base,
Required(NotificationUserExt::extract(
ctx,
&user_model.base_from_existing(ctx, &notifier?).await?,
)),
)),
),
NotificationType::FollowRequestAccepted => PackNotification::FollowRequestAccepted(
PackNotificationFollowRequestAccepted::pack_from((
id,
base,
Required(NotificationUserExt::extract(
ctx,
&user_model.base_from_existing(ctx, &notifier?).await?,
)),
)),
),
NotificationType::Mention => {
PackNotification::Mention(PackNotificationMention::pack_from((
id,
base,
Required(NotificationNoteExt::extract(
ctx,
&note_model.pack_full_single(ctx, &note?).await?,
)),
)))
}
NotificationType::Reply => PackNotification::Reply(PackNotificationReply::pack_from((
id,
base,
Required(NotificationNoteExt::extract(
ctx,
&note_model.pack_full_single(ctx, &note?).await?,
)),
))),
NotificationType::Renote => {
PackNotification::Renote(PackNotificationRenote::pack_from((
id,
base,
Required(NotificationNoteExt::extract(
ctx,
&note_model.pack_full_single(ctx, &note?).await?,
)),
)))
}
NotificationType::Reaction => {
PackNotification::Reaction(PackNotificationReaction::pack_from((
id,
base,
Required(NotificationNoteExt::extract(
ctx,
&note_model.pack_full_single(ctx, &note?).await?,
)),
Required(NotificationReactionExt::extract(
ctx,
&emoji_model
.resolve_reaction(
ctx,
notification_data
.notification
.reaction
.as_deref()
.ok_or_else(|| {
PackError::DataError(
"Missing notification reaction".to_string(),
)
})?,
)
.await?,
)),
Required(NotificationUserExt::extract(
ctx,
&user_model.base_from_existing(ctx, &notifier?).await?,
)),
)))
}
NotificationType::Quote => PackNotification::Quote(PackNotificationQuote::pack_from((
id,
base,
Required(NotificationNoteExt::extract(
ctx,
&note_model.pack_full_single(ctx, &note?).await?,
)),
))),
NotificationType::PollEnd => {
PackNotification::PollEnd(PackNotificationPollEnd::pack_from((
id,
base,
Required(NotificationNoteExt::extract(
ctx,
&note_model.pack_full_single(ctx, &note?).await?,
)),
)))
}
NotificationType::App => PackNotification::App(PackNotificationApp::pack_from((
id,
base,
Required(NotificationAppExt::extract(
ctx,
(&notification_data.notification, access_token?),
)),
))),
})
}
pub async fn get_notifications(
&self,
ctx: &PackingContext,
id: &str,
notification_types: &[NotificationType],
unread_only: bool,
pagination: &mut Pagination,
) -> PackResult<Vec<PackNotification>> {
let user_resolve_options = UserResolveOptions {
with_avatar: true,
with_banner: false,
with_profile: false,
};
let self_id = ctx.self_user.as_deref().map(ck::user::Model::get_id);
let notifications_raw = ctx
.service
.db
.get_notification_resolver()
.get(
&NotificationResolveOptions {
note_options: NoteResolveOptions {
ids: None,
visibility_filter: Arc::new(
NoteVisibilityFilterModel.new_note_visibility_filter(Some(id)),
),
time_range: None,
limit: None,
with_reply_target: true,
with_renote_target: true,
with_interactions_from: self_id.map(str::to_string),
only_pins_from: None,
user_options: user_resolve_options.clone(),
},
user_options: user_resolve_options,
},
id,
&notification_types
.iter()
.map(|v| NotificationTypeEnum::extract(ctx, v))
.collect::<Vec<_>>(),
unread_only,
&pagination.current,
&mut pagination.prev,
&mut pagination.next,
pagination.limit.into(),
)
.await?;
let note_model = NoteModel {
with_context: true,
attachments: false,
};
let user_model = UserModel;
let emoji_model = EmojiModel;
let fut_iter = notifications_raw
.iter()
.map(|n| self.pack_notification_single(ctx, n, &note_model, &user_model, &emoji_model))
.collect::<Vec<_>>();
let processed = futures::stream::iter(fut_iter)
.buffered(10)
.err_into::<PackError>()
.try_collect::<Vec<_>>()
.await?;
Ok(processed)
}
}