From f667f2f985473282227a9ca8a500c1ffacfa7c28 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 28 Sep 2023 21:37:27 +0200 Subject: [PATCH] [mastodon-client] POST /accounts/:id/mute, POST /accounts/:id/unmute; Fix timeline helper function --- .../server/api/mastodon/endpoints/account.ts | 46 +++++++++++-------- .../server/api/mastodon/endpoints/timeline.ts | 29 +++++------- .../src/server/api/mastodon/helpers/user.ts | 43 ++++++++++++++++- 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 6f64f5e3c..1010315a9 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -294,8 +294,8 @@ export function apiAccountMastodon(router: Router): void { const user = auth[0] ?? null; if (!user) { - ctx.status = 401; - return; + ctx.status = 401; + return; } const target = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId)); @@ -317,8 +317,8 @@ export function apiAccountMastodon(router: Router): void { const user = auth[0] ?? null; if (!user) { - ctx.status = 401; - return; + ctx.status = 401; + return; } const target = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId)); @@ -335,15 +335,19 @@ export function apiAccountMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/accounts/:id/mute", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.muteAccount( - convertId(ctx.params.id, IdType.IceshrimpId), - (ctx.request as any).body as any, - ); - ctx.body = convertRelationship(data.data); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query, ['duration']), ['notifications'])); + const target = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId)); + const result = await UserHelpers.muteUser(target, user, args.notifications, args.duration); + ctx.body = convertRelationship(result) } catch (e: any) { console.error(e); console.error(e.response.data); @@ -355,14 +359,18 @@ export function apiAccountMastodon(router: Router): void { router.post<{ Params: { id: string } }>( "/v1/accounts/:id/unmute", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unmuteAccount( - convertId(ctx.params.id, IdType.IceshrimpId), - ); - ctx.body = convertRelationship(data.data); + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? null; + + if (!user) { + ctx.status = 401; + return; + } + + const target = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId)); + const result = await UserHelpers.unmuteUser(target, user); + ctx.body = convertRelationship(result) } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 29096b428..f31dd22cb 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -13,16 +13,18 @@ import { TimelineHelpers } from "@/server/api/mastodon/helpers/timeline.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; -export function limitToInt(q: ParsedUrlQuery) { +export function limitToInt(q: ParsedUrlQuery, additional: string[] = []) { let object: any = q; if (q.limit) if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10); if (q.offset) if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10); + for (const key of additional) + if (typeof q[key] === "string") object[key] = parseInt(q[key], 10); return object; } -export function argsToBools(q: ParsedUrlQuery) { +export function argsToBools(q: ParsedUrlQuery, additional: string[] = []) { // Values taken from https://docs.joinmastodon.org/client/intro/#boolean const toBoolean = (value: string) => !["0", "f", "F", "false", "FALSE", "off", "OFF"].includes(value); @@ -31,23 +33,14 @@ export function argsToBools(q: ParsedUrlQuery) { // - https://docs.joinmastodon.org/methods/accounts/#statuses // - https://docs.joinmastodon.org/methods/timelines/#public // - https://docs.joinmastodon.org/methods/timelines/#tag + let keys = ['only_media', 'exclude_replies', 'exclude_reblogs', 'pinned', 'local', 'remote'] let object: any = q; - if (q.only_media) - if (typeof q.only_media === "string") - object.only_media = toBoolean(q.only_media); - if (q.exclude_replies) - if (typeof q.exclude_replies === "string") - object.exclude_replies = toBoolean(q.exclude_replies); - if (q.exclude_reblogs) - if (typeof q.exclude_reblogs === "string") - object.exclude_reblogs = toBoolean(q.exclude_reblogs); - if (q.pinned) - if (typeof q.pinned === "string") object.pinned = toBoolean(q.pinned); - if (q.local) - if (typeof q.local === "string") object.local = toBoolean(q.local); - if (q.remote) - if (typeof q.local === "string") object.local = toBoolean(q.local); - return q; + + for (const key of keys) + if (q[key] && typeof q[key] === "string") + object[key] = toBoolean(q[key]); + + return object; } export function convertTimelinesArgsId(q: ParsedUrlQuery) { diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 688c3d8c4..63c2fbb21 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -3,10 +3,10 @@ import { ILocalUser, User } from "@/models/entities/user.js"; import { Blockings, Followings, - FollowRequests, + FollowRequests, Mutings, NoteFavorites, NoteReactions, - Notes, + Notes, NoteWatchings, UserProfiles, Users } from "@/models/index.js"; @@ -26,6 +26,9 @@ import deleteFollowing from "@/services/following/delete.js"; import cancelFollowRequest from "@/services/following/requests/cancel.js"; import createBlocking from "@/services/blocking/create.js"; import deleteBlocking from "@/services/blocking/delete.js"; +import { genId } from "@/misc/gen-id.js"; +import { Muting } from "@/models/entities/muting.js"; +import { publishUserEvent } from "@/services/stream.js"; export type AccountCache = { locks: AsyncLock; @@ -79,6 +82,42 @@ export class UserHelpers { return this.getUserRelationshipTo(target.id, localUser.id); } + public static async muteUser(target: User, localUser: ILocalUser, notifications: boolean = true, duration: number = 0) { + //FIXME: respect notifications parameter + const muted = await Mutings.exist({where: {muterId: localUser.id, muteeId: target.id}}); + if (!muted) { + await Mutings.insert({ + id: genId(), + createdAt: new Date(), + expiresAt: duration === 0 ? null : new Date(new Date().getTime() + (duration * 1000)), + muterId: localUser.id, + muteeId: target.id, + } as Muting); + + publishUserEvent(localUser.id, "mute", target); + + NoteWatchings.delete({ + userId: localUser.id, + noteUserId: target.id, + }); + } + + return this.getUserRelationshipTo(target.id, localUser.id); + } + + public static async unmuteUser(target: User, localUser: ILocalUser) { + const muting = await Mutings.findOneBy({muterId: localUser.id, muteeId: target.id}); + if (muting) { + await Mutings.delete({ + id: muting.id, + }); + + publishUserEvent(localUser.id, "unmute", target); + } + + return this.getUserRelationshipTo(target.id, localUser.id); + } + public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise { if (limit > 40) limit = 40;