From 44b72a2ecc2e374baf39613bb871205ad536da82 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 5 Oct 2023 00:44:33 +0200 Subject: [PATCH] [mastodon-client] GET /v1/announcements, POST /v1/announcements/:id/dismiss --- .../api/mastodon/converters/announcement.ts | 27 +++++++++ .../src/server/api/mastodon/endpoints/misc.ts | 60 ++++++++++++------- .../src/server/api/mastodon/helpers/misc.ts | 44 +++++++++++++- 3 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 packages/backend/src/server/api/mastodon/converters/announcement.ts diff --git a/packages/backend/src/server/api/mastodon/converters/announcement.ts b/packages/backend/src/server/api/mastodon/converters/announcement.ts new file mode 100644 index 000000000..9b5445cef --- /dev/null +++ b/packages/backend/src/server/api/mastodon/converters/announcement.ts @@ -0,0 +1,27 @@ +import { Announcement } from "@/models/entities/announcement.js"; +import { ILocalUser } from "@/models/entities/user.js"; +import { awaitAll } from "@/prelude/await-all"; +import { AnnouncementReads } from "@/models/index.js"; +import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js"; +import mfm from "mfm-js"; + +export class AnnouncementConverter { + public static encode(announcement: Announcement, isRead: boolean): MastodonEntity.Announcement { + return { + id: announcement.id, + content: `

${MfmHelpers.toHtml(mfm.parse(announcement.title), []) ?? 'Announcement'}

${MfmHelpers.toHtml(mfm.parse(announcement.text), []) ?? ''}`, + starts_at: null, + ends_at: null, + published: true, + all_day: false, + published_at: announcement.createdAt.toISOString(), + updated_at: announcement.updatedAt?.toISOString() ?? announcement.createdAt.toISOString(), + read: isRead, + mentions: [], //FIXME + statuses: [], + tags: [], + emojis: [], //FIXME + reactions: [], + }; + } +} \ No newline at end of file diff --git a/packages/backend/src/server/api/mastodon/endpoints/misc.ts b/packages/backend/src/server/api/mastodon/endpoints/misc.ts index 5e157c0cd..0f47aa7b9 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/misc.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/misc.ts @@ -1,8 +1,11 @@ import Router from "@koa/router"; import { getClient } from "@/server/api/mastodon/index.js"; -import { convertId, IdType } from "@/misc/convert-id.js"; -import { convertAnnouncementId } from "@/server/api/mastodon/converters.js"; import { MiscHelpers } from "@/server/api/mastodon/helpers/misc.js"; +import authenticate from "@/server/api/authenticate.js"; +import { argsToBools } from "@/server/api/mastodon/endpoints/timeline.js"; +import { Announcements } from "@/models/index.js"; +import { convertAnnouncementId } from "@/server/api/mastodon/converters.js"; +import { convertId, IdType } from "@/misc/convert-id.js"; export function setupEndpointsMisc(router: Router): void { router.get("/v1/custom_emojis", async (ctx) => { @@ -30,36 +33,49 @@ export function setupEndpointsMisc(router: Router): void { }); router.get("/v1/announcements", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getInstanceAnnouncements(); - ctx.body = data.data.map((announcement) => - convertAnnouncementId(announcement), - ); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const args = argsToBools(ctx.query, ['with_dismissed']); + ctx.body = await MiscHelpers.getAnnouncements(user, args['with_dismissed']) + .then(p => p.map(x => convertAnnouncementId(x))); } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; + ctx.status = 500; + ctx.body = { error: e.message }; } }); router.post<{ Params: { id: string } }>( "/v1/announcements/:id/dismiss", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.dismissInstanceAnnouncement( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - ctx.body = data.data; + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const id = convertId(ctx.params.id, IdType.IceshrimpId); + const announcement = await Announcements.findOneBy({id: id}); + + if (!announcement) { + ctx.status = 404; + return; + } + + await MiscHelpers.dismissAnnouncement(announcement, user); + ctx.body = {}; } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; + ctx.status = 500; + ctx.body = { error: e.message }; } }, ); diff --git a/packages/backend/src/server/api/mastodon/helpers/misc.ts b/packages/backend/src/server/api/mastodon/helpers/misc.ts index 51666b68c..acefa9d4f 100644 --- a/packages/backend/src/server/api/mastodon/helpers/misc.ts +++ b/packages/backend/src/server/api/mastodon/helpers/misc.ts @@ -1,11 +1,15 @@ import config from "@/config/index.js"; import { FILE_TYPE_BROWSERSAFE, MAX_NOTE_TEXT_LENGTH } from "@/const.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Instances, Notes, Users } from "@/models/index.js"; +import { AnnouncementReads, Announcements, Instances, Notes, Users } from "@/models/index.js"; import { IsNull } from "typeorm"; import { awaitAll } from "@/prelude/await-all.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import { convertAccountId } from "@/server/api/mastodon/converters.js"; +import { Announcement } from "@/models/entities/announcement.js"; +import { ILocalUser } from "@/models/entities/user.js"; +import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js"; +import { genId } from "@/misc/gen-id.js"; export class MiscHelpers { public static async getInstance(): Promise { @@ -81,4 +85,42 @@ export class MiscHelpers { return awaitAll(res); } + + public static async getAnnouncements(user: ILocalUser, includeRead: boolean = false): Promise { + if (includeRead) { + const [announcements, reads] = await Promise.all([ + Announcements.createQueryBuilder("announcement") + .orderBy({"announcement.id": "DESC"}) + .getMany(), + AnnouncementReads.findBy({userId: user.id}) + .then(p => p.map(x => x.announcementId)) + ]); + + return announcements.map(p => AnnouncementConverter.encode(p, reads.includes(p.id))); + } + + const sq = AnnouncementReads.createQueryBuilder("reads") + .select("reads.announcementId") + .where("reads.userId = :userId"); + + const query = Announcements.createQueryBuilder("announcement") + .where(`announcement.id NOT IN (${sq.getQuery()})`) + .orderBy({"announcement.id": "DESC"}) + .setParameter("userId", user.id); + + return query.getMany() + .then(p => p.map(x => AnnouncementConverter.encode(x, false))); + } + + public static async dismissAnnouncement(announcement: Announcement, user: ILocalUser): Promise { + const exists = await AnnouncementReads.exist({where: {userId: user.id, announcementId: announcement.id}}); + if (!exists) { + await AnnouncementReads.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + announcementId: announcement.id + }); + } + } } \ No newline at end of file