mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-22 01:47:39 -07:00
[mastodon-client] GET /notifications
This commit is contained in:
parent
9d59ee09fd
commit
58dcbe68b7
7 changed files with 158 additions and 26 deletions
|
@ -27,7 +27,7 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
|||
return simpleConvert(tag);
|
||||
}
|
||||
|
||||
export function convertNotification(notification: Entity.Notification) {
|
||||
export function convertNotification(notification: MastodonEntity.Notification) {
|
||||
notification.account = convertAccount(notification.account);
|
||||
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||
if (notification.status)
|
||||
|
|
|
@ -33,7 +33,7 @@ export class NoteConverter {
|
|||
.map((x) => decodeReaction(x).reaction)
|
||||
.map((x) => x.replace(/:/g, ""));
|
||||
|
||||
const noteEmoji = Promise.resolve(host).then(async host => populateEmojis(
|
||||
const noteEmoji = host.then(async host => populateEmojis(
|
||||
note.emojis.concat(reactionEmojiNames),
|
||||
host,
|
||||
));
|
||||
|
@ -95,7 +95,7 @@ export class NoteConverter {
|
|||
in_reply_to_id: note.replyId,
|
||||
in_reply_to_account_id: note.replyUserId,
|
||||
reblog: Promise.resolve(renote).then(renote => renote && note.text === null ? this.encode(renote, user, cache) : null),
|
||||
content: Promise.resolve(text).then(text => text !== null ? toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers)) ?? escapeMFM(text) : ""),
|
||||
content: text.then(text => text !== null ? toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers)) ?? escapeMFM(text) : ""),
|
||||
text: text,
|
||||
created_at: note.createdAt.toISOString(),
|
||||
// Remove reaction emojis with names containing @ from the emojis list.
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { ILocalUser, User } from "@/models/entities/user.js";
|
||||
import config from "@/config/index.js";
|
||||
import { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
||||
import { Notification } from "@/models/entities/notification.js";
|
||||
import { notificationTypes } from "@/types.js";
|
||||
import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
||||
import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
import { awaitAll } from "@/prelude/await-all.js";
|
||||
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
|
||||
type NotificationType = typeof notificationTypes[number];
|
||||
|
||||
export class NotificationConverter {
|
||||
public static async encode(notification: Notification, localUser: ILocalUser, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Notification | null> {
|
||||
if (notification.notifieeId !== localUser.id) return null;
|
||||
|
||||
//TODO: Test this (poll ended etc)
|
||||
const account = notification.notifierId
|
||||
? UserHelpers.getUserCached(notification.notifierId, cache).then(p => UserConverter.encode(p))
|
||||
: UserConverter.encode(localUser);
|
||||
|
||||
let result = {
|
||||
id: notification.id,
|
||||
account: account,
|
||||
created_at: notification.createdAt.toISOString(),
|
||||
type: this.encodeNotificationType(notification.type),
|
||||
};
|
||||
|
||||
if (notification.note) {
|
||||
const isPureRenote = notification.note.renoteId !== null && notification.note.text === null;
|
||||
const encodedNote = isPureRenote
|
||||
? getNote(notification.note.renoteId!, localUser).then(note => NoteConverter.encode(note, localUser, cache))
|
||||
: NoteConverter.encode(notification.note, localUser, cache);
|
||||
result = Object.assign(result, {
|
||||
status: encodedNote,
|
||||
});
|
||||
if (result.type === 'poll') {
|
||||
result = Object.assign(result, {
|
||||
account: encodedNote.then(p => p.account),
|
||||
});
|
||||
}
|
||||
if (notification.reaction) {
|
||||
//FIXME: Implement reactions;
|
||||
}
|
||||
}
|
||||
return awaitAll(result);
|
||||
}
|
||||
|
||||
public static async encodeMany(notifications: Notification[], localUser: ILocalUser, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Notification[]> {
|
||||
const encoded = notifications.map(u => this.encode(u, localUser, cache));
|
||||
return Promise.all(encoded)
|
||||
.then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]);
|
||||
}
|
||||
|
||||
private static encodeNotificationType(t: NotificationType): MastodonEntity.NotificationType {
|
||||
//FIXME: Implement custom notification for followRequestAccepted
|
||||
//FIXME: Implement mastodon notification type 'update' on misskey side
|
||||
switch (t) {
|
||||
case "follow":
|
||||
return 'follow';
|
||||
case "mention":
|
||||
case "reply":
|
||||
return 'mention'
|
||||
case "renote":
|
||||
return 'reblog';
|
||||
case "quote":
|
||||
return 'reblog';
|
||||
case "reaction":
|
||||
return 'favourite';
|
||||
case "pollEnded":
|
||||
return 'poll';
|
||||
case "receiveFollowRequest":
|
||||
return 'follow_request';
|
||||
case "followRequestAccepted":
|
||||
case "pollVote":
|
||||
case "groupInvited":
|
||||
case "app":
|
||||
throw new Error(`Notification type ${t} not supported`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { koaBody } from "koa-body";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { convertTimelinesArgsId } from "./timeline.js";
|
||||
import { convertTimelinesArgsId, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { convertNotification } from "../converters.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
import { NotificationHelpers } from "@/server/api/mastodon/helpers/notification.js";
|
||||
import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js";
|
||||
|
||||
function toLimitToInt(q: any) {
|
||||
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
||||
return q;
|
||||
|
@ -12,25 +15,23 @@ function toLimitToInt(q: any) {
|
|||
|
||||
export function apiNotificationsMastodon(router: Router): void {
|
||||
router.get("/v1/notifications", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getNotifications(
|
||||
convertTimelinesArgsId(toLimitToInt(ctx.query)),
|
||||
);
|
||||
const notfs = data.data;
|
||||
const ret = notfs.map((n) => {
|
||||
n = convertNotification(n);
|
||||
if (n.type !== "follow" && n.type !== "follow_request") {
|
||||
if (n.type === "reaction") n.type = "favourite";
|
||||
return n;
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
});
|
||||
ctx.body = ret;
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const args = normalizeUrlQuery(convertTimelinesArgsId(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']);
|
||||
const data = NotificationHelpers.getNotifications(user, args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id)
|
||||
.then(p => NotificationConverter.encodeMany(p, user, cache))
|
||||
.then(p => p.map(n => convertNotification(n)));
|
||||
|
||||
ctx.body = await data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
|
|
|
@ -61,11 +61,14 @@ export function convertTimelinesArgsId(q: ParsedUrlQuery) {
|
|||
return q;
|
||||
}
|
||||
|
||||
export function normalizeUrlQuery(q: ParsedUrlQuery): any {
|
||||
export function normalizeUrlQuery(q: ParsedUrlQuery, arrayKeys: string[] = []): any {
|
||||
const dict: any = {};
|
||||
|
||||
for (const k in q) {
|
||||
dict[k] = Array.isArray(q[k]) ? q[k]?.at(-1) : q[k];
|
||||
if (arrayKeys.includes(k))
|
||||
dict[k] = Array.isArray(q[k]) ? q[k] : [q[k]];
|
||||
else
|
||||
dict[k] = Array.isArray(q[k]) ? q[k]?.at(-1) : q[k];
|
||||
}
|
||||
|
||||
return dict;
|
||||
|
|
|
@ -11,5 +11,5 @@ namespace MastodonEntity {
|
|||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type NotificationType = string;
|
||||
export type NotificationType = 'follow' | 'favourite' | 'reblog' | 'mention' | 'reaction' | 'follow_request' | 'status' | 'poll';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { ILocalUser } from "@/models/entities/user.js";
|
||||
import { Notifications } from "@/models/index.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { Notification } from "@/models/entities/notification.js";
|
||||
export class NotificationHelpers {
|
||||
public static async getNotifications(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 15, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined): Promise<Notification[]> {
|
||||
if (limit > 30) limit = 30;
|
||||
if (types && excludeTypes) throw new Error("types and exclude_types can not be used simultaneously");
|
||||
|
||||
let requestedTypes = types
|
||||
? this.decodeTypes(types)
|
||||
: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest'];
|
||||
|
||||
if (excludeTypes) {
|
||||
const excludedTypes = this.decodeTypes(excludeTypes);
|
||||
requestedTypes = requestedTypes.filter(p => !excludedTypes.includes(p));
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notifications.createQueryBuilder("notification"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("notification.notifieeId = :userId", { userId: user.id })
|
||||
.andWhere("notification.type IN (:...types)", { types: requestedTypes });
|
||||
|
||||
if (accountId !== undefined)
|
||||
query.andWhere("notification.notifierId = :notifierId", { notifierId: accountId });
|
||||
|
||||
query.leftJoinAndSelect("notification.note", "note");
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
|
||||
private static decodeTypes(types: string[]) {
|
||||
const result: string[] = [];
|
||||
if (types.includes('follow')) result.push('follow');
|
||||
if (types.includes('mention')) result.push('mention', 'reply');
|
||||
if (types.includes('reblog')) result.push('renote', 'quote');
|
||||
if (types.includes('favourite')) result.push('reaction');
|
||||
if (types.includes('poll')) result.push('pollEnded');
|
||||
if (types.includes('follow_request')) result.push('receiveFollowRequest');
|
||||
return result;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue