diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index 5f8c73a2e..ee86344a5 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -20,7 +20,7 @@ export function convertAttachment(attachment: Entity.Attachment) {
export function convertFilter(filter: Entity.Filter) {
return simpleConvert(filter);
}
-export function convertList(list: Entity.List) {
+export function convertList(list: MastodonEntity.List) {
return simpleConvert(list);
}
export function convertFeaturedTag(tag: Entity.FeaturedTag) {
diff --git a/packages/backend/src/server/api/mastodon/endpoints/list.ts b/packages/backend/src/server/api/mastodon/endpoints/list.ts
new file mode 100644
index 000000000..52b83c18e
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/endpoints/list.ts
@@ -0,0 +1,181 @@
+import Router from "@koa/router";
+import { getClient } from "../index.js";
+import { ParsedUrlQuery } from "querystring";
+import {
+ convertAccount,
+ convertConversation,
+ convertList,
+ convertStatus,
+} from "../converters.js";
+import { convertId, IdType } from "../../index.js";
+import authenticate from "@/server/api/authenticate.js";
+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";
+import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js";
+import { ListHelpers } from "@/server/api/mastodon/helpers/list.js";
+import { UserConverter } from "@/server/api/mastodon/converters/user.js";
+import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
+
+export function setupEndpointsList(router: Router): void {
+ router.get("/v1/lists", async (ctx, reply) => {
+ try {
+ const auth = await authenticate(ctx.headers.authorization, null);
+ const user = auth[0] ?? undefined;
+
+ if (!user) {
+ ctx.status = 401;
+ return;
+ }
+
+ ctx.body = await ListHelpers.getLists(user)
+ .then(p => p.map(list => convertList(list)));
+ } catch (e: any) {
+ console.error(e);
+ console.error(e.response.data);
+ ctx.status = 401;
+ ctx.body = e.response.data;
+ }
+ });
+ router.get<{ Params: { id: string } }>(
+ "/v1/lists/:id",
+ async (ctx, reply) => {
+ try {
+ const auth = await authenticate(ctx.headers.authorization, null);
+ const user = auth[0] ?? undefined;
+
+ if (!user) {
+ ctx.status = 401;
+ return;
+ }
+
+ const id = convertId(ctx.params.id, IdType.IceshrimpId);
+
+ ctx.body = await ListHelpers.getList(user, id)
+ .then(p => convertList(p));
+ } catch (e: any) {
+ ctx.status = 404;
+ }
+ },
+ );
+ router.post("/v1/lists", async (ctx, reply) => {
+ const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
+ const accessTokens = ctx.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.createList((ctx.request.body as any).title);
+ ctx.body = convertList(data.data);
+ } catch (e: any) {
+ console.error(e);
+ console.error(e.response.data);
+ ctx.status = 401;
+ ctx.body = e.response.data;
+ }
+ });
+ router.put<{ Params: { id: string } }>(
+ "/v1/lists/:id",
+ async (ctx, reply) => {
+ const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
+ const accessTokens = ctx.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.updateList(
+ convertId(ctx.params.id, IdType.IceshrimpId),
+ (ctx.request.body as any).title,
+ );
+ ctx.body = convertList(data.data);
+ } catch (e: any) {
+ console.error(e);
+ console.error(e.response.data);
+ ctx.status = 401;
+ ctx.body = e.response.data;
+ }
+ },
+ );
+ router.delete<{ Params: { id: string } }>(
+ "/v1/lists/:id",
+ async (ctx, reply) => {
+ const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
+ const accessTokens = ctx.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.deleteList(
+ convertId(ctx.params.id, IdType.IceshrimpId),
+ );
+ ctx.body = data.data;
+ } catch (e: any) {
+ console.error(e);
+ console.error(e.response.data);
+ ctx.status = 401;
+ ctx.body = e.response.data;
+ }
+ },
+ );
+ router.get<{ Params: { id: string } }>(
+ "/v1/lists/:id/accounts",
+ async (ctx, reply) => {
+ try {
+ const auth = await authenticate(ctx.headers.authorization, null);
+ const user = auth[0] ?? undefined;
+
+ if (!user) {
+ ctx.status = 401;
+ return;
+ }
+
+ const id = convertId(ctx.params.id, IdType.IceshrimpId);
+ const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
+ const res = await ListHelpers.getListUsers(user, id, args.max_id, args.since_id, args.min_id, args.limit);
+ const accounts = await UserConverter.encodeMany(res.data);
+ ctx.body = accounts.map(account => convertAccount(account));
+ PaginationHelpers.appendLinkPaginationHeader(args, ctx, res);
+ } catch (e: any) {
+ ctx.status = 404;
+ }
+ },
+ );
+ router.post<{ Params: { id: string } }>(
+ "/v1/lists/:id/accounts",
+ async (ctx, reply) => {
+ const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
+ const accessTokens = ctx.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.addAccountsToList(
+ convertId(ctx.params.id, IdType.IceshrimpId),
+ (ctx.query.account_ids as string[]).map((id) =>
+ convertId(id, IdType.IceshrimpId),
+ ),
+ );
+ ctx.body = data.data;
+ } catch (e: any) {
+ console.error(e);
+ console.error(e.response.data);
+ ctx.status = 401;
+ ctx.body = e.response.data;
+ }
+ },
+ );
+ router.delete<{ Params: { id: string } }>(
+ "/v1/lists/:id/accounts",
+ async (ctx, reply) => {
+ const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
+ const accessTokens = ctx.headers.authorization;
+ const client = getClient(BASE_URL, accessTokens);
+ try {
+ const data = await client.deleteAccountsFromList(
+ convertId(ctx.params.id, IdType.IceshrimpId),
+ (ctx.query.account_ids as string[]).map((id) =>
+ convertId(id, IdType.IceshrimpId),
+ ),
+ );
+ ctx.body = data.data;
+ } catch (e: any) {
+ console.error(e);
+ console.error(e.response.data);
+ ctx.status = 401;
+ ctx.body = 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 d102e6534..a986bb5a1 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -171,173 +171,4 @@ export function setupEndpointsTimeline(router: Router): void {
ctx.body = e.response.data;
}
});
- router.get("/v1/lists", async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.getLists();
- ctx.body = data.data.map((list) => convertList(list));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- });
- router.get<{ Params: { id: string } }>(
- "/v1/lists/:id",
- async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.getList(
- convertId(ctx.params.id, IdType.IceshrimpId),
- );
- ctx.body = convertList(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- },
- );
- router.post("/v1/lists", async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.createList((ctx.request.body as any).title);
- ctx.body = convertList(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- });
- router.put<{ Params: { id: string } }>(
- "/v1/lists/:id",
- async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.updateList(
- convertId(ctx.params.id, IdType.IceshrimpId),
- (ctx.request.body as any).title,
- );
- ctx.body = convertList(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- },
- );
- router.delete<{ Params: { id: string } }>(
- "/v1/lists/:id",
- async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.deleteList(
- convertId(ctx.params.id, IdType.IceshrimpId),
- );
- ctx.body = data.data;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- },
- );
- router.get<{ Params: { id: string } }>(
- "/v1/lists/:id/accounts",
- async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.getAccountsInList(
- convertId(ctx.params.id, IdType.IceshrimpId),
- convertPaginationArgsIds(ctx.query as any),
- );
- ctx.body = data.data.map((account) => convertAccount(account));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- },
- );
- router.post<{ Params: { id: string } }>(
- "/v1/lists/:id/accounts",
- async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.addAccountsToList(
- convertId(ctx.params.id, IdType.IceshrimpId),
- (ctx.query.account_ids as string[]).map((id) =>
- convertId(id, IdType.IceshrimpId),
- ),
- );
- ctx.body = data.data;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- },
- );
- router.delete<{ Params: { id: string } }>(
- "/v1/lists/:id/accounts",
- async (ctx, reply) => {
- const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
- const accessTokens = ctx.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- try {
- const data = await client.deleteAccountsFromList(
- convertId(ctx.params.id, IdType.IceshrimpId),
- (ctx.query.account_ids as string[]).map((id) =>
- convertId(id, IdType.IceshrimpId),
- ),
- );
- ctx.body = data.data;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- ctx.status = 401;
- ctx.body = e.response.data;
- }
- },
- );
-}
-function escapeHTML(str: string) {
- if (!str) {
- return "";
- }
- return str
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-}
-function nl2br(str: string) {
- if (!str) {
- return "";
- }
- str = str.replace(/\r\n/g, "
");
- str = str.replace(/(\n|\r)/g, "
");
- return str;
}
diff --git a/packages/backend/src/server/api/mastodon/helpers/list.ts b/packages/backend/src/server/api/mastodon/helpers/list.ts
new file mode 100644
index 000000000..cb5bff39b
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/helpers/list.ts
@@ -0,0 +1,50 @@
+import { ILocalUser, User } from "@/models/entities/user.js";
+import { UserListJoinings, UserLists } from "@/models/index.js";
+import { LinkPaginationObject } from "@/server/api/mastodon/helpers/user.js";
+import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
+
+export class ListHelpers {
+ public static async getLists(user: ILocalUser): Promise {
+ return UserLists.findBy({userId: user.id}).then(p => p.map(list => {
+ return {
+ id: list.id,
+ title: list.name
+ }
+ }));
+ }
+
+ public static async getList(user: ILocalUser, id: string): Promise {
+ return UserLists.findOneByOrFail({userId: user.id, id: id}).then(list => {
+ return {
+ id: list.id,
+ title: list.name
+ }
+ });
+ }
+
+ public static async getListUsers(user: ILocalUser, id: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise> {
+ if (limit > 80) limit = 80;
+ const list = await UserLists.findOneByOrFail({userId: user.id, id: id});
+ const query = PaginationHelpers.makePaginationQuery(
+ UserListJoinings.createQueryBuilder('member'),
+ sinceId,
+ maxId,
+ minId
+ )
+ .andWhere("member.userListId = :listId", {listId: id})
+ .innerJoinAndSelect("member.user", "user");
+
+ return query.take(limit).getMany().then(async p => {
+ if (minId !== undefined) p = p.reverse();
+ const users = p
+ .map(p => p.user)
+ .filter(p => p) as User[];
+
+ return {
+ data: users,
+ maxId: p.map(p => p.id).at(-1),
+ minId: p.map(p => p.id)[0],
+ };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/mastodon/index.ts b/packages/backend/src/server/api/mastodon/index.ts
index 069fbf9e4..2929c4948 100644
--- a/packages/backend/src/server/api/mastodon/index.ts
+++ b/packages/backend/src/server/api/mastodon/index.ts
@@ -11,6 +11,7 @@ import { setupEndpointsMedia } from "@/server/api/mastodon/endpoints/media.js";
import { setupEndpointsMisc } from "@/server/api/mastodon/endpoints/misc.js";
import { koaBody } from "koa-body";
import multer from "@koa/multer";
+import { setupEndpointsList } from "@/server/api/mastodon/endpoints/list.js";
export function getClient(
BASE_URL: string,
@@ -50,5 +51,6 @@ export function setupMastodonApi(router: Router, fileRouter: Router, upload: mul
setupEndpointsNotifications(router);
setupEndpointsSearch(router);
setupEndpointsMedia(router, fileRouter, upload);
+ setupEndpointsList(router);
setupEndpointsMisc(router);
}