diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 53fdafb83..44712b97b 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -8,7 +8,7 @@ function simpleConvert(data: any) { return result; } -export function convertAccount(account: Entity.Account) { +export function convertAccount(account: Entity.Account | MastodonEntity.MutedAccount) { return simpleConvert(account); } export function convertAnnouncement(announcement: Entity.Announcement) { diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 1010315a9..505e4045c 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -344,6 +344,7 @@ export function apiAccountMastodon(router: Router): void { return; } + //FIXME: parse form data 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); @@ -456,14 +457,20 @@ export function apiAccountMastodon(router: Router): void { } }); router.get("/v1/mutes", 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.getMutes( - convertTimelinesArgsId(limitToInt(ctx.query as any)), - ); - ctx.body = data.data.map((account) => convertAccount(account)); + 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 as any))); + const res = await UserHelpers.getUserMutes(user, args.max_id, args.since_id, args.min_id, args.limit, cache); + ctx.body = res.data.map(m => convertAccount(m)); + PaginationHelpers.appendLinkPaginationHeader(args, ctx, res); } catch (e: any) { console.error(e); console.error(e.response.data); diff --git a/packages/backend/src/server/api/mastodon/entities/account.ts b/packages/backend/src/server/api/mastodon/entities/account.ts index 15fdc6a1f..737f8ee0d 100644 --- a/packages/backend/src/server/api/mastodon/entities/account.ts +++ b/packages/backend/src/server/api/mastodon/entities/account.ts @@ -24,4 +24,8 @@ namespace MastodonEntity { bot: boolean | null; source?: Source; }; + + export type MutedAccount = Account | { + mute_expires_at: string | null; + } } diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 63c2fbb21..4035e725a 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -3,19 +3,19 @@ import { ILocalUser, User } from "@/models/entities/user.js"; import { Blockings, Followings, - FollowRequests, Mutings, + FollowRequests, + Mutings, NoteFavorites, NoteReactions, - Notes, NoteWatchings, + Notes, + NoteWatchings, UserProfiles, Users } from "@/models/index.js"; -import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js"; import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js"; import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js"; import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; -import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js"; import Entity from "megalodon/src/entity.js"; import AsyncLock from "async-lock"; import { getUser } from "@/server/api/common/getters.js"; @@ -29,6 +29,8 @@ 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"; +import { UserConverter } from "@/server/api/mastodon/converters/user.js"; +import { convertId, IdType } from "@/misc/convert-id.js"; export type AccountCache = { locks: AsyncLock; @@ -118,6 +120,42 @@ export class UserHelpers { return this.getUserRelationshipTo(target.id, localUser.id); } + public static async getUserMutes(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise> { + if (limit > 80) limit = 80; + + const query = PaginationHelpers.makePaginationQuery( + Mutings.createQueryBuilder("muting"), + sinceId, + maxId, + minId + ); + + query.andWhere("muting.muterId = :userId", {userId: user.id}) + .innerJoinAndSelect("muting.mutee", "mutee"); + + return query.take(limit).getMany().then(async p => { + if (minId !== undefined) p = p.reverse(); + const users = p + .map(p => p.mutee) + .filter(p => p) as User[]; + + const result = await UserConverter.encodeMany(users, cache) + .then(res => res.map(m => { + const muting = p.find(acc => acc.muteeId === m.id); + return { + ...m, + mute_expires_at: muting?.expiresAt?.toISOString() ?? null + } as MastodonEntity.MutedAccount + })); + + return { + data: result, + maxId: p.map(p => p.id).at(-1), + minId: p.map(p => p.id)[0], + }; + }); + } + 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;