mirror of
https://iceshrimp.dev/crimekillz/trashposs
synced 2024-11-22 17:03:49 +01:00
[mastodon-client] GET /notifications
This commit is contained in:
parent
9d59ee09fd
commit
58dcbe68b7
@ -27,7 +27,7 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
|||||||
return simpleConvert(tag);
|
return simpleConvert(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertNotification(notification: Entity.Notification) {
|
export function convertNotification(notification: MastodonEntity.Notification) {
|
||||||
notification.account = convertAccount(notification.account);
|
notification.account = convertAccount(notification.account);
|
||||||
notification.id = convertId(notification.id, IdType.MastodonId);
|
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||||
if (notification.status)
|
if (notification.status)
|
||||||
|
@ -33,7 +33,7 @@ export class NoteConverter {
|
|||||||
.map((x) => decodeReaction(x).reaction)
|
.map((x) => decodeReaction(x).reaction)
|
||||||
.map((x) => x.replace(/:/g, ""));
|
.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),
|
note.emojis.concat(reactionEmojiNames),
|
||||||
host,
|
host,
|
||||||
));
|
));
|
||||||
@ -95,7 +95,7 @@ export class NoteConverter {
|
|||||||
in_reply_to_id: note.replyId,
|
in_reply_to_id: note.replyId,
|
||||||
in_reply_to_account_id: note.replyUserId,
|
in_reply_to_account_id: note.replyUserId,
|
||||||
reblog: Promise.resolve(renote).then(renote => renote && note.text === null ? this.encode(renote, user, cache) : null),
|
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,
|
text: text,
|
||||||
created_at: note.createdAt.toISOString(),
|
created_at: note.createdAt.toISOString(),
|
||||||
// Remove reaction emojis with names containing @ from the emojis list.
|
// 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 Router from "@koa/router";
|
||||||
import { koaBody } from "koa-body";
|
|
||||||
import { convertId, IdType } from "../../index.js";
|
import { convertId, IdType } from "../../index.js";
|
||||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||||
import { convertTimelinesArgsId } from "./timeline.js";
|
import { convertTimelinesArgsId, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||||
import { convertNotification } from "../converters.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) {
|
function toLimitToInt(q: any) {
|
||||||
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
||||||
return q;
|
return q;
|
||||||
@ -12,25 +15,23 @@ function toLimitToInt(q: any) {
|
|||||||
|
|
||||||
export function apiNotificationsMastodon(router: Router): void {
|
export function apiNotificationsMastodon(router: Router): void {
|
||||||
router.get("/v1/notifications", async (ctx) => {
|
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;
|
const body: any = ctx.request.body;
|
||||||
try {
|
try {
|
||||||
const data = await client.getNotifications(
|
const auth = await authenticate(ctx.headers.authorization, null);
|
||||||
convertTimelinesArgsId(toLimitToInt(ctx.query)),
|
const user = auth[0] ?? null;
|
||||||
);
|
|
||||||
const notfs = data.data;
|
if (!user) {
|
||||||
const ret = notfs.map((n) => {
|
ctx.status = 401;
|
||||||
n = convertNotification(n);
|
return;
|
||||||
if (n.type !== "follow" && n.type !== "follow_request") {
|
}
|
||||||
if (n.type === "reaction") n.type = "favourite";
|
|
||||||
return n;
|
const cache = UserHelpers.getFreshAccountCache();
|
||||||
} else {
|
const args = normalizeUrlQuery(convertTimelinesArgsId(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']);
|
||||||
return n;
|
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 = ret;
|
|
||||||
|
ctx.body = await data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
@ -61,11 +61,14 @@ export function convertTimelinesArgsId(q: ParsedUrlQuery) {
|
|||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeUrlQuery(q: ParsedUrlQuery): any {
|
export function normalizeUrlQuery(q: ParsedUrlQuery, arrayKeys: string[] = []): any {
|
||||||
const dict: any = {};
|
const dict: any = {};
|
||||||
|
|
||||||
for (const k in q) {
|
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;
|
return dict;
|
||||||
|
@ -11,5 +11,5 @@ namespace MastodonEntity {
|
|||||||
type: NotificationType;
|
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 New Issue
Block a user