[mastodon-client] Use ctx instead of ctx.user as arguments everywhere

This commit is contained in:
Laura Hausmann 2023-10-07 21:39:22 +02:00
parent 79c3e56989
commit cc96b0ba72
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
19 changed files with 312 additions and 229 deletions

View file

@ -21,7 +21,8 @@ import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
import { MastoContext } from "@/server/api/mastodon/index.js"; import { MastoContext } from "@/server/api/mastodon/index.js";
export class NoteConverter { export class NoteConverter {
public static async encode(note: Note, user: ILocalUser | null, ctx: MastoContext, recurse: boolean = true): Promise<MastodonEntity.Status> { public static async encode(note: Note, ctx: MastoContext, recurse: boolean = true): Promise<MastodonEntity.Status> {
const user = ctx.user as ILocalUser | null;
const noteUser = note.user ?? UserHelpers.getUserCached(note.userId, ctx); const noteUser = note.user ?? UserHelpers.getUserCached(note.userId, ctx);
if (!await Notes.isVisibleForMe(note, user?.id ?? null)) if (!await Notes.isVisibleForMe(note, user?.id ?? null))
@ -109,7 +110,7 @@ export class NoteConverter {
account: Promise.resolve(noteUser).then(p => UserConverter.encode(p, ctx)), account: Promise.resolve(noteUser).then(p => UserConverter.encode(p, ctx)),
in_reply_to_id: note.replyId, in_reply_to_id: note.replyId,
in_reply_to_account_id: note.replyUserId, in_reply_to_account_id: note.replyUserId,
reblog: Promise.resolve(renote).then(renote => recurse && renote && note.text === null ? this.encode(renote, user, ctx, false) : null), reblog: Promise.resolve(renote).then(renote => recurse && renote && note.text === null ? this.encode(renote, ctx, false) : null),
content: text.then(text => text !== null ? MfmHelpers.toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers)) ?? escapeMFM(text) : ""), content: text.then(text => text !== null ? MfmHelpers.toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers)) ?? escapeMFM(text) : ""),
text: text, text: text,
created_at: note.createdAt.toISOString(), created_at: note.createdAt.toISOString(),
@ -135,13 +136,13 @@ export class NoteConverter {
// Use emojis list to provide URLs for emoji reactions. // Use emojis list to provide URLs for emoji reactions.
reactions: [], //FIXME: this.mapReactions(n.emojis, n.reactions, n.myReaction), reactions: [], //FIXME: this.mapReactions(n.emojis, n.reactions, n.myReaction),
bookmarked: isBookmarked, bookmarked: isBookmarked,
quote: Promise.resolve(renote).then(renote => recurse && renote && note.text !== null ? this.encode(renote, user, ctx, false) : null), quote: Promise.resolve(renote).then(renote => recurse && renote && note.text !== null ? this.encode(renote, ctx, false) : null),
edited_at: note.updatedAt?.toISOString() edited_at: note.updatedAt?.toISOString()
}); });
} }
public static async encodeMany(notes: Note[], user: ILocalUser | null, ctx: MastoContext): Promise<MastodonEntity.Status[]> { public static async encodeMany(notes: Note[], ctx: MastoContext): Promise<MastodonEntity.Status[]> {
const encoded = notes.map(n => this.encode(n, user, ctx)); const encoded = notes.map(n => this.encode(n, ctx));
return Promise.all(encoded); return Promise.all(encoded);
} }
} }

View file

@ -11,7 +11,8 @@ import { MastoContext } from "@/server/api/mastodon/index.js";
type NotificationType = typeof notificationTypes[number]; type NotificationType = typeof notificationTypes[number];
export class NotificationConverter { export class NotificationConverter {
public static async encode(notification: Notification, localUser: ILocalUser, ctx: MastoContext): Promise<MastodonEntity.Notification> { public static async encode(notification: Notification, ctx: MastoContext): Promise<MastodonEntity.Notification> {
const localUser = ctx.user as ILocalUser;
if (notification.notifieeId !== localUser.id) throw new Error('User is not recipient of notification'); if (notification.notifieeId !== localUser.id) throw new Error('User is not recipient of notification');
const account = notification.notifierId const account = notification.notifierId
@ -28,8 +29,8 @@ export class NotificationConverter {
if (notification.note) { if (notification.note) {
const isPureRenote = notification.note.renoteId !== null && notification.note.text === null; const isPureRenote = notification.note.renoteId !== null && notification.note.text === null;
const encodedNote = isPureRenote const encodedNote = isPureRenote
? getNote(notification.note.renoteId!, localUser).then(note => NoteConverter.encode(note, localUser, ctx)) ? getNote(notification.note.renoteId!, localUser).then(note => NoteConverter.encode(note, ctx))
: NoteConverter.encode(notification.note, localUser, ctx); : NoteConverter.encode(notification.note, ctx);
result = Object.assign(result, { result = Object.assign(result, {
status: encodedNote, status: encodedNote,
}); });
@ -45,8 +46,8 @@ export class NotificationConverter {
return awaitAll(result); return awaitAll(result);
} }
public static async encodeMany(notifications: Notification[], localUser: ILocalUser, ctx: MastoContext): Promise<MastodonEntity.Notification[]> { public static async encodeMany(notifications: Notification[], ctx: MastoContext): Promise<MastodonEntity.Notification[]> {
const encoded = notifications.map(u => this.encode(u, localUser, ctx)); const encoded = notifications.map(u => this.encode(u, ctx));
return Promise.all(encoded) return Promise.all(encoded)
.then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]); .then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]);
} }

View file

@ -6,22 +6,20 @@ import { UserConverter } from "@/server/api/mastodon/converters/user.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { ListHelpers } from "@/server/api/mastodon/helpers/list.js"; import { ListHelpers } from "@/server/api/mastodon/helpers/list.js";
import { Files } from "formidable";
import { auth } from "@/server/api/mastodon/middleware/auth.js"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
export function setupEndpointsAccount(router: Router): void { export function setupEndpointsAccount(router: Router): void {
router.get("/v1/accounts/verify_credentials", router.get("/v1/accounts/verify_credentials",
auth(true, ['read:accounts']), auth(true, ['read:accounts']),
async (ctx) => { async (ctx) => {
const acct = await UserHelpers.verifyCredentials(ctx.user, ctx); const acct = await UserHelpers.verifyCredentials(ctx);
ctx.body = convertAccountId(acct); ctx.body = convertAccountId(acct);
} }
); );
router.patch("/v1/accounts/update_credentials", router.patch("/v1/accounts/update_credentials",
auth(true, ['write:accounts']), auth(true, ['write:accounts']),
async (ctx) => { async (ctx) => {
const files = (ctx.request as any).files as Files | undefined; const acct = await UserHelpers.updateCredentials(ctx);
const acct = await UserHelpers.updateCredentials(ctx.user, (ctx.request as any).body as any, files, ctx);
ctx.body = convertAccountId(acct) ctx.body = convertAccountId(acct)
} }
); );
@ -57,8 +55,8 @@ export function setupEndpointsAccount(router: Router): void {
const userId = convertId(ctx.params.id, IdType.IceshrimpId); const userId = convertId(ctx.params.id, IdType.IceshrimpId);
const query = await UserHelpers.getUserCachedOr404(userId, ctx); const query = await UserHelpers.getUserCachedOr404(userId, ctx);
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query)))); const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
const res = 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); const res = await UserHelpers.getUserStatuses(query, args.max_id, args.since_id, args.min_id, args.limit, args['only_media'], args['exclude_replies'], args['exclude_reblogs'], args.pinned, args.tagged, ctx);
const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const tl = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -77,7 +75,7 @@ export function setupEndpointsAccount(router: Router): void {
const userId = convertId(ctx.params.id, IdType.IceshrimpId); const userId = convertId(ctx.params.id, IdType.IceshrimpId);
const query = await UserHelpers.getUserCachedOr404(userId, ctx); const query = await UserHelpers.getUserCachedOr404(userId, ctx);
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); 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 res = await UserHelpers.getUserFollowers(query, args.max_id, args.since_id, args.min_id, args.limit, ctx);
const followers = await UserConverter.encodeMany(res.data, ctx); const followers = await UserConverter.encodeMany(res.data, ctx);
ctx.body = followers.map((account) => convertAccountId(account)); ctx.body = followers.map((account) => convertAccountId(account));
@ -91,7 +89,7 @@ export function setupEndpointsAccount(router: Router): void {
const userId = convertId(ctx.params.id, IdType.IceshrimpId); const userId = convertId(ctx.params.id, IdType.IceshrimpId);
const query = await UserHelpers.getUserCachedOr404(userId, ctx); const query = await UserHelpers.getUserCachedOr404(userId, ctx);
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); 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 res = await UserHelpers.getUserFollowing(query, args.max_id, args.since_id, args.min_id, args.limit, ctx);
const following = await UserConverter.encodeMany(res.data, ctx); const following = await UserConverter.encodeMany(res.data, ctx);
ctx.body = following.map((account) => convertAccountId(account)); ctx.body = following.map((account) => convertAccountId(account));
@ -103,7 +101,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["read:lists"]), auth(true, ["read:lists"]),
async (ctx) => { async (ctx) => {
const member = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const member = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const results = await ListHelpers.getListsByMember(ctx.user, member); const results = await ListHelpers.getListsByMember(member, ctx);
ctx.body = results.map(p => convertListId(p)); ctx.body = results.map(p => convertListId(p));
}, },
); );
@ -113,7 +111,7 @@ export function setupEndpointsAccount(router: Router): void {
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
//FIXME: Parse form data //FIXME: Parse form data
const result = await UserHelpers.followUser(target, ctx.user, true, false); const result = await UserHelpers.followUser(target, true, false, ctx);
ctx.body = convertRelationshipId(result); ctx.body = convertRelationshipId(result);
}, },
); );
@ -122,7 +120,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["write:follows"]), auth(true, ["write:follows"]),
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.unfollowUser(target, ctx.user); const result = await UserHelpers.unfollowUser(target, ctx);
ctx.body = convertRelationshipId(result); ctx.body = convertRelationshipId(result);
}, },
); );
@ -131,7 +129,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["write:blocks"]), auth(true, ["write:blocks"]),
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.blockUser(target, ctx.user); const result = await UserHelpers.blockUser(target, ctx);
ctx.body = convertRelationshipId(result); ctx.body = convertRelationshipId(result);
}, },
); );
@ -140,7 +138,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["write:blocks"]), auth(true, ["write:blocks"]),
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.unblockUser(target, ctx.user); const result = await UserHelpers.unblockUser(target, ctx);
ctx.body = convertRelationshipId(result) ctx.body = convertRelationshipId(result)
}, },
); );
@ -151,7 +149,7 @@ export function setupEndpointsAccount(router: Router): void {
//FIXME: parse form data //FIXME: parse form data
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query, ['duration']), ['notifications'])); const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query, ['duration']), ['notifications']));
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.muteUser(target, ctx.user, args.notifications, args.duration); const result = await UserHelpers.muteUser(target, args.notifications, args.duration, ctx);
ctx.body = convertRelationshipId(result) ctx.body = convertRelationshipId(result)
}, },
); );
@ -160,7 +158,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["write:mutes"]), auth(true, ["write:mutes"]),
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.unmuteUser(target, ctx.user); const result = await UserHelpers.unmuteUser(target, ctx);
ctx.body = convertRelationshipId(result) ctx.body = convertRelationshipId(result)
}, },
); );
@ -178,8 +176,8 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["read:bookmarks"]), auth(true, ["read:bookmarks"]),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
const res = await UserHelpers.getUserBookmarks(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const res = await UserHelpers.getUserBookmarks(args.max_id, args.since_id, args.min_id, args.limit, ctx);
const bookmarks = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const bookmarks = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = bookmarks.map(s => convertStatusIds(s)); ctx.body = bookmarks.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
} }
@ -188,8 +186,8 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["read:favourites"]), auth(true, ["read:favourites"]),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
const res = await UserHelpers.getUserFavorites(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const res = await UserHelpers.getUserFavorites(args.max_id, args.since_id, args.min_id, args.limit, ctx);
const favorites = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const favorites = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = favorites.map(s => convertStatusIds(s)); ctx.body = favorites.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
} }
@ -198,7 +196,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["read:mutes"]), auth(true, ["read:mutes"]),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
const res = await UserHelpers.getUserMutes(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, ctx); const res = await UserHelpers.getUserMutes(args.max_id, args.since_id, args.min_id, args.limit, ctx);
ctx.body = res.data.map(m => convertAccountId(m)); ctx.body = res.data.map(m => convertAccountId(m));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
} }
@ -207,7 +205,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["read:blocks"]), auth(true, ["read:blocks"]),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
const res = await UserHelpers.getUserBlocks(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const res = await UserHelpers.getUserBlocks(args.max_id, args.since_id, args.min_id, args.limit, ctx);
const blocks = await UserConverter.encodeMany(res.data, ctx); const blocks = await UserConverter.encodeMany(res.data, ctx);
ctx.body = blocks.map(b => convertAccountId(b)); ctx.body = blocks.map(b => convertAccountId(b));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -217,7 +215,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["read:follows"]), auth(true, ["read:follows"]),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any)));
const res = await UserHelpers.getUserFollowRequests(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const res = await UserHelpers.getUserFollowRequests(args.max_id, args.since_id, args.min_id, args.limit, ctx);
const requests = await UserConverter.encodeMany(res.data, ctx); const requests = await UserConverter.encodeMany(res.data, ctx);
ctx.body = requests.map(b => convertAccountId(b)); ctx.body = requests.map(b => convertAccountId(b));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -228,7 +226,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["write:follows"]), auth(true, ["write:follows"]),
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.acceptFollowRequest(target, ctx.user); const result = await UserHelpers.acceptFollowRequest(target, ctx);
ctx.body = convertRelationshipId(result); ctx.body = convertRelationshipId(result);
}, },
); );
@ -237,7 +235,7 @@ export function setupEndpointsAccount(router: Router): void {
auth(true, ["write:follows"]), auth(true, ["write:follows"]),
async (ctx) => { async (ctx) => {
const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx); const target = await UserHelpers.getUserCachedOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
const result = await UserHelpers.rejectFollowRequest(target, ctx.user); const result = await UserHelpers.rejectFollowRequest(target, ctx);
ctx.body = convertRelationshipId(result); ctx.body = convertRelationshipId(result);
}, },
); );

View file

@ -14,7 +14,7 @@ export function setupEndpointsList(router: Router): void {
router.get("/v1/lists", router.get("/v1/lists",
auth(true, ['read:lists']), auth(true, ['read:lists']),
async (ctx, reply) => { async (ctx, reply) => {
ctx.body = await ListHelpers.getLists(ctx.user) ctx.body = await ListHelpers.getLists(ctx)
.then(p => p.map(list => convertListId(list))); .then(p => p.map(list => convertListId(list)));
} }
); );
@ -24,7 +24,7 @@ export function setupEndpointsList(router: Router): void {
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
ctx.body = await ListHelpers.getListOr404(ctx.user, id) ctx.body = await ListHelpers.getListOr404(id, ctx)
.then(p => convertListId(p)); .then(p => convertListId(p));
}, },
); );
@ -34,7 +34,7 @@ export function setupEndpointsList(router: Router): void {
const body = ctx.request.body as any; const body = ctx.request.body as any;
const title = (body.title ?? '').trim(); const title = (body.title ?? '').trim();
ctx.body = await ListHelpers.createList(ctx.user, title) ctx.body = await ListHelpers.createList(title, ctx)
.then(p => convertListId(p)); .then(p => convertListId(p));
} }
); );
@ -48,7 +48,7 @@ export function setupEndpointsList(router: Router): void {
const body = ctx.request.body as any; const body = ctx.request.body as any;
const title = (body.title ?? '').trim(); const title = (body.title ?? '').trim();
ctx.body = await ListHelpers.updateList(ctx.user, list, title) ctx.body = await ListHelpers.updateList(list, title, ctx)
.then(p => convertListId(p)); .then(p => convertListId(p));
}, },
); );
@ -60,7 +60,7 @@ export function setupEndpointsList(router: Router): void {
const list = await UserLists.findOneBy({ userId: ctx.user.id, id: id }); const list = await UserLists.findOneBy({ userId: ctx.user.id, id: id });
if (!list) throw new MastoApiError(404); if (!list) throw new MastoApiError(404);
await ListHelpers.deleteList(ctx.user, list); await ListHelpers.deleteList(list, ctx);
ctx.body = {}; ctx.body = {};
}, },
); );
@ -70,7 +70,7 @@ export function setupEndpointsList(router: Router): void {
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query))); 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 res = await ListHelpers.getListUsers(id, args.max_id, args.since_id, args.min_id, args.limit, ctx);
const accounts = await UserConverter.encodeMany(res.data, ctx); const accounts = await UserConverter.encodeMany(res.data, ctx);
ctx.body = accounts.map(account => convertAccountId(account)); ctx.body = accounts.map(account => convertAccountId(account));
@ -90,7 +90,7 @@ export function setupEndpointsList(router: Router): void {
const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId)); const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId));
const targets = await Promise.all(ids.map(p => getUser(p))); const targets = await Promise.all(ids.map(p => getUser(p)));
await ListHelpers.addToList(ctx.user, list, targets); await ListHelpers.addToList(list, targets, ctx);
ctx.body = {} ctx.body = {}
}, },
); );
@ -107,7 +107,7 @@ export function setupEndpointsList(router: Router): void {
const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId)); const ids = toArray(body['account_ids']).map(p => convertId(p, IdType.IceshrimpId));
const targets = await Promise.all(ids.map(p => getUser(p))); const targets = await Promise.all(ids.map(p => getUser(p)));
await ListHelpers.removeFromList(ctx.user, list, targets); await ListHelpers.removeFromList(list, targets, ctx);
ctx.body = {} ctx.body = {}
}, },
); );

View file

@ -3,8 +3,6 @@ import { convertId, IdType } from "@/misc/convert-id.js";
import { convertAttachmentId } from "@/server/api/mastodon/converters.js"; import { convertAttachmentId } from "@/server/api/mastodon/converters.js";
import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js"; import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js";
import { FileConverter } from "@/server/api/mastodon/converters/file.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"; import { auth } from "@/server/api/mastodon/middleware/auth.js";
export function setupEndpointsMedia(router: Router): void { export function setupEndpointsMedia(router: Router): void {
@ -12,7 +10,7 @@ export function setupEndpointsMedia(router: Router): void {
auth(true, ['write:media']), auth(true, ['write:media']),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const file = await MediaHelpers.getMediaPackedOr404(ctx.user, id); const file = await MediaHelpers.getMediaPackedOr404(id, ctx);
const attachment = FileConverter.encode(file); const attachment = FileConverter.encode(file);
ctx.body = convertAttachmentId(attachment); ctx.body = convertAttachmentId(attachment);
} }
@ -21,8 +19,8 @@ export function setupEndpointsMedia(router: Router): void {
auth(true, ['write:media']), auth(true, ['write:media']),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const file = await MediaHelpers.getMediaOr404(ctx.user, id); const file = await MediaHelpers.getMediaOr404(id, ctx);
const result = await MediaHelpers.updateMedia(ctx.user, file, ctx.request.body) const result = await MediaHelpers.updateMedia(file, ctx)
.then(p => FileConverter.encode(p)); .then(p => FileConverter.encode(p));
ctx.body = convertAttachmentId(result); ctx.body = convertAttachmentId(result);
} }
@ -30,10 +28,7 @@ export function setupEndpointsMedia(router: Router): void {
router.post(["/v2/media", "/v1/media"], router.post(["/v2/media", "/v1/media"],
auth(true, ['write:media']), auth(true, ['write:media']),
async (ctx) => { async (ctx) => {
//FIXME: why do we have to cast this to any first? const result = await MediaHelpers.uploadMedia(ctx)
const files = (ctx.request as any).files as Files | undefined;
const file = toSingleLast(files?.file);
const result = await MediaHelpers.uploadMedia(ctx.user, file, ctx.request.body)
.then(p => FileConverter.encode(p)); .then(p => FileConverter.encode(p));
ctx.body = convertAttachmentId(result); ctx.body = convertAttachmentId(result);
} }

View file

@ -24,7 +24,7 @@ export function setupEndpointsMisc(router: Router): void {
auth(true), auth(true),
async (ctx) => { async (ctx) => {
const args = argsToBools(ctx.query, ['with_dismissed']); const args = argsToBools(ctx.query, ['with_dismissed']);
ctx.body = await MiscHelpers.getAnnouncements(ctx.user, args['with_dismissed']) ctx.body = await MiscHelpers.getAnnouncements(args['with_dismissed'], ctx)
.then(p => p.map(x => convertAnnouncementId(x))); .then(p => p.map(x => convertAnnouncementId(x)));
} }
); );
@ -37,7 +37,7 @@ export function setupEndpointsMisc(router: Router): void {
const announcement = await Announcements.findOneBy({ id: id }); const announcement = await Announcements.findOneBy({ id: id });
if (!announcement) throw new MastoApiError(404); if (!announcement) throw new MastoApiError(404);
await MiscHelpers.dismissAnnouncement(announcement, ctx.user); await MiscHelpers.dismissAnnouncement(announcement, ctx);
ctx.body = {}; ctx.body = {};
}, },
); );
@ -68,7 +68,7 @@ export function setupEndpointsMisc(router: Router): void {
router.get("/v1/preferences", router.get("/v1/preferences",
auth(true, ['read:accounts']), auth(true, ['read:accounts']),
async (ctx) => { async (ctx) => {
ctx.body = await MiscHelpers.getPreferences(ctx.user); ctx.body = await MiscHelpers.getPreferences(ctx);
} }
); );
@ -76,7 +76,7 @@ export function setupEndpointsMisc(router: Router): void {
auth(true, ['read']), auth(true, ['read']),
async (ctx) => { async (ctx) => {
const args = limitToInt(ctx.query); const args = limitToInt(ctx.query);
ctx.body = await MiscHelpers.getFollowSuggestions(ctx.user, args.limit, ctx) ctx.body = await MiscHelpers.getFollowSuggestions(args.limit, ctx)
.then(p => p.map(x => convertSuggestionIds(x))); .then(p => p.map(x => convertSuggestionIds(x)));
} }
); );

View file

@ -11,8 +11,8 @@ export function setupEndpointsNotifications(router: Router): void {
auth(true, ['read:notifications']), auth(true, ['read:notifications']),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']);
const res = await NotificationHelpers.getNotifications(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id); const res = await NotificationHelpers.getNotifications(args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id, ctx);
const data = await NotificationConverter.encodeMany(res.data, ctx.user, ctx); const data = await NotificationConverter.encodeMany(res.data, ctx);
ctx.body = data.map(n => convertNotificationIds(n)); ctx.body = data.map(n => convertNotificationIds(n));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -22,15 +22,15 @@ export function setupEndpointsNotifications(router: Router): void {
router.get("/v1/notifications/:id", router.get("/v1/notifications/:id",
auth(true, ['read:notifications']), auth(true, ['read:notifications']),
async (ctx) => { async (ctx) => {
const notification = await NotificationHelpers.getNotificationOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx.user); const notification = await NotificationHelpers.getNotificationOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
ctx.body = convertNotificationIds(await NotificationConverter.encode(notification, ctx.user, ctx)); ctx.body = convertNotificationIds(await NotificationConverter.encode(notification, ctx));
} }
); );
router.post("/v1/notifications/clear", router.post("/v1/notifications/clear",
auth(true, ['write:notifications']), auth(true, ['write:notifications']),
async (ctx) => { async (ctx) => {
await NotificationHelpers.clearAllNotifications(ctx.user); await NotificationHelpers.clearAllNotifications(ctx);
ctx.body = {}; ctx.body = {};
} }
); );
@ -38,8 +38,8 @@ export function setupEndpointsNotifications(router: Router): void {
router.post("/v1/notifications/:id/dismiss", router.post("/v1/notifications/:id/dismiss",
auth(true, ['write:notifications']), auth(true, ['write:notifications']),
async (ctx) => { async (ctx) => {
const notification = await NotificationHelpers.getNotificationOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx.user); const notification = await NotificationHelpers.getNotificationOr404(convertId(ctx.params.id, IdType.IceshrimpId), ctx);
await NotificationHelpers.dismissNotification(notification.id, ctx.user); await NotificationHelpers.dismissNotification(notification.id, ctx);
ctx.body = {}; ctx.body = {};
} }
); );
@ -48,7 +48,7 @@ export function setupEndpointsNotifications(router: Router): void {
auth(true, ['write:conversations']), auth(true, ['write:conversations']),
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
await NotificationHelpers.markConversationAsRead(id, ctx.user); await NotificationHelpers.markConversationAsRead(id, ctx);
ctx.body = {}; ctx.body = {};
} }
); );

View file

@ -9,7 +9,7 @@ export function setupEndpointsSearch(router: Router): void {
auth(true, ['read:search']), auth(true, ['read:search']),
async (ctx) => { async (ctx) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed']))); const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
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, ctx); const result = await SearchHelpers.search(args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, ctx);
ctx.body = convertSearchIds(result); ctx.body = convertSearchIds(result);

View file

@ -20,7 +20,7 @@ export function setupEndpointsStatus(router: Router): void {
router.post("/v1/statuses", router.post("/v1/statuses",
auth(true, ['write:statuses']), auth(true, ['write:statuses']),
async (ctx) => { async (ctx) => {
const key = NoteHelpers.getIdempotencyKey(ctx.headers, ctx.user); const key = NoteHelpers.getIdempotencyKey(ctx);
if (key !== null) { if (key !== null) {
const result = await NoteHelpers.getFromIdempotencyCache(key); const result = await NoteHelpers.getFromIdempotencyCache(key);
@ -31,8 +31,8 @@ export function setupEndpointsStatus(router: Router): void {
} }
let request = NoteHelpers.normalizeComposeOptions(ctx.request.body); let request = NoteHelpers.normalizeComposeOptions(ctx.request.body);
ctx.body = await NoteHelpers.createNote(request, ctx.user) ctx.body = await NoteHelpers.createNote(request, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
if (key !== null) NoteHelpers.postIdempotencyCache.set(key, { status: ctx.body }); if (key !== null) NoteHelpers.postIdempotencyCache.set(key, { status: ctx.body });
@ -42,10 +42,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ['write:statuses']), auth(true, ['write:statuses']),
async (ctx) => { async (ctx) => {
const noteId = convertId(ctx.params.id, IdType.IceshrimpId); const noteId = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(noteId, ctx.user); const note = await NoteHelpers.getNoteOr404(noteId, ctx);
let request = NoteHelpers.normalizeEditOptions(ctx.request.body); let request = NoteHelpers.normalizeEditOptions(ctx.request.body);
ctx.body = await NoteHelpers.editNote(request, note, ctx.user) ctx.body = await NoteHelpers.editNote(request, note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
} }
); );
@ -53,9 +53,9 @@ export function setupEndpointsStatus(router: Router): void {
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
async (ctx) => { async (ctx) => {
const noteId = convertId(ctx.params.id, IdType.IceshrimpId); const noteId = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(noteId, ctx.user); const note = await NoteHelpers.getNoteOr404(noteId, ctx);
const status = await NoteConverter.encode(note, ctx.user, ctx); const status = await NoteConverter.encode(note, ctx);
ctx.body = convertStatusIds(status); ctx.body = convertStatusIds(status);
} }
); );
@ -63,8 +63,8 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ['write:statuses']), auth(true, ['write:statuses']),
async (ctx) => { async (ctx) => {
const noteId = convertId(ctx.params.id, IdType.IceshrimpId); const noteId = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(noteId, ctx.user); const note = await NoteHelpers.getNoteOr404(noteId, ctx);
ctx.body = await NoteHelpers.deleteNote(note, ctx.user, ctx) ctx.body = await NoteHelpers.deleteNote(note, ctx)
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
} }
); );
@ -73,13 +73,14 @@ export function setupEndpointsStatus(router: Router): void {
"/v1/statuses/:id/context", "/v1/statuses/:id/context",
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
async (ctx) => { async (ctx) => {
//FIXME: determine final limits within helper functions instead of here
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const ancestors = await NoteHelpers.getNoteAncestors(note, ctx.user, ctx.user ? 4096 : 60) const ancestors = await NoteHelpers.getNoteAncestors(note, ctx.user ? 4096 : 60, ctx)
.then(n => NoteConverter.encodeMany(n, ctx.user, ctx)) .then(n => NoteConverter.encodeMany(n, ctx))
.then(n => n.map(s => convertStatusIds(s))); .then(n => n.map(s => convertStatusIds(s)));
const descendants = await NoteHelpers.getNoteDescendants(note, ctx.user, ctx.user ? 4096 : 40, ctx.user ? 4096 : 20) const descendants = await NoteHelpers.getNoteDescendants(note, ctx.user ? 4096 : 40, ctx.user ? 4096 : 20, ctx)
.then(n => NoteConverter.encodeMany(n, ctx.user, ctx)) .then(n => NoteConverter.encodeMany(n, ctx))
.then(n => n.map(s => convertStatusIds(s))); .then(n => n.map(s => convertStatusIds(s)));
ctx.body = { ctx.body = {
@ -93,7 +94,7 @@ export function setupEndpointsStatus(router: Router): void {
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const res = await NoteHelpers.getNoteEditHistory(note, ctx); const res = await NoteHelpers.getNoteEditHistory(note, ctx);
ctx.body = res.map(p => convertStatusEditIds(p)); ctx.body = res.map(p => convertStatusEditIds(p));
} }
@ -103,7 +104,7 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["read:statuses"]), auth(true, ["read:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const src = NoteHelpers.getNoteSource(note); const src = NoteHelpers.getNoteSource(note);
ctx.body = convertStatusSourceId(src); ctx.body = convertStatusSourceId(src);
} }
@ -113,9 +114,9 @@ export function setupEndpointsStatus(router: Router): void {
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); 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 res = await NoteHelpers.getNoteRebloggedBy(note, args.max_id, args.since_id, args.min_id, args.limit, ctx);
const users = await UserConverter.encodeMany(res.data, ctx); const users = await UserConverter.encodeMany(res.data, ctx);
ctx.body = users.map(m => convertAccountId(m)); ctx.body = users.map(m => convertAccountId(m));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -126,7 +127,7 @@ export function setupEndpointsStatus(router: Router): void {
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query as any))); 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 res = await NoteHelpers.getNoteFavoritedBy(note, args.max_id, args.since_id, args.min_id, args.limit);
const users = await UserConverter.encodeMany(res.data, ctx); const users = await UserConverter.encodeMany(res.data, ctx);
@ -139,11 +140,11 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:favourites"]), auth(true, ["write:favourites"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const reaction = await NoteHelpers.getDefaultReaction(); const reaction = await NoteHelpers.getDefaultReaction();
ctx.body = await NoteHelpers.reactToNote(note, ctx.user, reaction) ctx.body = await NoteHelpers.reactToNote(note, reaction, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
} }
); );
@ -152,10 +153,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:favourites"]), auth(true, ["write:favourites"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.removeReactFromNote(note, ctx.user) ctx.body = await NoteHelpers.removeReactFromNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -165,10 +166,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:statuses"]), auth(true, ["write:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.reblogNote(note, ctx.user) ctx.body = await NoteHelpers.reblogNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -178,10 +179,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:statuses"]), auth(true, ["write:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.unreblogNote(note, ctx.user) ctx.body = await NoteHelpers.unreblogNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -191,10 +192,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:bookmarks"]), auth(true, ["write:bookmarks"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.bookmarkNote(note, ctx.user) ctx.body = await NoteHelpers.bookmarkNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -204,10 +205,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:bookmarks"]), auth(true, ["write:bookmarks"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.unbookmarkNote(note, ctx.user) ctx.body = await NoteHelpers.unbookmarkNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -217,10 +218,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:accounts"]), auth(true, ["write:accounts"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.pinNote(note, ctx.user) ctx.body = await NoteHelpers.pinNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -230,10 +231,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:accounts"]), auth(true, ["write:accounts"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.unpinNote(note, ctx.user) ctx.body = await NoteHelpers.unpinNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -243,10 +244,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:favourites"]), auth(true, ["write:favourites"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.reactToNote(note, ctx.user, ctx.params.name) ctx.body = await NoteHelpers.reactToNote(note, ctx.params.name, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -256,10 +257,10 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:favourites"]), auth(true, ["write:favourites"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
ctx.body = await NoteHelpers.removeReactFromNote(note, ctx.user) ctx.body = await NoteHelpers.removeReactFromNote(note, ctx)
.then(p => NoteConverter.encode(p, ctx.user, ctx)) .then(p => NoteConverter.encode(p, ctx))
.then(p => convertStatusIds(p)); .then(p => convertStatusIds(p));
}, },
); );
@ -267,8 +268,8 @@ export function setupEndpointsStatus(router: Router): void {
auth(false, ["read:statuses"]), auth(false, ["read:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const data = await PollHelpers.getPoll(note, ctx.user, ctx); const data = await PollHelpers.getPoll(note, ctx);
ctx.body = convertPollId(data); ctx.body = convertPollId(data);
}); });
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
@ -276,13 +277,13 @@ export function setupEndpointsStatus(router: Router): void {
auth(true, ["write:statuses"]), auth(true, ["write:statuses"]),
async (ctx) => { async (ctx) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx);
const body: any = ctx.request.body; const body: any = ctx.request.body;
const choices = toArray(body.choices ?? []).map(p => parseInt(p)); const choices = toArray(body.choices ?? []).map(p => parseInt(p));
if (choices.length < 1) throw new MastoApiError(400, "Must vote for at least one option"); if (choices.length < 1) throw new MastoApiError(400, "Must vote for at least one option");
const data = await PollHelpers.voteInPoll(choices, note, ctx.user, ctx); const data = await PollHelpers.voteInPoll(choices, note, ctx);
ctx.body = convertPollId(data); ctx.body = convertPollId(data);
}, },
); );

View file

@ -67,8 +67,8 @@ export function setupEndpointsTimeline(router: Router): void {
auth(true, ['read:statuses']), auth(true, ['read:statuses']),
async (ctx, reply) => { async (ctx, reply) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query)))); const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
const res = await TimelineHelpers.getPublicTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote); const res = await TimelineHelpers.getPublicTimeline(args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote, ctx);
const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const tl = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -80,7 +80,7 @@ export function setupEndpointsTimeline(router: Router): void {
const tag = (ctx.params.hashtag ?? '').trim(); const tag = (ctx.params.hashtag ?? '').trim();
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))), ['any[]', 'all[]', 'none[]']); const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))), ['any[]', 'all[]', 'none[]']);
const res = 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); const res = 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);
const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const tl = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -90,8 +90,8 @@ export function setupEndpointsTimeline(router: Router): void {
auth(true, ['read:statuses']), auth(true, ['read:statuses']),
async (ctx, reply) => { async (ctx, reply) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
const res = await TimelineHelpers.getHomeTimeline(ctx.user, args.max_id, args.since_id, args.min_id, args.limit); const res = await TimelineHelpers.getHomeTimeline(args.max_id, args.since_id, args.min_id, args.limit, ctx);
const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const tl = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -105,8 +105,8 @@ export function setupEndpointsTimeline(router: Router): void {
if (!list) throw new MastoApiError(404); if (!list) throw new MastoApiError(404);
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query))); const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
const res = await TimelineHelpers.getListTimeline(ctx.user, list, args.max_id, args.since_id, args.min_id, args.limit); const res = await TimelineHelpers.getListTimeline(list, args.max_id, args.since_id, args.min_id, args.limit, ctx);
const tl = await NoteConverter.encodeMany(res.data, ctx.user, ctx); const tl = await NoteConverter.encodeMany(res.data, ctx);
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;
@ -116,7 +116,7 @@ export function setupEndpointsTimeline(router: Router): void {
auth(true, ['read:statuses']), auth(true, ['read:statuses']),
async (ctx, reply) => { async (ctx, reply) => {
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query))); 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); const res = await TimelineHelpers.getConversations(args.max_id, args.since_id, args.min_id, args.limit, ctx);
ctx.body = res.data.map(c => convertConversationIds(c)); ctx.body = res.data.map(c => convertConversationIds(c));
ctx.pagination = res.pagination; ctx.pagination = res.pagination;

View file

@ -7,9 +7,12 @@ import { pushUserToUserList } from "@/services/user-list/push.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { publishUserListStream } from "@/services/stream.js"; import { publishUserListStream } from "@/services/stream.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { MastoContext } from "@/server/api/mastodon/index.js";
export class ListHelpers { export class ListHelpers {
public static async getLists(user: ILocalUser): Promise<MastodonEntity.List[]> { public static async getLists(ctx: MastoContext): Promise<MastodonEntity.List[]> {
const user = ctx.user as ILocalUser;
return UserLists.findBy({ userId: user.id }).then(p => p.map(list => { return UserLists.findBy({ userId: user.id }).then(p => p.map(list => {
return { return {
id: list.id, id: list.id,
@ -18,7 +21,9 @@ export class ListHelpers {
})); }));
} }
public static async getList(user: ILocalUser, id: string): Promise<MastodonEntity.List> { public static async getList(id: string, ctx: MastoContext): Promise<MastodonEntity.List> {
const user = ctx.user as ILocalUser;
return UserLists.findOneByOrFail({ userId: user.id, id: id }).then(list => { return UserLists.findOneByOrFail({ userId: user.id, id: id }).then(list => {
return { return {
id: list.id, id: list.id,
@ -27,14 +32,15 @@ export class ListHelpers {
}); });
} }
public static async getListOr404(user: ILocalUser, id: string): Promise<MastodonEntity.List> { public static async getListOr404(id: string, ctx: MastoContext): Promise<MastodonEntity.List> {
return this.getList(user, id).catch(_ => { return this.getList(id, ctx).catch(_ => {
throw new MastoApiError(404); 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[]>> { public static async getListUsers(id: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const user = ctx.user as ILocalUser;
const list = await UserLists.findOneBy({ userId: user.id, id: id }); const list = await UserLists.findOneBy({ userId: user.id, id: id });
if (!list) throw new MastoApiError(404); if (!list) throw new MastoApiError(404);
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
@ -59,12 +65,14 @@ export class ListHelpers {
}); });
} }
public static async deleteList(user: ILocalUser, list: UserList) { public static async deleteList(list: UserList, ctx: MastoContext) {
const user = ctx.user as ILocalUser;
if (user.id != list.userId) throw new Error("List is not owned by user"); if (user.id != list.userId) throw new Error("List is not owned by user");
await UserLists.delete(list.id); await UserLists.delete(list.id);
} }
public static async addToList(localUser: ILocalUser, list: UserList, usersToAdd: User[]) { public static async addToList(list: UserList, usersToAdd: User[], ctx: MastoContext) {
const localUser = ctx.user as ILocalUser;
if (localUser.id != list.userId) throw new Error("List is not owned by user"); if (localUser.id != list.userId) throw new Error("List is not owned by user");
for (const user of usersToAdd) { for (const user of usersToAdd) {
if (user.id !== localUser.id) { if (user.id !== localUser.id) {
@ -89,7 +97,8 @@ export class ListHelpers {
} }
} }
public static async removeFromList(localUser: ILocalUser, list: UserList, usersToRemove: User[]) { public static async removeFromList(list: UserList, usersToRemove: User[], ctx: MastoContext) {
const localUser = ctx.user as ILocalUser;
if (localUser.id != list.userId) throw new Error("List is not owned by user"); if (localUser.id != list.userId) throw new Error("List is not owned by user");
for (const user of usersToRemove) { for (const user of usersToRemove) {
const exist = await UserListJoinings.exist({ const exist = await UserListJoinings.exist({
@ -105,9 +114,10 @@ export class ListHelpers {
} }
} }
public static async createList(user: ILocalUser, title: string): Promise<MastodonEntity.List> { public static async createList(title: string, ctx: MastoContext): Promise<MastodonEntity.List> {
if (title.length < 1) throw new MastoApiError(400, "Title must not be empty"); if (title.length < 1) throw new MastoApiError(400, "Title must not be empty");
const user = ctx.user as ILocalUser;
const list = await UserLists.insert({ const list = await UserLists.insert({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
@ -121,8 +131,10 @@ export class ListHelpers {
}; };
} }
public static async updateList(user: ILocalUser, list: UserList, title: string) { public static async updateList(list: UserList, title: string, ctx: MastoContext) {
if (title.length < 1) throw new MastoApiError(400, "Title must not be empty"); if (title.length < 1) throw new MastoApiError(400, "Title must not be empty");
const user = ctx.user as ILocalUser;
if (user.id != list.userId) throw new Error("List is not owned by user"); if (user.id != list.userId) throw new Error("List is not owned by user");
const partial = { name: title }; const partial = { name: title };
@ -135,7 +147,8 @@ export class ListHelpers {
}; };
} }
public static async getListsByMember(user: ILocalUser, member: User): Promise<MastodonEntity.List[]> { public static async getListsByMember(member: User, ctx: MastoContext): Promise<MastodonEntity.List[]> {
const user = ctx.user as ILocalUser;
const joinQuery = UserListJoinings.createQueryBuilder('member') const joinQuery = UserListJoinings.createQueryBuilder('member')
.select("member.userListId") .select("member.userListId")
.where("member.userId = :memberId"); .where("member.userId = :memberId");

View file

@ -3,11 +3,18 @@ import { ILocalUser } from "@/models/entities/user.js";
import { DriveFiles } from "@/models/index.js"; import { DriveFiles } from "@/models/index.js";
import { Packed } from "@/misc/schema.js"; import { Packed } from "@/misc/schema.js";
import { DriveFile } from "@/models/entities/drive-file.js"; import { DriveFile } from "@/models/entities/drive-file.js";
import { File } from "formidable"; import { File, Files } from "formidable";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { MastoContext } from "@/server/api/mastodon/index.js";
import { toSingleLast } from "@/prelude/array.js";
export class MediaHelpers { export class MediaHelpers {
public static async uploadMedia(user: ILocalUser, file: File | undefined, body: any): Promise<Packed<"DriveFile">> { public static async uploadMedia(ctx: MastoContext): Promise<Packed<"DriveFile">> {
const files = ctx.request.files as Files | undefined;
const file = toSingleLast(files?.file);
const user = ctx.user as ILocalUser
const body = ctx.request.body as any;
if (!file) throw new MastoApiError(400, "Validation failed: File content type is invalid, File is invalid"); if (!file) throw new MastoApiError(400, "Validation failed: File content type is invalid, File is invalid");
return addFile({ return addFile({
@ -20,7 +27,9 @@ export class MediaHelpers {
.then(p => DriveFiles.pack(p)); .then(p => DriveFiles.pack(p));
} }
public static async uploadMediaBasic(user: ILocalUser, file: File): Promise<DriveFile> { public static async uploadMediaBasic(file: File, ctx: MastoContext): Promise<DriveFile> {
const user = ctx.user as ILocalUser;
return addFile({ return addFile({
user: user, user: user,
path: file.filepath, path: file.filepath,
@ -29,7 +38,10 @@ export class MediaHelpers {
}) })
} }
public static async updateMedia(user: ILocalUser, file: DriveFile, body: any): Promise<Packed<"DriveFile">> { public static async updateMedia(file: DriveFile, ctx: MastoContext): Promise<Packed<"DriveFile">> {
const user = ctx.user as ILocalUser;
const body = ctx.request.body as any;
await DriveFiles.update(file.id, { await DriveFiles.update(file.id, {
comment: body?.description ?? undefined comment: body?.description ?? undefined
}); });
@ -38,24 +50,26 @@ export class MediaHelpers {
.then(p => DriveFiles.pack(p)); .then(p => DriveFiles.pack(p));
} }
public static async getMediaPacked(user: ILocalUser, id: string): Promise<Packed<"DriveFile"> | null> { public static async getMediaPacked(id: string, ctx: MastoContext): Promise<Packed<"DriveFile"> | null> {
return this.getMedia(user, id) const user = ctx.user as ILocalUser;
return this.getMedia(id, ctx)
.then(p => p ? DriveFiles.pack(p) : null); .then(p => p ? DriveFiles.pack(p) : null);
} }
public static async getMediaPackedOr404(user: ILocalUser, id: string): Promise<Packed<"DriveFile">> { public static async getMediaPackedOr404(id: string, ctx: MastoContext): Promise<Packed<"DriveFile">> {
return this.getMediaPacked(user, id).then(p => { return this.getMediaPacked(id, ctx).then(p => {
if (p) return p; if (p) return p;
throw new MastoApiError(404); throw new MastoApiError(404);
}); });
} }
public static async getMedia(user: ILocalUser, id: string): Promise<DriveFile | null> { public static async getMedia(id: string, ctx: MastoContext): Promise<DriveFile | null> {
const user = ctx.user as ILocalUser;
return DriveFiles.findOneBy({ id: id, userId: user.id }); return DriveFiles.findOneBy({ id: id, userId: user.id });
} }
public static async getMediaOr404(user: ILocalUser, id: string): Promise<DriveFile> { public static async getMediaOr404(id: string, ctx: MastoContext): Promise<DriveFile> {
return this.getMedia(user, id).then(p => { return this.getMedia(id, ctx).then(p => {
if (p) return p; if (p) return p;
throw new MastoApiError(404); throw new MastoApiError(404);
}); });

View file

@ -96,7 +96,9 @@ export class MiscHelpers {
return awaitAll(res); return awaitAll(res);
} }
public static async getAnnouncements(user: ILocalUser, includeRead: boolean = false): Promise<MastodonEntity.Announcement[]> { public static async getAnnouncements(includeRead: boolean = false, ctx: MastoContext): Promise<MastodonEntity.Announcement[]> {
const user = ctx.user as ILocalUser;
if (includeRead) { if (includeRead) {
const [announcements, reads] = await Promise.all([ const [announcements, reads] = await Promise.all([
Announcements.createQueryBuilder("announcement") Announcements.createQueryBuilder("announcement")
@ -122,7 +124,8 @@ export class MiscHelpers {
.then(p => p.map(x => AnnouncementConverter.encode(x, false))); .then(p => p.map(x => AnnouncementConverter.encode(x, false)));
} }
public static async dismissAnnouncement(announcement: Announcement, user: ILocalUser): Promise<void> { public static async dismissAnnouncement(announcement: Announcement, ctx: MastoContext): Promise<void> {
const user = ctx.user as ILocalUser;
const exists = await AnnouncementReads.exist({ where: { userId: user.id, announcementId: announcement.id } }); const exists = await AnnouncementReads.exist({ where: { userId: user.id, announcementId: announcement.id } });
if (!exists) { if (!exists) {
await AnnouncementReads.insert({ await AnnouncementReads.insert({
@ -134,7 +137,8 @@ export class MiscHelpers {
} }
} }
public static async getFollowSuggestions(user: ILocalUser, limit: number, ctx: MastoContext): Promise<MastodonEntity.SuggestedAccount[]> { public static async getFollowSuggestions(limit: number, ctx: MastoContext): Promise<MastodonEntity.SuggestedAccount[]> {
const user = ctx.user as ILocalUser;
const results: Promise<MastodonEntity.SuggestedAccount[]>[] = []; const results: Promise<MastodonEntity.SuggestedAccount[]>[] = [];
const pinned = fetchMeta().then(meta => Promise.all( const pinned = fetchMeta().then(meta => Promise.all(
@ -220,7 +224,7 @@ export class MiscHelpers {
.skip(offset) .skip(offset)
.take(limit) .take(limit)
.getMany() .getMany()
.then(result => NoteConverter.encodeMany(result, null, ctx)); .then(result => NoteConverter.encodeMany(result, ctx));
} }
public static async getTrendingHashtags(limit: number = 10, offset: number = 0): Promise<MastodonEntity.Tag[]> { public static async getTrendingHashtags(limit: number = 10, offset: number = 0): Promise<MastodonEntity.Tag[]> {
@ -229,11 +233,12 @@ export class MiscHelpers {
//FIXME: This was already implemented in api/endpoints/hashtags/trend.ts, but the implementation is sketchy at best. Rewrite from scratch. //FIXME: This was already implemented in api/endpoints/hashtags/trend.ts, but the implementation is sketchy at best. Rewrite from scratch.
} }
public static getPreferences(user: ILocalUser): Promise<MastodonEntity.Preferences> { public static getPreferences(ctx: MastoContext): Promise<MastodonEntity.Preferences> {
const user = ctx.user as ILocalUser;
const profile = UserProfiles.findOneByOrFail({ userId: user.id }); const profile = UserProfiles.findOneByOrFail({ userId: user.id });
const sensitive = profile.then(p => p.alwaysMarkNsfw); const sensitive = profile.then(p => p.alwaysMarkNsfw);
const language = profile.then(p => p.lang); const language = profile.then(p => p.lang);
const privacy = UserHelpers.getDefaultNoteVisibility(user) const privacy = UserHelpers.getDefaultNoteVisibility(ctx)
.then(p => VisibilityConverter.encode(p)); .then(p => VisibilityConverter.encode(p));
const res = { const res = {

View file

@ -47,7 +47,8 @@ export class NoteHelpers {
}); });
} }
public static async reactToNote(note: Note, user: ILocalUser, reaction: string): Promise<Note> { public static async reactToNote(note: Note, reaction: string, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
await createReaction(user, note, reaction).catch(e => { await createReaction(user, note, reaction).catch(e => {
if (e instanceof IdentifiableError && e.id == '51c42bb4-931a-456b-bff7-e5a8a70dd298') return; if (e instanceof IdentifiableError && e.id == '51c42bb4-931a-456b-bff7-e5a8a70dd298') return;
throw e; throw e;
@ -55,12 +56,14 @@ export class NoteHelpers {
return getNote(note.id, user); return getNote(note.id, user);
} }
public static async removeReactFromNote(note: Note, user: ILocalUser): Promise<Note> { public static async removeReactFromNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
await deleteReaction(user, note); await deleteReaction(user, note);
return getNote(note.id, user); return getNote(note.id, user);
} }
public static async reblogNote(note: Note, user: ILocalUser): Promise<Note> { public static async reblogNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
const existingRenote = await Notes.findOneBy({ const existingRenote = await Notes.findOneBy({
userId: user.id, userId: user.id,
renoteId: note.id, renoteId: note.id,
@ -75,7 +78,8 @@ export class NoteHelpers {
return await createNote(user, data); return await createNote(user, data);
} }
public static async unreblogNote(note: Note, user: ILocalUser): Promise<Note> { public static async unreblogNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
return Notes.findBy({ return Notes.findBy({
userId: user.id, userId: user.id,
renoteId: note.id, renoteId: note.id,
@ -85,7 +89,8 @@ export class NoteHelpers {
.then(_ => getNote(note.id, user)); .then(_ => getNote(note.id, user));
} }
public static async bookmarkNote(note: Note, user: ILocalUser): Promise<Note> { public static async bookmarkNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
const bookmarked = await NoteFavorites.exist({ const bookmarked = await NoteFavorites.exist({
where: { where: {
noteId: note.id, noteId: note.id,
@ -105,7 +110,8 @@ export class NoteHelpers {
return note; return note;
} }
public static async unbookmarkNote(note: Note, user: ILocalUser): Promise<Note> { public static async unbookmarkNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
return NoteFavorites.findOneBy({ return NoteFavorites.findOneBy({
noteId: note.id, noteId: note.id,
userId: user.id, userId: user.id,
@ -114,7 +120,8 @@ export class NoteHelpers {
.then(_ => note); .then(_ => note);
} }
public static async pinNote(note: Note, user: ILocalUser): Promise<Note> { public static async pinNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
const pinned = await UserNotePinings.exist({ const pinned = await UserNotePinings.exist({
where: { where: {
userId: user.id, userId: user.id,
@ -129,7 +136,8 @@ export class NoteHelpers {
return note; return note;
} }
public static async unpinNote(note: Note, user: ILocalUser): Promise<Note> { public static async unpinNote(note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
const pinned = await UserNotePinings.exist({ const pinned = await UserNotePinings.exist({
where: { where: {
userId: user.id, userId: user.id,
@ -144,9 +152,10 @@ export class NoteHelpers {
return note; return note;
} }
public static async deleteNote(note: Note, user: ILocalUser, ctx: MastoContext): Promise<MastodonEntity.Status> { public static async deleteNote(note: Note, ctx: MastoContext): Promise<MastodonEntity.Status> {
const user = ctx.user as ILocalUser;
if (user.id !== note.userId) throw new MastoApiError(404); if (user.id !== note.userId) throw new MastoApiError(404);
const status = await NoteConverter.encode(note, user, ctx); const status = await NoteConverter.encode(note, ctx);
await deleteNote(user, note); await deleteNote(user, note);
status.content = undefined; status.content = undefined;
return status; return status;
@ -222,8 +231,9 @@ export class NoteHelpers {
} }
} }
public static async getNoteRebloggedBy(note: Note, user: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> { public static async getNoteRebloggedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const user = ctx.user as ILocalUser | null;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
Notes.createQueryBuilder("note"), Notes.createQueryBuilder("note"),
sinceId, sinceId,
@ -249,7 +259,8 @@ export class NoteHelpers {
}); });
} }
public static async getNoteDescendants(note: Note | string, user: ILocalUser | null, limit: number = 10, depth: number = 2): Promise<Note[]> { public static async getNoteDescendants(note: Note | string, limit: number = 10, depth: number = 2, ctx: MastoContext): Promise<Note[]> {
const user = ctx.user as ILocalUser | null;
const noteId = typeof note === "string" ? note : note.id; const noteId = typeof note === "string" ? note : note.id;
const query = makePaginationQuery(Notes.createQueryBuilder("note")) const query = makePaginationQuery(Notes.createQueryBuilder("note"))
.andWhere( .andWhere(
@ -266,7 +277,8 @@ export class NoteHelpers {
return query.getMany().then(p => p.reverse()); return query.getMany().then(p => p.reverse());
} }
public static async getNoteAncestors(rootNote: Note, user: ILocalUser | null, limit: number = 10): Promise<Note[]> { public static async getNoteAncestors(rootNote: Note, limit: number = 10, ctx: MastoContext): Promise<Note[]> {
const user = ctx.user as ILocalUser | null;
const notes = new Array<Note>; const notes = new Array<Note>;
for (let i = 0; i < limit; i++) { for (let i = 0; i < limit; i++) {
const currentNote = notes.at(-1) ?? rootNote; const currentNote = notes.at(-1) ?? rootNote;
@ -282,13 +294,14 @@ export class NoteHelpers {
return notes.reverse(); return notes.reverse();
} }
public static async createNote(request: MastodonEntity.StatusCreationRequest, user: ILocalUser): Promise<Note> { public static async createNote(request: MastodonEntity.StatusCreationRequest, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
const files = request.media_ids && request.media_ids.length > 0 const files = request.media_ids && request.media_ids.length > 0
? DriveFiles.findByIds(request.media_ids) ? DriveFiles.findByIds(request.media_ids)
: []; : [];
const reply = request.in_reply_to_id ? await getNote(request.in_reply_to_id, user) : undefined; const reply = request.in_reply_to_id ? await getNote(request.in_reply_to_id, user) : undefined;
const visibility = request.visibility ?? UserHelpers.getDefaultNoteVisibility(user); const visibility = request.visibility ?? UserHelpers.getDefaultNoteVisibility(ctx);
const data = { const data = {
createdAt: new Date(), createdAt: new Date(),
@ -304,13 +317,14 @@ export class NoteHelpers {
reply: reply, reply: reply,
cw: request.spoiler_text, cw: request.spoiler_text,
visibility: visibility, visibility: visibility,
visibleUsers: Promise.resolve(visibility).then(p => p === 'specified' ? this.extractMentions(request.text ?? '', user) : undefined) visibleUsers: Promise.resolve(visibility).then(p => p === 'specified' ? this.extractMentions(request.text ?? '', ctx) : undefined)
} }
return createNote(user, await awaitAll(data)); return createNote(user, await awaitAll(data));
} }
public static async editNote(request: MastodonEntity.StatusEditRequest, note: Note, user: ILocalUser): Promise<Note> { public static async editNote(request: MastodonEntity.StatusEditRequest, note: Note, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser;
const files = request.media_ids && request.media_ids.length > 0 const files = request.media_ids && request.media_ids.length > 0
? DriveFiles.findByIds(request.media_ids) ? DriveFiles.findByIds(request.media_ids)
: []; : [];
@ -331,7 +345,8 @@ export class NoteHelpers {
return editNote(user, note, await awaitAll(data)); return editNote(user, note, await awaitAll(data));
} }
public static async extractMentions(text: string, user: ILocalUser): Promise<User[]> { public static async extractMentions(text: string, ctx: MastoContext): Promise<User[]> {
const user = ctx.user as ILocalUser;
return extractMentionedUsers(user, mfm.parse(text)!); return extractMentionedUsers(user, mfm.parse(text)!);
} }
@ -397,13 +412,16 @@ export class NoteHelpers {
return result; return result;
} }
public static async getNoteOr404(id: string, user: ILocalUser | null): Promise<Note> { public static async getNoteOr404(id: string, ctx: MastoContext): Promise<Note> {
const user = ctx.user as ILocalUser | null;
return getNote(id, user).catch(_ => { return getNote(id, user).catch(_ => {
throw new MastoApiError(404); throw new MastoApiError(404);
}); });
} }
public static getIdempotencyKey(headers: any, user: ILocalUser): string | null { public static getIdempotencyKey(ctx: MastoContext): string | null {
const headers = ctx.headers;
const user = ctx.user as ILocalUser;
if (headers["idempotency-key"] === undefined || headers["idempotency-key"] === null) return 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"]}`; return `${user.id}-${Array.isArray(headers["idempotency-key"]) ? headers["idempotency-key"].at(-1)! : headers["idempotency-key"]}`;
} }

View file

@ -4,11 +4,13 @@ import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
import { Notification } from "@/models/entities/notification.js"; import { Notification } from "@/models/entities/notification.js";
import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js"; import { LinkPaginationObject } from "@/server/api/mastodon/middleware/pagination.js";
import { MastoContext } from "@/server/api/mastodon/index.js";
export class NotificationHelpers { 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<LinkPaginationObject<Notification[]>> { public static async getNotifications(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined, ctx: MastoContext): Promise<LinkPaginationObject<Notification[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const user = ctx.user as ILocalUser;
let requestedTypes = types let requestedTypes = types
? this.decodeTypes(types) ? this.decodeTypes(types)
: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest']; : ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest'];
@ -35,26 +37,30 @@ export class NotificationHelpers {
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined);
} }
public static async getNotification(id: string, user: ILocalUser): Promise<Notification | null> { public static async getNotification(id: string, ctx: MastoContext): Promise<Notification | null> {
const user = ctx.user as ILocalUser;
return Notifications.findOneBy({ id: id, notifieeId: user.id }); return Notifications.findOneBy({ id: id, notifieeId: user.id });
} }
public static async getNotificationOr404(id: string, user: ILocalUser): Promise<Notification> { public static async getNotificationOr404(id: string, ctx: MastoContext): Promise<Notification> {
return this.getNotification(id, user).then(p => { return this.getNotification(id, ctx).then(p => {
if (p) return p; if (p) return p;
throw new MastoApiError(404); throw new MastoApiError(404);
}); });
} }
public static async dismissNotification(id: string, user: ILocalUser): Promise<void> { public static async dismissNotification(id: string, ctx: MastoContext): Promise<void> {
const result = await Notifications.update({ id: id, notifieeId: user.id }, { isRead: true }); const user = ctx.user as ILocalUser;
await Notifications.update({ id: id, notifieeId: user.id }, { isRead: true });
} }
public static async clearAllNotifications(user: ILocalUser): Promise<void> { public static async clearAllNotifications(ctx: MastoContext): Promise<void> {
const user = ctx.user as ILocalUser;
await Notifications.update({ notifieeId: user.id }, { isRead: true }); await Notifications.update({ notifieeId: user.id }, { isRead: true });
} }
public static async markConversationAsRead(id: string, user: ILocalUser): Promise<void> { public static async markConversationAsRead(id: string, ctx: MastoContext): Promise<void> {
const user = ctx.user as ILocalUser;
const notesQuery = Notes.createQueryBuilder("note") const notesQuery = Notes.createQueryBuilder("note")
.select("note.id") .select("note.id")
.andWhere("COALESCE(note.threadId, note.id) = :conversationId"); .andWhere("COALESCE(note.threadId, note.id) = :conversationId");

View file

@ -17,7 +17,8 @@ import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { MastoContext } from "@/server/api/mastodon/index.js"; import { MastoContext } from "@/server/api/mastodon/index.js";
export class PollHelpers { export class PollHelpers {
public static async getPoll(note: Note, user: ILocalUser | null, ctx: MastoContext): Promise<MastodonEntity.Poll> { public static async getPoll(note: Note, ctx: MastoContext): Promise<MastodonEntity.Poll> {
const user = ctx.user as ILocalUser | null;
if (!await Notes.isVisibleForMe(note, user?.id ?? null)) if (!await Notes.isVisibleForMe(note, user?.id ?? null))
throw new Error('Cannot encode poll not visible for user'); throw new Error('Cannot encode poll not visible for user');
@ -32,8 +33,9 @@ export class PollHelpers {
return populatePoll(note, user?.id ?? null).then(p => PollConverter.encode(p, note.id, noteEmoji)); return populatePoll(note, user?.id ?? null).then(p => PollConverter.encode(p, note.id, noteEmoji));
} }
public static async voteInPoll(choices: number[], note: Note, user: ILocalUser, ctx: MastoContext): Promise<MastodonEntity.Poll> { public static async voteInPoll(choices: number[], note: Note, ctx: MastoContext): Promise<MastodonEntity.Poll> {
if (!note.hasPoll) throw new MastoApiError(404); if (!note.hasPoll) throw new MastoApiError(404);
const user = ctx.user as ILocalUser;
for (const choice of choices) { for (const choice of choices) {
const createdAt = new Date(); const createdAt = new Date();
@ -123,6 +125,6 @@ export class PollHelpers {
); );
} }
} }
return this.getPoll(note, user, ctx); return this.getPoll(note, ctx);
} }
} }

View file

@ -24,15 +24,16 @@ import config from "@/config/index.js";
import { MastoContext } from "@/server/api/mastodon/index.js"; import { MastoContext } from "@/server/api/mastodon/index.js";
export class SearchHelpers { export class SearchHelpers {
public static async search(user: ILocalUser, q: string | undefined, type: string | undefined, resolve: boolean = false, following: boolean = false, accountId: string | undefined, excludeUnreviewed: boolean = false, maxId: string | undefined, minId: string | undefined, limit: number = 20, offset: number | undefined, ctx: MastoContext): Promise<MastodonEntity.Search> { public static async search(q: string | undefined, type: string | undefined, resolve: boolean = false, following: boolean = false, accountId: string | undefined, excludeUnreviewed: boolean = false, maxId: string | undefined, minId: string | undefined, limit: number = 20, offset: number | undefined, ctx: MastoContext): Promise<MastodonEntity.Search> {
if (q === undefined || q.trim().length === 0) throw new Error('Search query cannot be empty'); if (q === undefined || q.trim().length === 0) throw new Error('Search query cannot be empty');
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const notes = type === 'statuses' || !type ? this.searchNotes(user, q, resolve, following, accountId, maxId, minId, limit, offset) : []; const user = ctx.user as ILocalUser;
const users = type === 'accounts' || !type ? this.searchUsers(user, q, resolve, following, maxId, minId, limit, offset) : []; const notes = type === 'statuses' || !type ? this.searchNotes(q, resolve, following, accountId, maxId, minId, limit, offset, ctx) : [];
const users = type === 'accounts' || !type ? this.searchUsers(q, resolve, following, maxId, minId, limit, offset, ctx) : [];
const tags = type === 'hashtags' || !type ? this.searchTags(q, excludeUnreviewed, limit, offset) : []; const tags = type === 'hashtags' || !type ? this.searchTags(q, excludeUnreviewed, limit, offset) : [];
const result = { const result = {
statuses: Promise.resolve(notes).then(p => NoteConverter.encodeMany(p, user, ctx)), statuses: Promise.resolve(notes).then(p => NoteConverter.encodeMany(p, ctx)),
accounts: Promise.resolve(users).then(p => UserConverter.encodeMany(p, ctx)), accounts: Promise.resolve(users).then(p => UserConverter.encodeMany(p, ctx)),
hashtags: Promise.resolve(tags) hashtags: Promise.resolve(tags)
}; };
@ -40,7 +41,8 @@ export class SearchHelpers {
return awaitAll(result); return awaitAll(result);
} }
private static async searchUsers(user: ILocalUser, q: string, resolve: boolean, following: boolean, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined): Promise<User[]> { private static async searchUsers(q: string, resolve: boolean, following: boolean, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined, ctx: MastoContext): Promise<User[]> {
const user = ctx.user as ILocalUser;
if (resolve) { if (resolve) {
try { try {
if (q.startsWith('https://') || q.startsWith('http://')) { if (q.startsWith('https://') || q.startsWith('http://')) {
@ -113,8 +115,9 @@ export class SearchHelpers {
return query.skip(offset ?? 0).take(limit).getMany().then(p => minId ? p.reverse() : p); return query.skip(offset ?? 0).take(limit).getMany().then(p => minId ? p.reverse() : p);
} }
private static async searchNotes(user: ILocalUser, q: string, resolve: boolean, following: boolean, accountId: string | undefined, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined): Promise<Note[]> { private static async searchNotes(q: string, resolve: boolean, following: boolean, accountId: string | undefined, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined, ctx: MastoContext): Promise<Note[]> {
if (accountId && following) throw new Error("The 'following' and 'accountId' parameters cannot be used simultaneously"); if (accountId && following) throw new Error("The 'following' and 'accountId' parameters cannot be used simultaneously");
const user = ctx.user as ILocalUser;
if (resolve) { if (resolve) {
try { try {

View file

@ -22,8 +22,9 @@ import { generatePaginationData, LinkPaginationObject } from "@/server/api/masto
import { MastoContext } from "@/server/api/mastodon/index.js"; import { MastoContext } from "@/server/api/mastodon/index.js";
export class TimelineHelpers { export class TimelineHelpers {
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> { public static async getHomeTimeline(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const user = ctx.user as ILocalUser;
const followingQuery = Followings.createQueryBuilder("following") const followingQuery = Followings.createQueryBuilder("following")
.select("following.followeeId") .select("following.followeeId")
@ -56,8 +57,9 @@ export class TimelineHelpers {
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined);
} }
public static async getPublicTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise<LinkPaginationObject<Note[]>> { public static async getPublicTimeline(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const user = ctx.user as ILocalUser;
if (local && remote) { if (local && remote) {
throw new Error("local and remote are mutually exclusive options"); throw new Error("local and remote are mutually exclusive options");
@ -99,8 +101,9 @@ export class TimelineHelpers {
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined);
} }
public static async getListTimeline(user: ILocalUser, list: UserList, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> { public static async getListTimeline(list: UserList, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const user = ctx.user as ILocalUser;
if (user.id != list.userId) throw new Error("List is not owned by user"); if (user.id != list.userId) throw new Error("List is not owned by user");
const listQuery = UserListJoinings.createQueryBuilder("member") const listQuery = UserListJoinings.createQueryBuilder("member")
@ -123,8 +126,9 @@ export class TimelineHelpers {
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined);
} }
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<LinkPaginationObject<Note[]>> { public static async getTagTimeline(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, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const user = ctx.user as ILocalUser | null;
if (tag.length < 1) throw new MastoApiError(400, "Tag cannot be empty"); if (tag.length < 1) throw new MastoApiError(400, "Tag cannot be empty");
@ -164,8 +168,9 @@ export class TimelineHelpers {
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined);
} }
public static async getConversations(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<LinkPaginationObject<MastodonEntity.Conversation[]>> { public static async getConversations(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<LinkPaginationObject<MastodonEntity.Conversation[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const user = ctx.user as ILocalUser;
const sq = Notes.createQueryBuilder("note") const sq = Notes.createQueryBuilder("note")
.select("COALESCE(note.threadId, note.id)", "conversationId") .select("COALESCE(note.threadId, note.id)", "conversationId")
.addSelect("note.id", "latest") .addSelect("note.id", "latest")
@ -207,7 +212,7 @@ export class TimelineHelpers {
return { return {
id: c.threadId ?? c.id, id: c.threadId ?? c.id,
accounts: accounts.then(u => u.length > 0 ? u : UserConverter.encodeMany([user], ctx)), // failsafe to prevent apps from crashing case when all participant users have been deleted accounts: accounts.then(u => u.length > 0 ? u : UserConverter.encodeMany([user], ctx)), // failsafe to prevent apps from crashing case when all participant users have been deleted
last_status: NoteConverter.encode(c, user, ctx), last_status: NoteConverter.encode(c, ctx),
unread: unread unread: unread
} }
}); });

View file

@ -61,8 +61,9 @@ export type updateCredsData = {
type RelationshipType = 'followers' | 'following'; type RelationshipType = 'followers' | 'following';
export class UserHelpers { export class UserHelpers {
public static async followUser(target: User, localUser: ILocalUser, reblogs: boolean, notify: boolean): Promise<MastodonEntity.Relationship> { public static async followUser(target: User, reblogs: boolean, notify: boolean, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
//FIXME: implement reblogs & notify params //FIXME: implement reblogs & notify params
const localUser = ctx.user as ILocalUser;
const following = await Followings.exist({ where: { followerId: localUser.id, followeeId: target.id } }); const following = await Followings.exist({ where: { followerId: localUser.id, followeeId: target.id } });
const requested = await FollowRequests.exist({ where: { followerId: localUser.id, followeeId: target.id } }); const requested = await FollowRequests.exist({ where: { followerId: localUser.id, followeeId: target.id } });
if (!following && !requested) if (!following && !requested)
@ -71,7 +72,8 @@ export class UserHelpers {
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async unfollowUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async unfollowUser(target: User, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
const localUser = ctx.user as ILocalUser;
const following = await Followings.exist({ where: { followerId: localUser.id, followeeId: target.id } }); const following = await Followings.exist({ where: { followerId: localUser.id, followeeId: target.id } });
const requested = await FollowRequests.exist({ where: { followerId: localUser.id, followeeId: target.id } }); const requested = await FollowRequests.exist({ where: { followerId: localUser.id, followeeId: target.id } });
if (following) if (following)
@ -82,7 +84,8 @@ export class UserHelpers {
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async blockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async blockUser(target: User, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
const localUser = ctx.user as ILocalUser;
const blocked = await Blockings.exist({ where: { blockerId: localUser.id, blockeeId: target.id } }); const blocked = await Blockings.exist({ where: { blockerId: localUser.id, blockeeId: target.id } });
if (!blocked) if (!blocked)
await createBlocking(localUser, target); await createBlocking(localUser, target);
@ -90,7 +93,8 @@ export class UserHelpers {
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async unblockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async unblockUser(target: User, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
const localUser = ctx.user as ILocalUser;
const blocked = await Blockings.exist({ where: { blockerId: localUser.id, blockeeId: target.id } }); const blocked = await Blockings.exist({ where: { blockerId: localUser.id, blockeeId: target.id } });
if (blocked) if (blocked)
await deleteBlocking(localUser, target); await deleteBlocking(localUser, target);
@ -98,8 +102,9 @@ export class UserHelpers {
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async muteUser(target: User, localUser: ILocalUser, notifications: boolean = true, duration: number = 0): Promise<MastodonEntity.Relationship> { public static async muteUser(target: User, notifications: boolean = true, duration: number = 0, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
//FIXME: respect notifications parameter //FIXME: respect notifications parameter
const localUser = ctx.user as ILocalUser;
const muted = await Mutings.exist({ where: { muterId: localUser.id, muteeId: target.id } }); const muted = await Mutings.exist({ where: { muterId: localUser.id, muteeId: target.id } });
if (!muted) { if (!muted) {
await Mutings.insert({ await Mutings.insert({
@ -121,7 +126,8 @@ export class UserHelpers {
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async unmuteUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async unmuteUser(target: User, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
const localUser = ctx.user as ILocalUser;
const muting = await Mutings.findOneBy({ muterId: localUser.id, muteeId: target.id }); const muting = await Mutings.findOneBy({ muterId: localUser.id, muteeId: target.id });
if (muting) { if (muting) {
await Mutings.delete({ await Mutings.delete({
@ -134,21 +140,27 @@ export class UserHelpers {
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async acceptFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async acceptFollowRequest(target: User, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
const localUser = ctx.user as ILocalUser;
const pending = await FollowRequests.exist({ where: { followerId: target.id, followeeId: localUser.id } }); const pending = await FollowRequests.exist({ where: { followerId: target.id, followeeId: localUser.id } });
if (pending) if (pending)
await acceptFollowRequest(localUser, target); await acceptFollowRequest(localUser, target);
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async rejectFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async rejectFollowRequest(target: User, ctx: MastoContext): Promise<MastodonEntity.Relationship> {
const localUser = ctx.user as ILocalUser;
const pending = await FollowRequests.exist({ where: { followerId: target.id, followeeId: localUser.id } }); const pending = await FollowRequests.exist({ where: { followerId: target.id, followeeId: localUser.id } });
if (pending) if (pending)
await rejectFollowRequest(localUser, target); await rejectFollowRequest(localUser, target);
return this.getUserRelationshipTo(target.id, localUser.id); return this.getUserRelationshipTo(target.id, localUser.id);
} }
public static async updateCredentials(user: ILocalUser, formData: updateCredsData, files: Files | undefined, ctx: MastoContext): Promise<MastodonEntity.Account> { public static async updateCredentials(ctx: MastoContext): Promise<MastodonEntity.Account> {
const user = ctx.user as ILocalUser;
const files = (ctx.request as any).files as Files | undefined;
const formData = (ctx.request as any).body as updateCredsData;
const updates: Partial<User> = {}; const updates: Partial<User> = {};
const profileUpdates: Partial<UserProfile> = {}; const profileUpdates: Partial<UserProfile> = {};
@ -156,12 +168,12 @@ export class UserHelpers {
const header = toSingleLast(files?.header); const header = toSingleLast(files?.header);
if (avatar) { if (avatar) {
const file = await MediaHelpers.uploadMediaBasic(user, avatar); const file = await MediaHelpers.uploadMediaBasic(avatar, ctx);
updates.avatarId = file.id; updates.avatarId = file.id;
} }
if (header) { if (header) {
const file = await MediaHelpers.uploadMediaBasic(user, header); const file = await MediaHelpers.uploadMediaBasic(header, ctx);
updates.bannerId = file.id; updates.bannerId = file.id;
} }
@ -184,13 +196,14 @@ export class UserHelpers {
if (Object.keys(updates).length > 0) await Users.update(user.id, updates); if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profileUpdates).length > 0) await UserProfiles.update({ userId: user.id }, profileUpdates); if (Object.keys(profileUpdates).length > 0) await UserProfiles.update({ userId: user.id }, profileUpdates);
return this.verifyCredentials(user, ctx); return this.verifyCredentials(ctx);
} }
public static async verifyCredentials(user: ILocalUser, ctx: MastoContext): Promise<MastodonEntity.Account> { public static async verifyCredentials(ctx: MastoContext): Promise<MastodonEntity.Account> {
const user = ctx.user as ILocalUser;
const acct = UserConverter.encode(user, ctx); const acct = UserConverter.encode(user, ctx);
const profile = UserProfiles.findOneByOrFail({ userId: user.id }); const profile = UserProfiles.findOneByOrFail({ userId: user.id });
const privacy = this.getDefaultNoteVisibility(user); const privacy = this.getDefaultNoteVisibility(ctx);
const fields = profile.then(profile => profile.fields.map(field => { const fields = profile.then(profile => profile.fields.map(field => {
return { return {
name: field.name, name: field.name,
@ -225,9 +238,10 @@ export class UserHelpers {
}); });
} }
public static async getUserMutes(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<MastodonEntity.MutedAccount[]>> { public static async getUserMutes(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<MastodonEntity.MutedAccount[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const user = ctx.user as ILocalUser;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
Mutings.createQueryBuilder("muting"), Mutings.createQueryBuilder("muting"),
sinceId, sinceId,
@ -260,9 +274,10 @@ export class UserHelpers {
}); });
} }
public static async getUserBlocks(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> { public static async getUserBlocks(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const user = ctx.user as ILocalUser;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
Blockings.createQueryBuilder("blocking"), Blockings.createQueryBuilder("blocking"),
sinceId, sinceId,
@ -286,9 +301,10 @@ export class UserHelpers {
}); });
} }
public static async getUserFollowRequests(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> { public static async getUserFollowRequests(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const user = ctx.user as ILocalUser;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
FollowRequests.createQueryBuilder("request"), FollowRequests.createQueryBuilder("request"),
sinceId, sinceId,
@ -312,8 +328,9 @@ export class UserHelpers {
}); });
} }
public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise<LinkPaginationObject<Note[]>> { public static async getUserStatuses(user: User, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const localUser = ctx.user as ILocalUser | null;
if (tagged !== undefined) { if (tagged !== undefined) {
//FIXME respect tagged //FIXME respect tagged
@ -373,9 +390,10 @@ export class UserHelpers {
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined); return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined);
} }
public static async getUserBookmarks(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> { public static async getUserBookmarks(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const localUser = ctx.user as ILocalUser;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
NoteFavorites.createQueryBuilder("favorite"), NoteFavorites.createQueryBuilder("favorite"),
sinceId, sinceId,
@ -396,9 +414,10 @@ export class UserHelpers {
}); });
} }
public static async getUserFavorites(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> { public static async getUserFavorites(maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, ctx: MastoContext): Promise<LinkPaginationObject<Note[]>> {
if (limit > 40) limit = 40; if (limit > 40) limit = 40;
const localUser = ctx.user as ILocalUser;
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
NoteReactions.createQueryBuilder("reaction"), NoteReactions.createQueryBuilder("reaction"),
sinceId, sinceId,
@ -419,9 +438,10 @@ export class UserHelpers {
}); });
} }
private static async getUserRelationships(type: RelationshipType, user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> { private static async getUserRelationships(type: RelationshipType, user: User, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
const localUser = ctx.user as ILocalUser | null;
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === "private") { if (profile.ffVisibility === "private") {
if (!localUser || user.id !== localUser.id) return { data: [] }; if (!localUser || user.id !== localUser.id) return { data: [] };
@ -463,12 +483,12 @@ export class UserHelpers {
}); });
} }
public static async getUserFollowers(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> { public static async getUserFollowers(user: User, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
return this.getUserRelationships('followers', user, localUser, maxId, sinceId, minId, limit); return this.getUserRelationships('followers', user, maxId, sinceId, minId, limit, ctx);
} }
public static async getUserFollowing(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> { public static async getUserFollowing(user: User, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, ctx: MastoContext): Promise<LinkPaginationObject<User[]>> {
return this.getUserRelationships('following', user, localUser, maxId, sinceId, minId, limit); return this.getUserRelationships('following', user, maxId, sinceId, minId, limit, ctx);
} }
public static async getUserRelationhipToMany(targetIds: string[], localUserId: string): Promise<MastodonEntity.Relationship[]> { public static async getUserRelationhipToMany(targetIds: string[], localUserId: string): Promise<MastodonEntity.Relationship[]> {
@ -528,7 +548,8 @@ export class UserHelpers {
}; };
} }
public static async getDefaultNoteVisibility(user: ILocalUser): Promise<IceshrimpVisibility> { public static async getDefaultNoteVisibility(ctx: MastoContext): Promise<IceshrimpVisibility> {
const user = ctx.user as ILocalUser;
return RegistryItems.findOneBy({ return RegistryItems.findOneBy({
domain: IsNull(), domain: IsNull(),
userId: user.id, userId: user.id,