mirror of
https://iceshrimp.dev/iceshrimp/iceshrimp
synced 2024-11-27 22:38:10 +09:00
[mastodon-client] Migrate endpoints to auth middleware
This commit is contained in:
parent
e3186e98f8
commit
4b76d0ce6f
@ -21,6 +21,7 @@ export class AuthenticationError extends Error {
|
||||
export default async (
|
||||
authorization: string | null | undefined,
|
||||
bodyToken: string | null,
|
||||
bypassUserCache: boolean = false
|
||||
): Promise<
|
||||
[CacheableLocalUser | null | undefined, AccessToken | null | undefined]
|
||||
> => {
|
||||
@ -46,11 +47,13 @@ export default async (
|
||||
}
|
||||
|
||||
if (isNativeToken(token)) {
|
||||
const user = await localUserByNativeTokenCache.fetch(
|
||||
token,
|
||||
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>,
|
||||
true,
|
||||
);
|
||||
const user = bypassUserCache
|
||||
? await Users.findOneBy({ token }) as ILocalUser | null
|
||||
: await localUserByNativeTokenCache.fetch(
|
||||
token,
|
||||
() => Users.findOneBy({ token: token ?? undefined }) as Promise<ILocalUser | null>,
|
||||
true,
|
||||
);
|
||||
|
||||
if (user == null) {
|
||||
throw new AuthenticationError("unknown token");
|
||||
@ -77,14 +80,18 @@ export default async (
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
const user = await localUserByIdCache.fetch(
|
||||
accessToken.userId,
|
||||
() =>
|
||||
Users.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<ILocalUser>,
|
||||
true,
|
||||
);
|
||||
const user = bypassUserCache
|
||||
? await Users.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as ILocalUser
|
||||
: await localUserByIdCache.fetch(
|
||||
accessToken.userId,
|
||||
() =>
|
||||
Users.findOneBy({
|
||||
id: accessToken.userId,
|
||||
}) as Promise<ILocalUser>,
|
||||
true,
|
||||
);
|
||||
|
||||
if (accessToken.appId) {
|
||||
const app = await appCache.fetch(
|
||||
|
@ -1,7 +1,4 @@
|
||||
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";
|
||||
|
||||
|
149
packages/backend/src/server/api/mastodon/converters/auth.ts
Normal file
149
packages/backend/src/server/api/mastodon/converters/auth.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { unique } from "@/prelude/array.js";
|
||||
|
||||
export class AuthConverter {
|
||||
private static readScopes = [
|
||||
"read:account",
|
||||
"read:drive",
|
||||
"read:blocks",
|
||||
"read:favorites",
|
||||
"read:following",
|
||||
"read:messaging",
|
||||
"read:mutes",
|
||||
"read:notifications",
|
||||
"read:reactions",
|
||||
];
|
||||
|
||||
private static writeScopes = [
|
||||
"write:account",
|
||||
"write:drive",
|
||||
"write:blocks",
|
||||
"write:favorites",
|
||||
"write:following",
|
||||
"write:messaging",
|
||||
"write:mutes",
|
||||
"write:notes",
|
||||
"write:notifications",
|
||||
"write:reactions",
|
||||
"write:votes",
|
||||
];
|
||||
|
||||
private static followScopes = [
|
||||
"read:following",
|
||||
"read:blocks",
|
||||
"read:mutes",
|
||||
"write:following",
|
||||
"write:blocks",
|
||||
"write:mutes",
|
||||
];
|
||||
|
||||
public static decode(scopes: string[]): string[] {
|
||||
const res: string[] = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
if (scope === "read")
|
||||
res.push(...this.readScopes);
|
||||
else if (scope === "write")
|
||||
res.push(...this.writeScopes);
|
||||
else if (scope === "follow")
|
||||
res.push(...this.followScopes);
|
||||
else if (scope === "read:accounts")
|
||||
res.push("read:account");
|
||||
else if (scope === "read:blocks")
|
||||
res.push("read:blocks");
|
||||
else if (scope === "read:bookmarks")
|
||||
res.push("read:favorites");
|
||||
else if (scope === "read:favourites")
|
||||
res.push("read:reactions");
|
||||
else if (scope === "read:filters")
|
||||
res.push("read:account")
|
||||
else if (scope === "read:follows")
|
||||
res.push("read:following");
|
||||
else if (scope === "read:lists")
|
||||
res.push("read:account");
|
||||
else if (scope === "read:mutes")
|
||||
res.push("read:mutes");
|
||||
else if (scope === "read:notifications")
|
||||
res.push("read:notifications");
|
||||
else if (scope === "read:search")
|
||||
res.push("read:account"); // FIXME: move this to a new scope "read:search"
|
||||
else if (scope === "read:statuses")
|
||||
res.push("read:messaging");
|
||||
else if (scope === "write:accounts")
|
||||
res.push("write:account");
|
||||
else if (scope === "write:blocks")
|
||||
res.push("write:blocks");
|
||||
else if (scope === "write:bookmarks")
|
||||
res.push("write:favorites");
|
||||
else if (scope === "write:favourites")
|
||||
res.push("write:reactions");
|
||||
else if (scope === "write:filters")
|
||||
res.push("write:account");
|
||||
else if (scope === "write:follows")
|
||||
res.push("write:following");
|
||||
else if (scope === "write:lists")
|
||||
res.push("write:account");
|
||||
else if (scope === "write:media")
|
||||
res.push("write:drive");
|
||||
else if (scope === "write:mutes")
|
||||
res.push("write:mutes");
|
||||
else if (scope === "write:notifications")
|
||||
res.push("write:notifications");
|
||||
else if (scope === "write:reports")
|
||||
res.push("read:account"); // FIXME: move this to a new scope "write:reports"
|
||||
else if (scope === "write:statuses")
|
||||
res.push(...["write:notes", "write:messaging", "write:votes"]);
|
||||
else if (scope === "write:conversations")
|
||||
res.push("write:messaging");
|
||||
// ignored: "push"
|
||||
}
|
||||
|
||||
return unique(res);
|
||||
}
|
||||
|
||||
public static encode(scopes: string[]): string[] {
|
||||
const res: string[] = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
if (scope === "read:account")
|
||||
res.push(...["read:accounts", "read:filters", "read:search", "read:lists"]);
|
||||
else if (scope === "read:blocks")
|
||||
res.push("read:blocks");
|
||||
else if (scope === "read:favorites")
|
||||
res.push("read:bookmarks");
|
||||
else if (scope === "read:reactions")
|
||||
res.push("read:favourites");
|
||||
else if (scope === "read:following")
|
||||
res.push("read:follows");
|
||||
else if (scope === "read:mutes")
|
||||
res.push("read:mutes");
|
||||
else if (scope === "read:notifications")
|
||||
res.push("read:notifications");
|
||||
else if (scope === "read:messaging")
|
||||
res.push("read:statuses");
|
||||
else if (scope === "write:account")
|
||||
res.push(...["write:accounts", "write:lists", "write:filters", "write:reports"]);
|
||||
else if (scope === "write:blocks")
|
||||
res.push("write:blocks");
|
||||
else if (scope === "write:favorites")
|
||||
res.push("write:bookmarks");
|
||||
else if (scope === "write:reactions")
|
||||
res.push("write:favourites");
|
||||
else if (scope === "write:following")
|
||||
res.push("write:follows");
|
||||
else if (scope === "write:drive")
|
||||
res.push("write:media");
|
||||
else if (scope === "write:mutes")
|
||||
res.push("write:mutes");
|
||||
else if (scope === "write:notifications")
|
||||
res.push("write:notifications");
|
||||
else if (scope === "write:notes")
|
||||
res.push("write:statuses");
|
||||
else if (scope === "write:messaging")
|
||||
res.push("write:conversations");
|
||||
else if (scope === "write:votes")
|
||||
res.push("write:statuses");
|
||||
}
|
||||
|
||||
return unique(res);
|
||||
}
|
||||
}
|
@ -2,527 +2,243 @@ import Router from "@koa/router";
|
||||
import { argsToBools, convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import { convertAccountId, convertListId, convertRelationshipId, convertStatusIds, } from "../converters.js";
|
||||
import { getUser } from "@/server/api/common/getters.js";
|
||||
import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
|
||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { ListHelpers } from "@/server/api/mastodon/helpers/list.js";
|
||||
import { Files } from "formidable";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
|
||||
export function setupEndpointsAccount(router: Router): void {
|
||||
router.get("/v1/accounts/verify_credentials", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const acct = await UserHelpers.verifyCredentials(user);
|
||||
router.get("/v1/accounts/verify_credentials",
|
||||
auth(true, ['read:accounts']),
|
||||
async (ctx) => {
|
||||
const acct = await UserHelpers.verifyCredentials(ctx.user);
|
||||
ctx.body = convertAccountId(acct);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.patch("/v1/accounts/update_credentials", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
);
|
||||
router.patch("/v1/accounts/update_credentials",
|
||||
auth(true, ['write:accounts']),
|
||||
async (ctx) => {
|
||||
const files = (ctx.request as any).files as Files | undefined;
|
||||
const acct = await UserHelpers.updateCredentials(user, (ctx.request as any).body as any, files);
|
||||
const acct = await UserHelpers.updateCredentials(ctx.user, (ctx.request as any).body as any, files);
|
||||
ctx.body = convertAccountId(acct)
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/accounts/lookup", async (ctx) => {
|
||||
try {
|
||||
);
|
||||
router.get("/v1/accounts/lookup",
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(ctx.query);
|
||||
const user = await UserHelpers.getUserFromAcct(args.acct);
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
const account = await UserConverter.encode(user);
|
||||
ctx.body = convertAccountId(account);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/accounts/relationships", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
);
|
||||
router.get("/v1/accounts/relationships",
|
||||
auth(true, ['read:follows']),
|
||||
async (ctx) => {
|
||||
const ids = (normalizeUrlQuery(ctx.query, ['id[]'])['id[]'] ?? [])
|
||||
.map((id: string) => convertId(id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.getUserRelationhipToMany(ids, user.id);
|
||||
const result = await UserHelpers.getUserRelationhipToMany(ids, ctx.user.id);
|
||||
ctx.body = result.map(rel => convertRelationshipId(rel));
|
||||
} 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/accounts/:id", async (ctx) => {
|
||||
try {
|
||||
);
|
||||
router.get<{ Params: { id: string } }>("/v1/accounts/:id",
|
||||
auth(false),
|
||||
async (ctx) => {
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const account = await UserConverter.encode(await getUser(userId));
|
||||
const account = await UserConverter.encode(await UserHelpers.getUserOr404(userId));
|
||||
ctx.body = convertAccountId(account);
|
||||
} 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/accounts/:id/statuses",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const query = await UserHelpers.getUserCachedOr404(userId, ctx.cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
|
||||
const tl = await UserHelpers.getUserStatuses(query, ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args['only_media'], args['exclude_replies'], args['exclude_reblogs'], args.pinned, args.tagged)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, ctx.cache));
|
||||
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const query = await UserHelpers.getUserCached(userId, cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
|
||||
const tl = await UserHelpers.getUserStatuses(query, user, args.max_id, args.since_id, args.min_id, args.limit, args['only_media'], args['exclude_replies'], args['exclude_reblogs'], args.pinned, args.tagged)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/featured_tags",
|
||||
async (ctx) => {
|
||||
try {
|
||||
ctx.body = [];
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
ctx.body = [];
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/followers",
|
||||
auth(false),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const query = await UserHelpers.getUserCachedOr404(userId, ctx.cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserFollowers(query, ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const followers = await UserConverter.encodeMany(res.data, ctx.cache);
|
||||
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const query = await UserHelpers.getUserCached(userId, cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
|
||||
const res = await UserHelpers.getUserFollowers(query, user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const followers = await UserConverter.encodeMany(res.data, cache);
|
||||
|
||||
ctx.body = followers.map((account) => convertAccountId(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = followers.map((account) => convertAccountId(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/following",
|
||||
auth(false),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const query = await UserHelpers.getUserCachedOr404(userId, ctx.cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserFollowing(query, ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const following = await UserConverter.encodeMany(res.data, ctx.cache);
|
||||
|
||||
const userId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const query = await UserHelpers.getUserCached(userId, cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
|
||||
const res = await UserHelpers.getUserFollowing(query, user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const following = await UserConverter.encodeMany(res.data, cache);
|
||||
|
||||
ctx.body = following.map((account) => convertAccountId(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = following.map((account) => convertAccountId(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/lists",
|
||||
auth(true, ["read:lists"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await UserHelpers.getUserCached(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const results = await ListHelpers.getListsByMember(user, member);
|
||||
ctx.body = results.map(p => convertListId(p));
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
const member = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const results = await ListHelpers.getListsByMember(ctx.user, member);
|
||||
ctx.body = results.map(p => convertListId(p));
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/follow",
|
||||
auth(true, ["write:follows"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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));
|
||||
//FIXME: Parse form data
|
||||
const result = await UserHelpers.followUser(target, user, true, false);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
//FIXME: Parse form data
|
||||
const result = await UserHelpers.followUser(target, ctx.user, true, false);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/unfollow",
|
||||
auth(true, ["write:follows"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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.unfollowUser(target, user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.unfollowUser(target, ctx.user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/block",
|
||||
auth(true, ["write:blocks"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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.blockUser(target, user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.blockUser(target, ctx.user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/unblock",
|
||||
auth(true, ["write:blocks"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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.unblockUser(target, user);
|
||||
ctx.body = convertRelationshipId(result)
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.unblockUser(target, ctx.user);
|
||||
ctx.body = convertRelationshipId(result)
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/mute",
|
||||
auth(true, ["write:mutes"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
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);
|
||||
ctx.body = convertRelationshipId(result)
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
//FIXME: parse form data
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query, ['duration']), ['notifications']));
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.muteUser(target, ctx.user, args.notifications, args.duration);
|
||||
ctx.body = convertRelationshipId(result)
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/unmute",
|
||||
auth(true, ["write:mutes"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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 = convertRelationshipId(result)
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.unmuteUser(target, ctx.user);
|
||||
ctx.body = convertRelationshipId(result)
|
||||
},
|
||||
);
|
||||
router.get("/v1/featured_tags", async (ctx) => {
|
||||
try {
|
||||
router.get("/v1/featured_tags",
|
||||
async (ctx) => {
|
||||
ctx.body = [];
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
router.get("/v1/followed_tags", async (ctx) => {
|
||||
try {
|
||||
);
|
||||
router.get("/v1/followed_tags",
|
||||
async (ctx) => {
|
||||
ctx.body = [];
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
router.get("/v1/bookmarks", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
);
|
||||
router.get("/v1/bookmarks",
|
||||
auth(true, ["read:bookmarks"]),
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserBookmarks(user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const bookmarks = await NoteConverter.encodeMany(res.data, user, cache);
|
||||
|
||||
const res = await UserHelpers.getUserBookmarks(ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const bookmarks = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache);
|
||||
ctx.body = bookmarks.map(s => convertStatusIds(s));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/favourites", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
);
|
||||
router.get("/v1/favourites",
|
||||
auth(true, ["read:favourites"]),
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserFavorites(user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const favorites = await NoteConverter.encodeMany(res.data, user, cache);
|
||||
|
||||
const res = await UserHelpers.getUserFavorites(ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const favorites = await NoteConverter.encodeMany(res.data, ctx.user, ctx.cache);
|
||||
ctx.body = favorites.map(s => convertStatusIds(s));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/mutes", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
);
|
||||
router.get("/v1/mutes",
|
||||
auth(true, ["read:mutes"]),
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserMutes(user, args.max_id, args.since_id, args.min_id, args.limit, cache);
|
||||
const res = await UserHelpers.getUserMutes(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, ctx.cache);
|
||||
ctx.body = res.data.map(m => convertAccountId(m));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/blocks", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
);
|
||||
router.get("/v1/blocks",
|
||||
auth(true, ["read:blocks"]),
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserBlocks(user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const blocks = await UserConverter.encodeMany(res.data, cache);
|
||||
const res = await UserHelpers.getUserBlocks(ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const blocks = await UserConverter.encodeMany(res.data, ctx.cache);
|
||||
ctx.body = blocks.map(b => convertAccountId(b));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/follow_requests", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
);
|
||||
router.get("/v1/follow_requests",
|
||||
auth(true, ["read:follows"]),
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await UserHelpers.getUserFollowRequests(user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const requests = await UserConverter.encodeMany(res.data, cache);
|
||||
const res = await UserHelpers.getUserFollowRequests(ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const requests = await UserConverter.encodeMany(res.data, ctx.cache);
|
||||
ctx.body = requests.map(b => convertAccountId(b));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} 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/follow_requests/:id/authorize",
|
||||
auth(true, ["write:follows"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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.acceptFollowRequest(target, user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.acceptFollowRequest(target, ctx.user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/follow_requests/:id/reject",
|
||||
auth(true, ["write:follows"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
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.rejectFollowRequest(target, user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId));
|
||||
const result = await UserHelpers.rejectFollowRequest(target, ctx.user);
|
||||
ctx.body = convertRelationshipId(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,72 +1,68 @@
|
||||
import Router from "@koa/router";
|
||||
import { AuthHelpers } from "@/server/api/mastodon/helpers/auth.js";
|
||||
import { convertId, IdType } from "@/misc/convert-id.js";
|
||||
|
||||
const readScope = [
|
||||
"read:account",
|
||||
"read:drive",
|
||||
"read:blocks",
|
||||
"read:favorites",
|
||||
"read:following",
|
||||
"read:messaging",
|
||||
"read:mutes",
|
||||
"read:notifications",
|
||||
"read:reactions",
|
||||
"read:pages",
|
||||
"read:page-likes",
|
||||
"read:user-groups",
|
||||
"read:channels",
|
||||
"read:gallery",
|
||||
"read:gallery-likes",
|
||||
];
|
||||
const writeScope = [
|
||||
"write:account",
|
||||
"write:drive",
|
||||
"write:blocks",
|
||||
"write:favorites",
|
||||
"write:following",
|
||||
"write:messaging",
|
||||
"write:mutes",
|
||||
"write:notes",
|
||||
"write:notifications",
|
||||
"write:reactions",
|
||||
"write:votes",
|
||||
"write:pages",
|
||||
"write:page-likes",
|
||||
"write:user-groups",
|
||||
"write:channels",
|
||||
"write:gallery",
|
||||
"write:gallery-likes",
|
||||
];
|
||||
import { AuthConverter } from "@/server/api/mastodon/converters/auth.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
export function setupEndpointsAuth(router: Router): void {
|
||||
router.post("/v1/apps", async (ctx) => {
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
try {
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === "string") scope = scope.split(" ");
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
|
||||
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
|
||||
}
|
||||
const scopeArr = Array.from(pushScope);
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === "string") scope = scope.split(" ");
|
||||
const scopeArr = AuthConverter.decode(scope);
|
||||
const red = body.redirect_uris;
|
||||
const appData = await AuthHelpers.registerApp(body['client_name'], scopeArr, red, body['website']);
|
||||
ctx.body = {
|
||||
id: convertId(appData.id, IdType.MastodonId),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url ?? "").toString("base64"),
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const red = body.redirect_uris;
|
||||
const appData = await AuthHelpers.registerApp(body['client_name'], scopeArr, red, body['website']);
|
||||
const returns = {
|
||||
id: convertId(appData.id, IdType.MastodonId),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url ?? "").toString("base64"),
|
||||
client_secret: appData.clientSecret,
|
||||
export function setupEndpointsAuthRoot(router: Router): void {
|
||||
router.get("/oauth/authorize", async (ctx) => {
|
||||
const { client_id, state, redirect_uri } = ctx.request.query;
|
||||
let param = "mastodon=true";
|
||||
if (state) param += `&state=${state}`;
|
||||
if (redirect_uri) param += `&redirect_uri=${redirect_uri}`;
|
||||
const client = client_id ? client_id : "";
|
||||
ctx.redirect(
|
||||
`${Buffer.from(client.toString(), "base64").toString()}?${param}`,
|
||||
);
|
||||
});
|
||||
|
||||
router.post("/oauth/token", async (ctx) => {
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
if (body.grant_type === "client_credentials") {
|
||||
ctx.body = {
|
||||
access_token: uuid(),
|
||||
token_type: "Bearer",
|
||||
scope: "read",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
ctx.body = returns;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
let token = null;
|
||||
if (body.code) {
|
||||
token = body.code;
|
||||
}
|
||||
try {
|
||||
const accessToken = await AuthHelpers.getAuthToken(body.client_secret, token ? token : "");
|
||||
const ret = {
|
||||
access_token: accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: body.scope || "read write follow push",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
ctx.body = ret;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
ctx.body = err.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
import Router from "@koa/router";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
|
||||
export function setupEndpointsFilter(router: Router): void {
|
||||
router.get(["/v1/filters", "/v2/filters"], async (ctx) => {
|
||||
ctx.body = [];
|
||||
});
|
||||
router.post(["/v1/filters", "/v2/filters"], async (ctx) => {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: "Please change word mute settings in the web frontend settings." };
|
||||
});
|
||||
router.get(["/v1/filters", "/v2/filters"],
|
||||
auth(true, ['read:filters']),
|
||||
async (ctx) => {
|
||||
ctx.body = [];
|
||||
}
|
||||
);
|
||||
router.post(["/v1/filters", "/v2/filters"],
|
||||
auth(true, ['write:filters']),
|
||||
async (ctx) => {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: "Please change word mute settings in the web frontend settings." };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,243 +1,115 @@
|
||||
import Router from "@koa/router";
|
||||
import { convertAccountId, convertListId, } from "../converters.js";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import authenticate from "@/server/api/authenticate.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";
|
||||
import { UserLists } from "@/models/index.js";
|
||||
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
|
||||
import { getUser } from "@/server/api/common/getters.js";
|
||||
import { toArray } from "@/prelude/array.js";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.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)
|
||||
router.get("/v1/lists",
|
||||
auth(true, ['read:lists']),
|
||||
async (ctx, reply) => {
|
||||
ctx.body = await ListHelpers.getLists(ctx.user)
|
||||
.then(p => p.map(list => convertListId(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",
|
||||
auth(true, ['read:lists']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
|
||||
ctx.body = await ListHelpers.getList(user, id)
|
||||
.then(p => convertListId(p));
|
||||
} catch (e: any) {
|
||||
ctx.status = 404;
|
||||
}
|
||||
ctx.body = await ListHelpers.getListOr404(ctx.user, id)
|
||||
.then(p => convertListId(p));
|
||||
},
|
||||
);
|
||||
router.post("/v1/lists", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
router.post("/v1/lists",
|
||||
auth(true, ['write:lists']),
|
||||
async (ctx, reply) => {
|
||||
const body = ctx.request.body as any;
|
||||
const title = (body.title ?? '').trim();
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
ctx.body = await ListHelpers.createList(ctx.user, title)
|
||||
.then(p => convertListId(p));
|
||||
}
|
||||
);
|
||||
router.put<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
auth(true, ['write:lists']),
|
||||
async (ctx, reply) => {
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: ctx.user.id, id: id});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
const title = (body.title ?? '').trim();
|
||||
if (title.length < 1) {
|
||||
ctx.body = { error: "Title must not be empty" };
|
||||
ctx.status = 400;
|
||||
return
|
||||
}
|
||||
|
||||
ctx.body = await ListHelpers.createList(user, title)
|
||||
ctx.body = await ListHelpers.updateList(ctx.user, list, title)
|
||||
.then(p => convertListId(p));
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
router.put<{ 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);
|
||||
const list = await UserLists.findOneBy({userId: user.id, id: id});
|
||||
|
||||
if (!list) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
const title = (body.title ?? '').trim();
|
||||
if (title.length < 1) {
|
||||
ctx.body = { error: "Title must not be empty" };
|
||||
ctx.status = 400;
|
||||
return
|
||||
}
|
||||
|
||||
ctx.body = await ListHelpers.updateList(user, list, title)
|
||||
.then(p => convertListId(p));
|
||||
} 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",
|
||||
auth(true, ['write:lists']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: ctx.user.id, id: id});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: user.id, id: id});
|
||||
|
||||
if (!list) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
await ListHelpers.deleteList(user, list);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
await ListHelpers.deleteList(ctx.user, list);
|
||||
ctx.body = {};
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
auth(true, ['read:lists']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const res = await ListHelpers.getListUsers(ctx.user, id, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const accounts = await UserConverter.encodeMany(res.data);
|
||||
|
||||
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 => convertAccountId(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
ctx.status = 404;
|
||||
}
|
||||
ctx.body = accounts.map(account => convertAccountId(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
auth(true, ['write:lists']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: ctx.user.id, id: id});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
const body = ctx.request.body as any;
|
||||
if (!body['account_ids']) throw new MastoApiError(400, "Missing account_ids[] field");
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: user.id, id: id});
|
||||
|
||||
if (!list) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
if (!body['account_ids']) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: "Missing account_ids[] field" };
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId));
|
||||
const targets = await Promise.all(ids.map(p => getUser(p)));
|
||||
await ListHelpers.addToList(user, list, targets);
|
||||
ctx.body = {}
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId));
|
||||
const targets = await Promise.all(ids.map(p => getUser(p)));
|
||||
await ListHelpers.addToList(ctx.user, list, targets);
|
||||
ctx.body = {}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
auth(true, ['write:lists']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: ctx.user.id, id: id});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
const body = ctx.request.body as any;
|
||||
if (!body['account_ids']) throw new MastoApiError(400, "Missing account_ids[] field");
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: user.id, id: id});
|
||||
|
||||
if (!list) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
if (!body['account_ids']) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: "Missing account_ids[] field" };
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId));
|
||||
const targets = await Promise.all(ids.map(p => getUser(p)));
|
||||
await ListHelpers.removeFromList(user, list, targets);
|
||||
ctx.body = {}
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId));
|
||||
const targets = await Promise.all(ids.map(p => getUser(p)));
|
||||
await ListHelpers.removeFromList(ctx.user, list, targets);
|
||||
ctx.body = {}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,94 +1,41 @@
|
||||
import Router from "@koa/router";
|
||||
import { convertId, IdType } from "@/misc/convert-id.js";
|
||||
import { convertAttachmentId } from "@/server/api/mastodon/converters.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js";
|
||||
import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
||||
import { Files } from "formidable";
|
||||
import { toSingleLast } from "@/prelude/array.js";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
|
||||
export function setupEndpointsMedia(router: Router): void {
|
||||
router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
router.get<{ Params: { id: string } }>("/v1/media/:id",
|
||||
auth(true, ['write:media']),
|
||||
async (ctx) => {
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const file = await MediaHelpers.getMediaPacked(user, id);
|
||||
|
||||
if (!file) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {error: "File not found"};
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await MediaHelpers.getMediaPackedOr404(ctx.user, id);
|
||||
const attachment = FileConverter.encode(file);
|
||||
ctx.body = convertAttachmentId(attachment);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.put<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
);
|
||||
router.put<{ Params: { id: string } }>("/v1/media/:id",
|
||||
auth(true, ['write:media']),
|
||||
async (ctx) => {
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const file = await MediaHelpers.getMedia(user, id);
|
||||
|
||||
if (!file) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {error: "File not found"};
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await MediaHelpers.updateMedia(user, file, ctx.request.body)
|
||||
const file = await MediaHelpers.getMediaOr404(ctx.user, id);
|
||||
const result = await MediaHelpers.updateMedia(ctx.user, file, ctx.request.body)
|
||||
.then(p => FileConverter.encode(p));
|
||||
ctx.body = convertAttachmentId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post(["/v2/media", "/v1/media"], async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
);
|
||||
router.post(["/v2/media", "/v1/media"],
|
||||
auth(true, ['write:media']),
|
||||
async (ctx) => {
|
||||
//FIXME: why do we have to cast this to any first?
|
||||
const files = (ctx.request as any).files as Files | undefined;
|
||||
const file = toSingleLast(files?.file);
|
||||
if (!file) {
|
||||
ctx.body = {error: "No image"};
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
const result = await MediaHelpers.uploadMedia(user, file, ctx.request.body)
|
||||
const result = await MediaHelpers.uploadMedia(ctx.user, file, ctx.request.body)
|
||||
.then(p => FileConverter.encode(p));
|
||||
ctx.body = convertAttachmentId(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = {error: e.message};
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
@ -1,136 +1,80 @@
|
||||
import Router from "@koa/router";
|
||||
import { MiscHelpers } from "@/server/api/mastodon/helpers/misc.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { argsToBools, limitToInt } from "@/server/api/mastodon/endpoints/timeline.js";
|
||||
import { Announcements } from "@/models/index.js";
|
||||
import { convertAnnouncementId, convertSuggestionIds } from "@/server/api/mastodon/converters.js";
|
||||
import { convertId, IdType } from "@/misc/convert-id.js";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export function setupEndpointsMisc(router: Router): void {
|
||||
router.get("/v1/custom_emojis", async (ctx) => {
|
||||
try {
|
||||
router.get("/v1/custom_emojis",
|
||||
async (ctx) => {
|
||||
ctx.body = await MiscHelpers.getCustomEmoji();
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.get("/v1/instance", async (ctx) => {
|
||||
try {
|
||||
router.get("/v1/instance",
|
||||
async (ctx) => {
|
||||
ctx.body = await MiscHelpers.getInstance();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/announcements", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
router.get("/v1/announcements",
|
||||
auth(true),
|
||||
async (ctx) => {
|
||||
const args = argsToBools(ctx.query, ['with_dismissed']);
|
||||
ctx.body = await MiscHelpers.getAnnouncements(user, args['with_dismissed'])
|
||||
ctx.body = await MiscHelpers.getAnnouncements(ctx.user, args['with_dismissed'])
|
||||
.then(p => p.map(x => convertAnnouncementId(x)));
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/announcements/:id/dismiss",
|
||||
auth(true, ['write:accounts']),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const announcement = await Announcements.findOneBy({id: id});
|
||||
if (!announcement) throw new MastoApiError(404);
|
||||
|
||||
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) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
await MiscHelpers.dismissAnnouncement(announcement, ctx.user);
|
||||
ctx.body = {};
|
||||
},
|
||||
);
|
||||
|
||||
router.get(["/v1/trends/tags", "/v1/trends"], async (ctx) => {
|
||||
try {
|
||||
router.get(["/v1/trends/tags", "/v1/trends"],
|
||||
async (ctx) => {
|
||||
const args = limitToInt(ctx.query);
|
||||
ctx.body = await MiscHelpers.getTrendingHashtags(args.limit, args.offset);
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.get("/v1/trends/statuses", async (ctx) => {
|
||||
try {
|
||||
router.get("/v1/trends/statuses",
|
||||
async (ctx) => {
|
||||
const args = limitToInt(ctx.query);
|
||||
ctx.body = await MiscHelpers.getTrendingStatuses(args.limit, args.offset);
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.get("/v1/trends/links", async (ctx) => {
|
||||
ctx.body = [];
|
||||
});
|
||||
|
||||
router.get("/v1/preferences", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await MiscHelpers.getPreferences(user);
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
router.get("/v1/trends/links",
|
||||
async (ctx) => {
|
||||
ctx.body = [];
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.get("/v2/suggestions", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
router.get("/v1/preferences",
|
||||
auth(true, ['read:accounts']),
|
||||
async (ctx) => {
|
||||
ctx.body = await MiscHelpers.getPreferences(ctx.user);
|
||||
}
|
||||
);
|
||||
|
||||
router.get("/v2/suggestions",
|
||||
auth(true, ['read']),
|
||||
async (ctx) => {
|
||||
const args = limitToInt(ctx.query);
|
||||
ctx.body = await MiscHelpers.getFollowSuggestions(user, args.limit)
|
||||
ctx.body = await MiscHelpers.getFollowSuggestions(ctx.user, args.limit)
|
||||
.then(p => p.map(x => convertSuggestionIds(x)));
|
||||
} catch (e: any) {
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
@ -1,118 +1,57 @@
|
||||
import Router from "@koa/router";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { convertConversationIds, convertNotificationIds } from "../converters.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { convertNotificationIds } from "../converters.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";
|
||||
import { TimelineHelpers } from "@/server/api/mastodon/helpers/timeline.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
|
||||
export function setupEndpointsNotifications(router: Router): void {
|
||||
router.get("/v1/notifications", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
router.get("/v1/notifications",
|
||||
auth(true, ['read:notifications']),
|
||||
async (ctx) => {
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']);
|
||||
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))
|
||||
const data = NotificationHelpers.getNotifications(ctx.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, ctx.user, cache))
|
||||
.then(p => p.map(n => convertNotificationIds(n)));
|
||||
|
||||
ctx.body = await data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.get("/v1/notifications/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await NotificationHelpers.getNotification(convertId(ctx.params.id, IdType.IceshrimpId), user);
|
||||
if (notification === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = convertNotificationIds(await NotificationConverter.encode(notification, user));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
router.get("/v1/notifications/:id",
|
||||
auth(true, ['read:notifications']),
|
||||
async (ctx) => {
|
||||
const notification = await NotificationHelpers.getNotificationOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx.user);
|
||||
ctx.body = convertNotificationIds(await NotificationConverter.encode(notification, ctx.user));
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.post("/v1/notifications/clear", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
await NotificationHelpers.clearAllNotifications(user);
|
||||
router.post("/v1/notifications/clear",
|
||||
auth(true, ['write:notifications']),
|
||||
async (ctx) => {
|
||||
await NotificationHelpers.clearAllNotifications(ctx.user);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.post("/v1/notifications/:id/dismiss", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await NotificationHelpers.getNotification(convertId(ctx.params.id, IdType.IceshrimpId), user);
|
||||
if (notification === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
await NotificationHelpers.dismissNotification(notification.id, user);
|
||||
router.post("/v1/notifications/:id/dismiss",
|
||||
auth(true, ['write:notifications']),
|
||||
async (ctx) => {
|
||||
const notification = await NotificationHelpers.getNotificationOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx.user);
|
||||
await NotificationHelpers.dismissNotification(notification.id, ctx.user);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.post("/v1/conversations/:id/read", async (ctx, reply) => {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
router.post("/v1/conversations/:id/read",
|
||||
auth(true, ['write:conversations']),
|
||||
async (ctx, reply) => {
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
await NotificationHelpers.markConversationAsRead(id, ctx.user);
|
||||
ctx.body = {};
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
await NotificationHelpers.markConversationAsRead(id, user);
|
||||
ctx.body = {};
|
||||
});
|
||||
);
|
||||
}
|
||||
|
@ -1,54 +1,26 @@
|
||||
import Router from "@koa/router";
|
||||
import { argsToBools, convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { convertSearchIds } from "../converters.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
import { SearchHelpers } from "@/server/api/mastodon/helpers/search.js";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
|
||||
export function setupEndpointsSearch(router: Router): void {
|
||||
router.get("/v1/search", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
router.get(["/v1/search", "/v2/search"],
|
||||
auth(true, ['read:search']),
|
||||
async (ctx) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const result = await SearchHelpers.search(user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
|
||||
ctx.body = {
|
||||
...convertSearchIds(result),
|
||||
hashtags: result.hashtags.map(p => p.name),
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 400;
|
||||
ctx.body = {error: e.message};
|
||||
}
|
||||
});
|
||||
router.get("/v2/search", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const result = await SearchHelpers.search(user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
const result = await SearchHelpers.search(ctx.user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
|
||||
ctx.body = convertSearchIds(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 400;
|
||||
ctx.body = {error: e.message};
|
||||
|
||||
if (ctx.path === "/v1/search") {
|
||||
ctx.body = {
|
||||
...ctx.body,
|
||||
hashtags: result.hashtags.map(p => p.name),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
@ -2,36 +2,21 @@ import Router from "@koa/router";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import { convertAccountId, convertPollId, convertStatusIds, convertStatusEditIds, convertStatusSourceId, } from "../converters.js";
|
||||
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
|
||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
||||
import { Cache } from "@/misc/cache.js";
|
||||
import AsyncLock from "async-lock";
|
||||
import { ILocalUser } from "@/models/entities/user.js";
|
||||
import { PollHelpers } from "@/server/api/mastodon/helpers/poll.js";
|
||||
import { toArray } from "@/prelude/array.js";
|
||||
|
||||
const postIdempotencyCache = new Cache<{ status?: MastodonEntity.Status }>('postIdempotencyCache', 60 * 60);
|
||||
const postIdempotencyLocks = new AsyncLock();
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
|
||||
export function setupEndpointsStatus(router: Router): void {
|
||||
router.post("/v1/statuses", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getIdempotencyKey(ctx.headers, user);
|
||||
router.post("/v1/statuses",
|
||||
auth(true, ['write:statuses']),
|
||||
async (ctx) => {
|
||||
const key = NoteHelpers.getIdempotencyKey(ctx.headers, ctx.user);
|
||||
if (key !== null) {
|
||||
const result = await getFromIdempotencyCache(key);
|
||||
const result = await NoteHelpers.getFromIdempotencyCache(key);
|
||||
|
||||
if (result) {
|
||||
ctx.body = result;
|
||||
@ -40,645 +25,263 @@ export function setupEndpointsStatus(router: Router): void {
|
||||
}
|
||||
|
||||
let request = NoteHelpers.normalizeComposeOptions(ctx.request.body);
|
||||
ctx.body = await NoteHelpers.createNote(request, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
ctx.body = await NoteHelpers.createNote(request, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
|
||||
if (key !== null) postIdempotencyCache.set(key, {status: ctx.body});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = {error: e.message};
|
||||
if (key !== null) NoteHelpers.postIdempotencyCache.set(key, {status: ctx.body});
|
||||
}
|
||||
});
|
||||
router.put("/v1/statuses/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
);
|
||||
router.put("/v1/statuses/:id",
|
||||
auth(true, ['write:statuses']),
|
||||
async (ctx) => {
|
||||
const noteId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(noteId, user ?? null).then(n => n).catch(() => null);
|
||||
if (!note) {
|
||||
if (!note) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
error: "Note not found"
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const note = await NoteHelpers.getNoteOr404(noteId, ctx.user);
|
||||
let request = NoteHelpers.normalizeEditOptions(ctx.request.body);
|
||||
ctx.body = await NoteHelpers.editNote(request, note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
ctx.body = await NoteHelpers.editNote(request, note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = ctx.status == 404 ? 404 : 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
);
|
||||
router.get<{ Params: { id: string } }>("/v1/statuses/:id",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
const noteId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(noteId, user ?? null).then(n => n).catch(() => null);
|
||||
const note = await NoteHelpers.getNoteOr404(noteId, ctx.user);
|
||||
|
||||
if (!note) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await NoteConverter.encode(note, user);
|
||||
const status = await NoteConverter.encode(note, ctx.user);
|
||||
ctx.body = convertStatusIds(status);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = ctx.status == 404 ? 404 : 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.delete<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>("/v1/statuses/:id",
|
||||
auth(true, ['write:statuses']),
|
||||
async (ctx) => {
|
||||
const noteId = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(noteId, user ?? null).then(n => n).catch(() => null);
|
||||
|
||||
if (!note) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {
|
||||
error: "Note not found"
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.id !== note.userId) {
|
||||
ctx.status = 403;
|
||||
ctx.body = {
|
||||
error: "Cannot delete someone else's note"
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.deleteNote(note, user)
|
||||
const note = await NoteHelpers.getNoteOr404(noteId, ctx.user);
|
||||
ctx.body = await NoteHelpers.deleteNote(note, ctx.user)
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(`Error processing ${ctx.method} /api${ctx.path}: ${e.message}`);
|
||||
ctx.status = 500;
|
||||
ctx.body = {
|
||||
error: e.message
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/context",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const ancestors = await NoteHelpers.getNoteAncestors(note, ctx.user, ctx.user ? 4096 : 60)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, ctx.cache))
|
||||
.then(n => n.map(s => convertStatusIds(s)));
|
||||
const descendants = await NoteHelpers.getNoteDescendants(note, ctx.user, ctx.user ? 4096 : 40, ctx.user ? 4096 : 20)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, ctx.cache))
|
||||
.then(n => n.map(s => convertStatusIds(s)));
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const note = await getNote(id, user ?? null).then(n => n).catch(() => null);
|
||||
if (!note) {
|
||||
if (!note) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const ancestors = await NoteHelpers.getNoteAncestors(note, user, user ? 4096 : 60)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache))
|
||||
.then(n => n.map(s => convertStatusIds(s)));
|
||||
const descendants = await NoteHelpers.getNoteDescendants(note, user, user ? 4096 : 40, user ? 4096 : 20)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache))
|
||||
.then(n => n.map(s => convertStatusIds(s)));
|
||||
|
||||
ctx.body = {
|
||||
ancestors,
|
||||
descendants,
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
ctx.body = {
|
||||
ancestors,
|
||||
descendants,
|
||||
};
|
||||
}
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/history",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await NoteHelpers.getNoteEditHistory(note);
|
||||
ctx.body = res.map(p => convertStatusEditIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const res = await NoteHelpers.getNoteEditHistory(note);
|
||||
ctx.body = res.map(p => convertStatusEditIds(p));
|
||||
}
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/source",
|
||||
auth(true, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const src = NoteHelpers.getNoteSource(note);
|
||||
ctx.body = convertStatusSourceId(src);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const src = NoteHelpers.getNoteSource(note);
|
||||
ctx.body = convertStatusSourceId(src);
|
||||
}
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/reblogged_by",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await NoteHelpers.getNoteRebloggedBy(note, user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const users = await UserConverter.encodeMany(res.data, cache);
|
||||
ctx.body = users.map(m => convertAccountId(m));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await NoteHelpers.getNoteRebloggedBy(note, ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const users = await UserConverter.encodeMany(res.data, ctx.cache);
|
||||
ctx.body = users.map(m => convertAccountId(m));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
}
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/favourited_by",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await NoteHelpers.getNoteFavoritedBy(note, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const users = await UserConverter.encodeMany(res.data, cache);
|
||||
ctx.body = users.map(m => convertAccountId(m));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
|
||||
const res = await NoteHelpers.getNoteFavoritedBy(note, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const users = await UserConverter.encodeMany(res.data, ctx.cache);
|
||||
ctx.body = users.map(m => convertAccountId(m));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 40);
|
||||
}
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/favourite",
|
||||
auth(true, ["write:favourites"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const reaction = await NoteHelpers.getDefaultReaction();
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const reaction = await NoteHelpers.getDefaultReaction().catch(_ => null);
|
||||
|
||||
if (reaction === null) {
|
||||
ctx.status = 500;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.reactToNote(note, user, reaction)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 400;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
ctx.body = await NoteHelpers.reactToNote(note, ctx.user, reaction)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
}
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unfavourite",
|
||||
auth(true, ["write:favourites"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.removeReactFromNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.removeReactFromNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/reblog",
|
||||
auth(true, ["write:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.reblogNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.reblogNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unreblog",
|
||||
auth(true, ["write:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.unreblogNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.unreblogNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/bookmark",
|
||||
auth(true, ["write:bookmarks"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.bookmarkNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.bookmarkNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unbookmark",
|
||||
auth(true, ["write:bookmarks"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.unbookmarkNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.unbookmarkNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/pin",
|
||||
auth(true, ["write:accounts"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.pinNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.pinNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/unpin",
|
||||
auth(true, ["write:accounts"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.unpinNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.unpinNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string; name: string } }>(
|
||||
"/v1/statuses/:id/react/:name",
|
||||
auth(true, ["write:favourites"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.reactToNote(note, user, ctx.params.name)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.reactToNote(note, ctx.user, ctx.params.name)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
|
||||
router.post<{ Params: { id: string; name: string } }>(
|
||||
"/v1/statuses/:id/unreact/:name",
|
||||
auth(true, ["write:favourites"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await NoteHelpers.removeReactFromNote(note, user)
|
||||
.then(p => NoteConverter.encode(p, user))
|
||||
.then(p => convertStatusIds(p));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
ctx.body = await NoteHelpers.removeReactFromNote(note, ctx.user)
|
||||
.then(p => NoteConverter.encode(p, ctx.user))
|
||||
.then(p => convertStatusIds(p));
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>("/v1/polls/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
router.get<{ Params: { id: string } }>("/v1/polls/:id",
|
||||
auth(false, ["read:statuses"]),
|
||||
async (ctx) => {
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null || !note.hasPoll) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await PollHelpers.getPoll(note, user);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
const data = await PollHelpers.getPoll(note, ctx.user);
|
||||
ctx.body = convertPollId(data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/polls/:id/votes",
|
||||
auth(true, ["write:statuses"]),
|
||||
async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await NoteHelpers.getNoteOr404(id, ctx.user);
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const note = await getNote(id, user).catch(_ => null);
|
||||
|
||||
if (note === null || !note.hasPoll) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const body: any = ctx.request.body;
|
||||
const choices = toArray(body.choices ?? []).map(p => parseInt(p));
|
||||
if (choices.length < 1) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {error: 'Must vote for at least one option'};
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await PollHelpers.voteInPoll(choices, note, user);
|
||||
ctx.body = convertPollId(data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
const body: any = ctx.request.body;
|
||||
const choices = toArray(body.choices ?? []).map(p => parseInt(p));
|
||||
if (choices.length < 1) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {error: 'Must vote for at least one option'};
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await PollHelpers.voteInPoll(choices, note, ctx.user);
|
||||
ctx.body = convertPollId(data);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function getIdempotencyKey(headers: any, user: ILocalUser): string | null {
|
||||
if (headers["idempotency-key"] === undefined || headers["idempotency-key"] === null) return null;
|
||||
return `${user.id}-${Array.isArray(headers["idempotency-key"]) ? headers["idempotency-key"].at(-1)! : headers["idempotency-key"]}`;
|
||||
}
|
||||
|
||||
async function getFromIdempotencyCache(key: string): Promise<MastodonEntity.Status | undefined> {
|
||||
return postIdempotencyLocks.acquire(key, async (): Promise<MastodonEntity.Status | undefined> => {
|
||||
if (await postIdempotencyCache.get(key) !== undefined) {
|
||||
let i = 5;
|
||||
while ((await postIdempotencyCache.get(key))?.status === undefined) {
|
||||
if (++i > 5) throw new Error('Post is duplicate but unable to resolve original');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
}
|
||||
|
||||
return (await postIdempotencyCache.get(key))?.status;
|
||||
} else {
|
||||
await postIdempotencyCache.set(key, {});
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2,13 +2,15 @@ import Router from "@koa/router";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import { convertConversationIds, convertStatusIds, } 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 { UserLists } from "@/models/index.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { auth } from "@/server/api/mastodon/middleware/auth.js";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
//TODO: Move helper functions to a helper class
|
||||
export function limitToInt(q: ParsedUrlQuery, additional: string[] = []) {
|
||||
let object: any = q;
|
||||
if (q.limit)
|
||||
@ -63,138 +65,63 @@ export function normalizeUrlQuery(q: ParsedUrlQuery, arrayKeys: string[] = []):
|
||||
}
|
||||
|
||||
export function setupEndpointsTimeline(router: Router): void {
|
||||
router.get("/v1/timelines/public", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
router.get("/v1/timelines/public",
|
||||
auth(true, ['read:statuses']),
|
||||
async (ctx, reply) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getPublicTimeline(user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
const tl = await TimelineHelpers.getPublicTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { hashtag: string } }>(
|
||||
"/v1/timelines/tag/:hashtag",
|
||||
auth(false, ['read:statuses']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = (ctx.params.hashtag ?? '').trim();
|
||||
if (tag.length < 1) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: "tag cannot be empty" };
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))), ['any[]', 'all[]', 'none[]']);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getTagTimeline(user, tag, args.max_id, args.since_id, args.min_id, args.limit, args['any[]'] ?? [], args['all[]'] ?? [], args['none[]'] ?? [], args.only_media, args.local, args.remote)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
} catch (e: any) {
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/timelines/home", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const tag = (ctx.params.hashtag ?? '').trim();
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))), ['any[]', 'all[]', 'none[]']);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getHomeTimeline(user, args.max_id, args.since_id, args.min_id, args.limit)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
const tl = await TimelineHelpers.getTagTimeline(ctx.user, tag, args.max_id, args.since_id, args.min_id, args.limit, args['any[]'] ?? [], args['all[]'] ?? [], args['none[]'] ?? [], args.only_media, args.local, args.remote)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
},
|
||||
);
|
||||
router.get("/v1/timelines/home",
|
||||
auth(true, ['read:statuses']),
|
||||
async (ctx, reply) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getHomeTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { listId: string } }>(
|
||||
"/v1/timelines/list/:listId",
|
||||
auth(true, ['read:lists']),
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const listId = convertId(ctx.params.listId, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: user.id, id: listId});
|
||||
|
||||
if (!list) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getListTimeline(user, list, args.max_id, args.since_id, args.min_id, args.limit)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/conversations", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
const listId = convertId(ctx.params.listId, IdType.IceshrimpId);
|
||||
const list = await UserLists.findOneBy({userId: ctx.user.id, id: listId});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const res = await TimelineHelpers.getConversations(user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getListTimeline(ctx.user, list, args.max_id, args.since_id, args.min_id, args.limit)
|
||||
.then(n => NoteConverter.encodeMany(n, ctx.user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatusIds(s));
|
||||
},
|
||||
);
|
||||
router.get("/v1/conversations",
|
||||
auth(true, ['read:statuses']),
|
||||
async (ctx, reply) => {
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const res = await TimelineHelpers.getConversations(ctx.user, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
|
||||
ctx.body = res.data.map(c => convertConversationIds(c));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res, 20);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { UserList } from "@/models/entities/user-list.js";
|
||||
import { pushUserToUserList } from "@/services/user-list/push.js";
|
||||
import { genId } from "@/misc/gen-id.js";
|
||||
import { publishUserListStream } from "@/services/stream.js";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export class ListHelpers {
|
||||
public static async getLists(user: ILocalUser): Promise<MastodonEntity.List[]> {
|
||||
@ -26,9 +27,15 @@ export class ListHelpers {
|
||||
});
|
||||
}
|
||||
|
||||
public static async getListOr404(user: ILocalUser, id: string): Promise<MastodonEntity.List> {
|
||||
return this.getList(user, id).catch(_ => {
|
||||
throw new MastoApiError(404);
|
||||
})
|
||||
}
|
||||
public static async getListUsers(user: ILocalUser, id: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const list = await UserLists.findOneByOrFail({userId: user.id, id: id});
|
||||
const list = await UserLists.findOneBy({userId: user.id, id: id});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
UserListJoinings.createQueryBuilder('member'),
|
||||
sinceId,
|
||||
@ -99,6 +106,8 @@ export class ListHelpers {
|
||||
}
|
||||
|
||||
public static async createList(user: ILocalUser, title: string): Promise<MastodonEntity.List> {
|
||||
if (title.length < 1) throw new MastoApiError(400, "Title must not be empty");
|
||||
|
||||
const list = await UserLists.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
@ -113,7 +122,9 @@ export class ListHelpers {
|
||||
}
|
||||
|
||||
public static async updateList(user: ILocalUser, list: UserList, title: string) {
|
||||
if (title.length < 1) throw new MastoApiError(400, "Title must not be empty");
|
||||
if (user.id != list.userId) throw new Error("List is not owned by user");
|
||||
|
||||
const partial = {name: title};
|
||||
const result = await UserLists.update(list.id, partial)
|
||||
.then(async _ => await UserLists.findOneByOrFail({id: list.id}));
|
||||
|
@ -4,9 +4,12 @@ import { DriveFiles } from "@/models/index.js";
|
||||
import { Packed } from "@/misc/schema.js";
|
||||
import { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import { File } from "formidable";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export class MediaHelpers {
|
||||
public static async uploadMedia(user: ILocalUser, file: File, body: any): Promise<Packed<"DriveFile">> {
|
||||
public static async uploadMedia(user: ILocalUser, file: File | undefined, body: any): Promise<Packed<"DriveFile">> {
|
||||
if (!file) throw new MastoApiError(400, "Validation failed: File content type is invalid, File is invalid");
|
||||
|
||||
return addFile({
|
||||
user: user,
|
||||
path: file.filepath,
|
||||
@ -40,7 +43,21 @@ export class MediaHelpers {
|
||||
.then(p => p ? DriveFiles.pack(p) : null);
|
||||
}
|
||||
|
||||
public static async getMediaPackedOr404(user: ILocalUser, id: string): Promise<Packed<"DriveFile">> {
|
||||
return this.getMediaPacked(user, id).then(p => {
|
||||
if (p) return p;
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getMedia(user: ILocalUser, id: string): Promise<DriveFile | null> {
|
||||
return DriveFiles.findOneBy({id: id, userId: user.id});
|
||||
}
|
||||
|
||||
public static async getMediaOr404(user: ILocalUser, id: string): Promise<DriveFile> {
|
||||
return this.getMedia(user, id).then(p => {
|
||||
if (p) return p;
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +24,23 @@ import mfm from "mfm-js";
|
||||
import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
||||
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
||||
import { toArray } from "@/prelude/array.js";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
import { Cache } from "@/misc/cache.js";
|
||||
import AsyncLock from "async-lock";
|
||||
|
||||
export class NoteHelpers {
|
||||
public static postIdempotencyCache = new Cache<{ status?: MastodonEntity.Status }>('postIdempotencyCache', 60 * 60);
|
||||
public static postIdempotencyLocks = new AsyncLock();
|
||||
|
||||
public static async getDefaultReaction(): Promise<string> {
|
||||
return Metas.createQueryBuilder()
|
||||
.select('"defaultReaction"')
|
||||
.execute()
|
||||
.then(p => p[0].defaultReaction);
|
||||
.then(p => p[0].defaultReaction)
|
||||
.then(p => {
|
||||
if (p != null) return p;
|
||||
throw new MastoApiError(500, "Failed to get default reaction");
|
||||
});
|
||||
}
|
||||
|
||||
public static async reactToNote(note: Note, user: ILocalUser, reaction: string): Promise<Note> {
|
||||
@ -122,7 +132,7 @@ export class NoteHelpers {
|
||||
}
|
||||
|
||||
public static async deleteNote(note: Note, user: ILocalUser): Promise<MastodonEntity.Status> {
|
||||
if (user.id !== note.userId) throw new Error("Can't delete someone elses note");
|
||||
if (user.id !== note.userId) throw new MastoApiError(404);
|
||||
const status = await NoteConverter.encode(note, user);
|
||||
await deleteNote(user, note);
|
||||
status.content = undefined;
|
||||
@ -376,4 +386,35 @@ export class NoteHelpers {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async getNoteOr404(id: string, user: ILocalUser | null): Promise<Note> {
|
||||
return getNote(id, user).then(p => {
|
||||
if (p === null) throw new MastoApiError(404);
|
||||
return p;
|
||||
});
|
||||
}
|
||||
|
||||
public static getIdempotencyKey(headers: any, user: ILocalUser): string | null {
|
||||
if (headers["idempotency-key"] === undefined || headers["idempotency-key"] === null) return null;
|
||||
return `${user.id}-${Array.isArray(headers["idempotency-key"]) ? headers["idempotency-key"].at(-1)! : headers["idempotency-key"]}`;
|
||||
}
|
||||
|
||||
public static async getFromIdempotencyCache(key: string): Promise<MastodonEntity.Status | undefined> {
|
||||
return this.postIdempotencyLocks.acquire(key, async (): Promise<MastodonEntity.Status | undefined> => {
|
||||
if (await this.postIdempotencyCache.get(key) !== undefined) {
|
||||
let i = 5;
|
||||
while ((await this.postIdempotencyCache.get(key))?.status === undefined) {
|
||||
if (++i > 5) throw new Error('Post is duplicate but unable to resolve original');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
}
|
||||
|
||||
return (await this.postIdempotencyCache.get(key))?.status;
|
||||
} else {
|
||||
await this.postIdempotencyCache.set(key, {});
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { ILocalUser } from "@/models/entities/user.js";
|
||||
import { Notes, Notifications } from "@/models/index.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { Notification } from "@/models/entities/notification.js";
|
||||
import {MastoApiError} from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export class NotificationHelpers {
|
||||
public static async getNotifications(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined): Promise<Notification[]> {
|
||||
@ -38,6 +39,13 @@ export class NotificationHelpers {
|
||||
return Notifications.findOneBy({id: id, notifieeId: user.id});
|
||||
}
|
||||
|
||||
public static async getNotificationOr404(id: string, user: ILocalUser): Promise<Notification> {
|
||||
return this.getNotification(id, user).then(p => {
|
||||
if (p) return p;
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
|
||||
public static async dismissNotification(id: string, user: ILocalUser): Promise<void> {
|
||||
const result = await Notifications.update({id: id, notifieeId: user.id}, {isRead: true});
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { deliver } from "@/queue/index.js";
|
||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||
import renderVote from "@/remote/activitypub/renderer/vote.js";
|
||||
import { Not } from "typeorm";
|
||||
import {MastoApiError} from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export class PollHelpers {
|
||||
public static async getPoll(note: Note, user: ILocalUser | null): Promise<MastodonEntity.Poll> {
|
||||
@ -17,10 +18,12 @@ export class PollHelpers {
|
||||
}
|
||||
|
||||
public static async voteInPoll(choices: number[], note: Note, user: ILocalUser): Promise<MastodonEntity.Poll> {
|
||||
if (!note.hasPoll) throw new MastoApiError(404);
|
||||
|
||||
for (const choice of choices) {
|
||||
const createdAt = new Date();
|
||||
|
||||
if (!note.hasPoll) throw new Error('Note has no poll');
|
||||
if (!note.hasPoll) throw new MastoApiError(404);
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
|
@ -17,6 +17,7 @@ import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
||||
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
|
||||
import { awaitAll } from "@/prelude/await-all.js";
|
||||
import { unique } from "@/prelude/array.js";
|
||||
import {MastoApiError} from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export class TimelineHelpers {
|
||||
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
|
||||
@ -123,6 +124,8 @@ export class TimelineHelpers {
|
||||
public static async getTagTimeline(user: ILocalUser, tag: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, any: string[], all: string[], none: string[], onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
if (tag.length < 1) throw new MastoApiError(400, "Tag cannot be empty");
|
||||
|
||||
if (local && remote) {
|
||||
throw new Error("local and remote are mutually exclusive options");
|
||||
}
|
||||
|
@ -37,9 +37,9 @@ import { IceshrimpVisibility, VisibilityConverter } from "@/server/api/mastodon/
|
||||
import { Files } from "formidable";
|
||||
import { toSingleLast } from "@/prelude/array.js";
|
||||
import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js";
|
||||
import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
||||
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { verifyLink } from "@/services/fetch-rel-me.js";
|
||||
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
|
||||
export type AccountCache = {
|
||||
locks: AsyncLock;
|
||||
@ -192,8 +192,7 @@ export class UserHelpers {
|
||||
}
|
||||
|
||||
public static async verifyCredentials(user: ILocalUser): Promise<MastodonEntity.Account> {
|
||||
// re-fetch local user because auth user possibly contains outdated info
|
||||
const acct = getUser(user.id).then(u => UserConverter.encode(u));
|
||||
const acct = UserConverter.encode(user);
|
||||
const profile = UserProfiles.findOneByOrFail({userId: user.id});
|
||||
const privacy = this.getDefaultNoteVisibility(user);
|
||||
const fields = profile.then(profile => profile.fields.map(field => {
|
||||
@ -220,10 +219,14 @@ export class UserHelpers {
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFromAcct(acct: string): Promise<User | null> {
|
||||
public static async getUserFromAcct(acct: string): Promise<User> {
|
||||
const split = acct.toLowerCase().split('@');
|
||||
if (split.length > 2) throw new Error('Invalid acct');
|
||||
return Users.findOneBy({usernameLower: split[0], host: split[1] ?? IsNull()});
|
||||
return Users.findOneBy({usernameLower: split[0], host: split[1] ?? IsNull()})
|
||||
.then(p => {
|
||||
if (p) return p;
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserMutes(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<LinkPaginationObject<MastodonEntity.MutedAccount[]>> {
|
||||
@ -514,6 +517,18 @@ export class UserHelpers {
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserCachedOr404(id: string, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<User> {
|
||||
return this.getUserCached(id, cache).catch(_ => {
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserOr404(id: string): Promise<User> {
|
||||
return getUser(id).catch(_ => {
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
|
||||
public static getFreshAccountCache(): AccountCache {
|
||||
return {
|
||||
locks: new AsyncLock(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Router from "@koa/router";
|
||||
import { DefaultContext } from "koa";
|
||||
import Router, { RouterContext } from "@koa/router";
|
||||
import { setupEndpointsAuth } from "./endpoints/auth.js";
|
||||
import { setupEndpointsAccount } from "./endpoints/account.js";
|
||||
import { setupEndpointsStatus } from "./endpoints/status.js";
|
||||
@ -8,29 +9,19 @@ import { setupEndpointsNotifications } from "./endpoints/notifications.js";
|
||||
import { setupEndpointsSearch } from "./endpoints/search.js";
|
||||
import { setupEndpointsMedia } from "@/server/api/mastodon/endpoints/media.js";
|
||||
import { setupEndpointsMisc } from "@/server/api/mastodon/endpoints/misc.js";
|
||||
import { HttpMethodEnum, koaBody } from "koa-body";
|
||||
import { setupEndpointsList } from "@/server/api/mastodon/endpoints/list.js";
|
||||
import { AuthMiddleware } from "@/server/api/mastodon/middleware/auth.js";
|
||||
import { CatchErrorsMiddleware } from "@/server/api/mastodon/middleware/catch-errors.js";
|
||||
import { apiLogger } from "@/server/api/logger.js";
|
||||
import { CacheMiddleware } from "@/server/api/mastodon/middleware/cache.js";
|
||||
import { KoaBodyMiddleware } from "@/server/api/mastodon/middleware/koa-body.js";
|
||||
import { NormalizeQueryMiddleware } from "@/server/api/mastodon/middleware/normalize-query.js";
|
||||
|
||||
export const logger = apiLogger.createSubLogger("mastodon");
|
||||
export type MastoContext = RouterContext & DefaultContext;
|
||||
|
||||
export function setupMastodonApi(router: Router): void {
|
||||
router.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
urlencoded: true,
|
||||
parsedMethods: [HttpMethodEnum.POST, HttpMethodEnum.PUT, HttpMethodEnum.PATCH, HttpMethodEnum.DELETE] // dear god mastodon why
|
||||
}),
|
||||
);
|
||||
|
||||
router.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = {...ctx.request.body, ...ctx.request.query};
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
setupMiddleware(router);
|
||||
setupEndpointsAuth(router);
|
||||
setupEndpointsAccount(router);
|
||||
setupEndpointsStatus(router);
|
||||
@ -42,3 +33,11 @@ export function setupMastodonApi(router: Router): void {
|
||||
setupEndpointsList(router);
|
||||
setupEndpointsMisc(router);
|
||||
}
|
||||
|
||||
function setupMiddleware(router: Router): void {
|
||||
router.use(KoaBodyMiddleware());
|
||||
router.use(NormalizeQueryMiddleware);
|
||||
router.use(AuthMiddleware);
|
||||
router.use(CacheMiddleware);
|
||||
router.use(CatchErrorsMiddleware);
|
||||
}
|
||||
|
37
packages/backend/src/server/api/mastodon/middleware/auth.ts
Normal file
37
packages/backend/src/server/api/mastodon/middleware/auth.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { ILocalUser } from "@/models/entities/user.js";
|
||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||
import { AuthConverter } from "@/server/api/mastodon/converters/auth.js";
|
||||
|
||||
export async function AuthMiddleware(ctx: MastoContext, next: () => Promise<any>) {
|
||||
const auth = await authenticate(ctx.headers.authorization, null, true);
|
||||
ctx.user = auth[0] ?? null as ILocalUser | null;
|
||||
ctx.scopes = auth[1]?.permission ?? [] as string[];
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
export function auth(required: boolean, scopes: string[] = []) {
|
||||
return async function auth(ctx: MastoContext, next: () => Promise<any>) {
|
||||
if (required && !ctx.user) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { error: "This method requires an authenticated user" };
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AuthConverter.decode(scopes).every(p => ctx.scopes.includes(p))) {
|
||||
if (required) {
|
||||
ctx.status = 403;
|
||||
ctx.body = {error: "This action is outside the authorized scopes"};
|
||||
}
|
||||
else {
|
||||
ctx.user = null;
|
||||
ctx.scopes = [];
|
||||
}
|
||||
}
|
||||
|
||||
ctx.scopes = AuthConverter.encode(ctx.scopes);
|
||||
|
||||
await next();
|
||||
};
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
|
||||
export async function CacheMiddleware(ctx: MastoContext, next: () => Promise<any>) {
|
||||
ctx.cache = UserHelpers.getFreshAccountCache();
|
||||
await next();
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { MastoContext, logger } from "@/server/api/mastodon/index.js";
|
||||
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
||||
|
||||
export class MastoApiError extends Error {
|
||||
statusCode: number;
|
||||
constructor(statusCode: number, message?: string) {
|
||||
if (message == null) {
|
||||
switch (statusCode) {
|
||||
case 404:
|
||||
message = 'Record not found';
|
||||
break;
|
||||
default:
|
||||
message = 'Unknown error occurred';
|
||||
break;
|
||||
}
|
||||
}
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
export async function CatchErrorsMiddleware(ctx: MastoContext, next: () => Promise<any>) {
|
||||
try {
|
||||
await next();
|
||||
} catch (e: any) {
|
||||
if (e instanceof MastoApiError) {
|
||||
ctx.status = e.statusCode;
|
||||
}
|
||||
else if (e instanceof IdentifiableError) {
|
||||
ctx.status = 400;
|
||||
}
|
||||
else {
|
||||
logger.error(`Error occured in ${ctx.method} ${ctx.path}:`);
|
||||
if (e instanceof Error) {
|
||||
if (e.stack) logger.error(e.stack);
|
||||
else logger.error(`${e.name}: ${e.message}`);
|
||||
}
|
||||
else {
|
||||
logger.error(e);
|
||||
}
|
||||
ctx.status = 500;
|
||||
}
|
||||
ctx.body = { error: e.message };
|
||||
return;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Middleware } from "@koa/router";
|
||||
import { HttpMethodEnum, koaBody } from "koa-body";
|
||||
|
||||
export function KoaBodyMiddleware(): Middleware {
|
||||
const options = {
|
||||
multipart: true,
|
||||
urlencoded: true,
|
||||
parsedMethods: [HttpMethodEnum.POST, HttpMethodEnum.PUT, HttpMethodEnum.PATCH, HttpMethodEnum.DELETE] // dear god mastodon why
|
||||
};
|
||||
|
||||
return koaBody(options);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||
|
||||
export async function NormalizeQueryMiddleware(ctx: MastoContext, next: () => Promise<any>) {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = {...ctx.request.body, ...ctx.request.query};
|
||||
}
|
||||
}
|
||||
await next();
|
||||
}
|
@ -29,11 +29,9 @@ import fileServer from "./file/index.js";
|
||||
import proxyServer from "./proxy/index.js";
|
||||
import webServer from "./web/index.js";
|
||||
import { initializeStreamingServer } from "./api/streaming.js";
|
||||
import { koaBody } from "koa-body";
|
||||
import removeTrailingSlash from "koa-remove-trailing-slashes";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { AuthHelpers } from "@/server/api/mastodon/helpers/auth.js";
|
||||
|
||||
import { koaBody } from "koa-body";
|
||||
import { setupEndpointsAuthRoot } from "@/server/api/mastodon/endpoints/auth.js";
|
||||
export const serverLogger = new Logger("server", "gray", false);
|
||||
|
||||
// Init app
|
||||
@ -83,24 +81,6 @@ app.use(mount("/proxy", proxyServer));
|
||||
const router = new Router();
|
||||
const mastoRouter = new Router();
|
||||
|
||||
mastoRouter.use(
|
||||
koaBody({
|
||||
urlencoded: true,
|
||||
multipart: true,
|
||||
}),
|
||||
);
|
||||
|
||||
mastoRouter.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// Routing
|
||||
router.use(activityPub.routes());
|
||||
router.use(nodeinfo.routes());
|
||||
@ -136,55 +116,29 @@ router.get("/identicon/:x", async (ctx) => {
|
||||
}
|
||||
});
|
||||
|
||||
//TODO: move these to auth.ts
|
||||
mastoRouter.get("/oauth/authorize", async (ctx) => {
|
||||
const { client_id, state, redirect_uri } = ctx.request.query;
|
||||
console.log(ctx.request.req);
|
||||
let param = "mastodon=true";
|
||||
if (state) param += `&state=${state}`;
|
||||
if (redirect_uri) param += `&redirect_uri=${redirect_uri}`;
|
||||
const client = client_id ? client_id : "";
|
||||
ctx.redirect(
|
||||
`${Buffer.from(client.toString(), "base64").toString()}?${param}`,
|
||||
);
|
||||
mastoRouter.use(
|
||||
koaBody({
|
||||
urlencoded: true,
|
||||
multipart: true,
|
||||
}),
|
||||
);
|
||||
|
||||
mastoRouter.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
mastoRouter.post("/oauth/token", async (ctx) => {
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
console.log("token-request", body);
|
||||
console.log("token-query", ctx.request.query);
|
||||
if (body.grant_type === "client_credentials") {
|
||||
ctx.body = {
|
||||
access_token: uuid(),
|
||||
token_type: "Bearer",
|
||||
scope: "read",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
return;
|
||||
}
|
||||
let token = null;
|
||||
if (body.code) {
|
||||
token = body.code;
|
||||
}
|
||||
try {
|
||||
const accessToken = await AuthHelpers.getAuthToken(body.client_secret, token ? token : "");
|
||||
const ret = {
|
||||
access_token: accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: body.scope || "read write follow push",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
ctx.body = ret;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ctx.status = 401;
|
||||
ctx.body = err.response.data;
|
||||
}
|
||||
});
|
||||
setupEndpointsAuthRoot(mastoRouter);
|
||||
|
||||
// Register router
|
||||
app.use(mastoRouter.routes());
|
||||
app.use(router.routes());
|
||||
app.use(mastoRouter.routes());
|
||||
|
||||
app.use(mount(webServer));
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user