[mastodon-client] Code cleanup & reformat

This commit is contained in:
Laura Hausmann 2023-10-06 03:12:52 +02:00
parent 8bc7bf373e
commit afd9e236a3
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
21 changed files with 168 additions and 157 deletions

View file

@ -38,7 +38,7 @@ export class NoteConverter {
host, host,
)); ));
const reactionCount = NoteReactions.countBy({noteId: note.id}); const reactionCount = NoteReactions.countBy({ noteId: note.id });
const reaction = user ? NoteReactions.findOneBy({ const reaction = user ? NoteReactions.findOneBy({
userId: user.id, userId: user.id,
@ -87,7 +87,7 @@ export class NoteConverter {
}); });
const isPinned = user && note.userId === user.id const isPinned = user && note.userId === user.id
? UserNotePinings.exist({where: {userId: user.id, noteId: note.id}}) ? UserNotePinings.exist({ where: { userId: user.id, noteId: note.id } })
: undefined; : undefined;
const tags = note.tags.map(tag => { const tags = note.tags.map(tag => {

View file

@ -27,14 +27,14 @@ export class UserConverter {
acct = `${u.username}@${u.host}`; acct = `${u.username}@${u.host}`;
acctUrl = `https://${u.host}/@${u.username}`; acctUrl = `https://${u.host}/@${u.username}`;
} }
const profile = UserProfiles.findOneBy({userId: u.id}); const profile = UserProfiles.findOneBy({ userId: u.id });
const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? "")) ?? escapeMFM(profile?.description ?? "")); const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? "")) ?? escapeMFM(profile?.description ?? ""));
const avatar = u.avatarId const avatar = u.avatarId
? (DriveFiles.findOneBy({id: u.avatarId})) ? (DriveFiles.findOneBy({ id: u.avatarId }))
.then(p => p?.url ?? Users.getIdenticonUrl(u.id)) .then(p => p?.url ?? Users.getIdenticonUrl(u.id))
: Users.getIdenticonUrl(u.id); : Users.getIdenticonUrl(u.id);
const banner = u.bannerId const banner = u.bannerId
? (DriveFiles.findOneBy({id: u.bannerId})) ? (DriveFiles.findOneBy({ id: u.bannerId }))
.then(p => p?.url ?? `${config.url}/static-assets/transparent.png`) .then(p => p?.url ?? `${config.url}/static-assets/transparent.png`)
: `${config.url}/static-assets/transparent.png`; : `${config.url}/static-assets/transparent.png`;
const followersCount = profile.then(profile => { const followersCount = profile.then(profile => {

View file

@ -11,7 +11,7 @@ export function setupEndpointsFilter(router: Router): void {
router.post(["/v1/filters", "/v2/filters"], router.post(["/v1/filters", "/v2/filters"],
auth(true, ['write:filters']), auth(true, ['write:filters']),
async (ctx) => { async (ctx) => {
ctx.status = 400; ctx.status = 400;
ctx.body = { error: "Please change word mute settings in the web frontend settings." }; ctx.body = { error: "Please change word mute settings in the web frontend settings." };
} }
); );

View file

@ -44,7 +44,7 @@ export function setupEndpointsList(router: Router): void {
auth(true, ['write:lists']), auth(true, ['write:lists']),
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
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);
const body = ctx.request.body as any; const body = ctx.request.body as any;
@ -58,7 +58,7 @@ export function setupEndpointsList(router: Router): void {
auth(true, ['write:lists']), auth(true, ['write:lists']),
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
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(ctx.user, list);
@ -83,7 +83,7 @@ export function setupEndpointsList(router: Router): void {
auth(true, ['write:lists']), auth(true, ['write:lists']),
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
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);
const body = ctx.request.body as any; const body = ctx.request.body as any;
@ -100,7 +100,7 @@ export function setupEndpointsList(router: Router): void {
auth(true, ['write:lists']), auth(true, ['write:lists']),
async (ctx, reply) => { async (ctx, reply) => {
const id = convertId(ctx.params.id, IdType.IceshrimpId); const id = convertId(ctx.params.id, IdType.IceshrimpId);
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);
const body = ctx.request.body as any; const body = ctx.request.body as any;

View file

@ -34,7 +34,7 @@ export function setupEndpointsMisc(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 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.user);

View file

@ -1,6 +1,12 @@
import Router from "@koa/router"; import Router from "@koa/router";
import { convertId, IdType } from "../../index.js"; import { convertId, IdType } from "../../index.js";
import { convertAccountId, convertPollId, convertStatusIds, convertStatusEditIds, convertStatusSourceId, } from "../converters.js"; import {
convertAccountId,
convertPollId,
convertStatusEditIds,
convertStatusIds,
convertStatusSourceId,
} from "../converters.js";
import { NoteConverter } from "@/server/api/mastodon/converters/note.js"; import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js"; import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js"; import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js";
@ -29,7 +35,7 @@ export function setupEndpointsStatus(router: Router): void {
.then(p => NoteConverter.encode(p, ctx.user)) .then(p => NoteConverter.encode(p, ctx.user))
.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 });
} }
); );
router.put("/v1/statuses/:id", router.put("/v1/statuses/:id",
@ -264,7 +270,7 @@ export function setupEndpointsStatus(router: Router): void {
const note = await NoteHelpers.getNoteOr404(id, ctx.user); const note = await NoteHelpers.getNoteOr404(id, ctx.user);
const data = await PollHelpers.getPoll(note, ctx.user); const data = await PollHelpers.getPoll(note, ctx.user);
ctx.body = convertPollId(data); ctx.body = convertPollId(data);
}); });
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/polls/:id/votes", "/v1/polls/:id/votes",
auth(true, ["write:statuses"]), auth(true, ["write:statuses"]),
@ -276,7 +282,7 @@ export function setupEndpointsStatus(router: Router): void {
const choices = toArray(body.choices ?? []).map(p => parseInt(p)); const choices = toArray(body.choices ?? []).map(p => parseInt(p));
if (choices.length < 1) { if (choices.length < 1) {
ctx.status = 400; ctx.status = 400;
ctx.body = {error: 'Must vote for at least one option'}; ctx.body = { error: 'Must vote for at least one option' };
return; return;
} }

View file

@ -74,7 +74,7 @@ export function setupEndpointsTimeline(router: Router): void {
.then(n => NoteConverter.encodeMany(n, ctx.user, cache)); .then(n => NoteConverter.encodeMany(n, ctx.user, cache));
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
}); });
router.get<{ Params: { hashtag: string } }>( router.get<{ Params: { hashtag: string } }>(
"/v1/timelines/tag/:hashtag", "/v1/timelines/tag/:hashtag",
auth(false, ['read:statuses']), auth(false, ['read:statuses']),
@ -97,13 +97,13 @@ export function setupEndpointsTimeline(router: Router): void {
.then(n => NoteConverter.encodeMany(n, ctx.user, cache)); .then(n => NoteConverter.encodeMany(n, ctx.user, cache));
ctx.body = tl.map(s => convertStatusIds(s)); ctx.body = tl.map(s => convertStatusIds(s));
}); });
router.get<{ Params: { listId: string } }>( router.get<{ Params: { listId: string } }>(
"/v1/timelines/list/:listId", "/v1/timelines/list/:listId",
auth(true, ['read:lists']), auth(true, ['read:lists']),
async (ctx, reply) => { async (ctx, reply) => {
const listId = convertId(ctx.params.listId, IdType.IceshrimpId); const listId = convertId(ctx.params.listId, IdType.IceshrimpId);
const list = await UserLists.findOneBy({userId: ctx.user.id, id: listId}); const list = await UserLists.findOneBy({ userId: ctx.user.id, id: listId });
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)));

View file

@ -10,7 +10,7 @@ import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js"
export class ListHelpers { export class ListHelpers {
public static async getLists(user: ILocalUser): Promise<MastodonEntity.List[]> { public static async getLists(user: ILocalUser): Promise<MastodonEntity.List[]> {
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,
title: list.name title: list.name
@ -19,7 +19,7 @@ export class ListHelpers {
} }
public static async getList(user: ILocalUser, id: string): Promise<MastodonEntity.List> { public static async getList(user: ILocalUser, id: string): Promise<MastodonEntity.List> {
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,
title: list.name title: list.name
@ -32,9 +32,10 @@ export class ListHelpers {
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(user: ILocalUser, id: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
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(
UserListJoinings.createQueryBuilder('member'), UserListJoinings.createQueryBuilder('member'),
@ -42,7 +43,7 @@ export class ListHelpers {
maxId, maxId,
minId minId
) )
.andWhere("member.userListId = :listId", {listId: list.id}) .andWhere("member.userListId = :listId", { listId: list.id })
.innerJoinAndSelect("member.user", "user"); .innerJoinAndSelect("member.user", "user");
return query.take(limit).getMany().then(async p => { return query.take(limit).getMany().then(async p => {
@ -125,9 +126,9 @@ export class ListHelpers {
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");
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 };
const result = await UserLists.update(list.id, partial) const result = await UserLists.update(list.id, partial)
.then(async _ => await UserLists.findOneByOrFail({id: list.id})); .then(async _ => await UserLists.findOneByOrFail({ id: list.id }));
return { return {
id: result.id, id: result.id,
@ -140,9 +141,9 @@ export class ListHelpers {
.select("member.userListId") .select("member.userListId")
.where("member.userId = :memberId"); .where("member.userId = :memberId");
const query = UserLists.createQueryBuilder('list') const query = UserLists.createQueryBuilder('list')
.where("list.userId = :userId", {userId: user.id}) .where("list.userId = :userId", { userId: user.id })
.andWhere(`list.id IN (${joinQuery.getQuery()})`) .andWhere(`list.id IN (${joinQuery.getQuery()})`)
.setParameters({memberId: member.id}); .setParameters({ memberId: member.id });
return query.getMany() return query.getMany()
.then(results => results.map(result => { .then(results => results.map(result => {

View file

@ -34,7 +34,7 @@ export class MediaHelpers {
comment: body?.description ?? undefined comment: body?.description ?? undefined
}); });
return DriveFiles.findOneByOrFail({id: file.id, userId: user.id}) return DriveFiles.findOneByOrFail({ id: file.id, userId: user.id })
.then(p => DriveFiles.pack(p)); .then(p => DriveFiles.pack(p));
} }
@ -51,13 +51,13 @@ export class MediaHelpers {
} }
public static async getMedia(user: ILocalUser, id: string): Promise<DriveFile | null> { public static async getMedia(user: ILocalUser, id: string): Promise<DriveFile | null> {
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(user: ILocalUser, id: string): Promise<DriveFile> {
return this.getMedia(user, id).then(p => { return this.getMedia(user, id).then(p => {
if (p) return p; if (p) return p;
throw new MastoApiError(404); throw new MastoApiError(404);
}); });
} }
} }

View file

@ -13,7 +13,7 @@ export class MfmHelpers {
return null; return null;
} }
const {window} = new JSDOM(""); const { window } = new JSDOM("");
const doc = window.document; const doc = window.document;
@ -122,7 +122,7 @@ export class MfmHelpers {
mention(node) { mention(node) {
const a = doc.createElement("a"); const a = doc.createElement("a");
const {username, host, acct} = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find( const remoteUserInfo = mentionedRemoteUsers.find(
(remoteUser) => (remoteUser) =>
remoteUser.username === username && remoteUser.host === host, remoteUser.username === username && remoteUser.host === host,

View file

@ -7,11 +7,10 @@ import { awaitAll } from "@/prelude/await-all.js";
import { UserConverter } from "@/server/api/mastodon/converters/user.js"; import { UserConverter } from "@/server/api/mastodon/converters/user.js";
import { convertAccountId } from "@/server/api/mastodon/converters.js"; import { convertAccountId } from "@/server/api/mastodon/converters.js";
import { Announcement } from "@/models/entities/announcement.js"; import { Announcement } from "@/models/entities/announcement.js";
import { ILocalUser } from "@/models/entities/user.js"; import { ILocalUser, User } from "@/models/entities/user.js";
import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js"; import { AnnouncementConverter } from "@/server/api/mastodon/converters/announcement.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import * as Acct from "@/misc/acct.js"; import * as Acct from "@/misc/acct.js";
import { User } from "@/models/entities/user.js";
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
import { generateMutedUserQueryForUsers } from "@/server/api/common/generate-muted-user-query.js"; import { generateMutedUserQueryForUsers } from "@/server/api/common/generate-muted-user-query.js";
import { generateBlockQueryForUsers } from "@/server/api/common/generate-block-query.js"; import { generateBlockQueryForUsers } from "@/server/api/common/generate-block-query.js";
@ -23,8 +22,8 @@ import { VisibilityConverter } from "@/server/api/mastodon/converters/visibility
export class MiscHelpers { export class MiscHelpers {
public static async getInstance(): Promise<MastodonEntity.Instance> { public static async getInstance(): Promise<MastodonEntity.Instance> {
const userCount = Users.count({where: {host: IsNull()}}); const userCount = Users.count({ where: { host: IsNull() } });
const noteCount = Notes.count({where: {userHost: IsNull()}}); const noteCount = Notes.count({ where: { userHost: IsNull() } });
const instanceCount = Instances.count({ cache: 3600000 }); const instanceCount = Instances.count({ cache: 3600000 });
const contact = await Users.findOne({ const contact = await Users.findOne({
where: { where: {
@ -33,7 +32,7 @@ export class MiscHelpers {
isDeleted: false, isDeleted: false,
isSuspended: false, isSuspended: false,
}, },
order: {id: "ASC"}, order: { id: "ASC" },
}) })
.then(p => p ? UserConverter.encode(p) : null) .then(p => p ? UserConverter.encode(p) : null)
.then(p => p ? convertAccountId(p) : null); .then(p => p ? convertAccountId(p) : null);
@ -100,9 +99,9 @@ export class MiscHelpers {
if (includeRead) { if (includeRead) {
const [announcements, reads] = await Promise.all([ const [announcements, reads] = await Promise.all([
Announcements.createQueryBuilder("announcement") Announcements.createQueryBuilder("announcement")
.orderBy({"announcement.id": "DESC"}) .orderBy({ "announcement.id": "DESC" })
.getMany(), .getMany(),
AnnouncementReads.findBy({userId: user.id}) AnnouncementReads.findBy({ userId: user.id })
.then(p => p.map(x => x.announcementId)) .then(p => p.map(x => x.announcementId))
]); ]);
@ -115,7 +114,7 @@ export class MiscHelpers {
const query = Announcements.createQueryBuilder("announcement") const query = Announcements.createQueryBuilder("announcement")
.where(`announcement.id NOT IN (${sq.getQuery()})`) .where(`announcement.id NOT IN (${sq.getQuery()})`)
.orderBy({"announcement.id": "DESC"}) .orderBy({ "announcement.id": "DESC" })
.setParameter("userId", user.id); .setParameter("userId", user.id);
return query.getMany() return query.getMany()
@ -123,7 +122,7 @@ export class MiscHelpers {
} }
public static async dismissAnnouncement(announcement: Announcement, user: ILocalUser): Promise<void> { public static async dismissAnnouncement(announcement: Announcement, user: ILocalUser): Promise<void> {
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({
id: genId(), id: genId(),
@ -139,19 +138,19 @@ export class MiscHelpers {
const results: Promise<MastodonEntity.SuggestedAccount[]>[] = []; const results: Promise<MastodonEntity.SuggestedAccount[]>[] = [];
const pinned = fetchMeta().then(meta => Promise.all( const pinned = fetchMeta().then(meta => Promise.all(
meta.pinnedUsers meta.pinnedUsers
.map((acct) => Acct.parse(acct)) .map((acct) => Acct.parse(acct))
.map((acct) => .map((acct) =>
Users.findOneBy({ Users.findOneBy({
usernameLower: acct.username.toLowerCase(), usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(), host: acct.host ?? IsNull(),
})) }))
) )
.then(p => p.filter(x => !!x) as User[]) .then(p => p.filter(x => !!x) as User[])
.then(p => UserConverter.encodeMany(p, cache)) .then(p => UserConverter.encodeMany(p, cache))
.then(p => p.map(x => { .then(p => p.map(x => {
return {source: "staff", account: x} as MastodonEntity.SuggestedAccount return { source: "staff", account: x } as MastodonEntity.SuggestedAccount
})) }))
); );
const query = Users.createQueryBuilder("user") const query = Users.createQueryBuilder("user")
@ -170,7 +169,7 @@ export class MiscHelpers {
.getMany() .getMany()
.then(p => UserConverter.encodeMany(p, cache)) .then(p => UserConverter.encodeMany(p, cache))
.then(p => p.map(x => { .then(p => p.map(x => {
return {source: "global", account: x} as MastodonEntity.SuggestedAccount return { source: "global", account: x } as MastodonEntity.SuggestedAccount
})); }));
results.push(pinned); results.push(pinned);
@ -182,17 +181,18 @@ export class MiscHelpers {
public static async getCustomEmoji() { public static async getCustomEmoji() {
return Emojis.find({ return Emojis.find({
where: { where: {
host: IsNull(), host: IsNull(),
}, },
order: { order: {
category: "ASC", category: "ASC",
name: "ASC", name: "ASC",
}, },
cache: { cache: {
id: "meta_emojis", id: "meta_emojis",
milliseconds: 3600000, // 1 hour milliseconds: 3600000, // 1 hour
}} }
}
) )
.then(dbRes => populateEmojis(dbRes.map(p => p.name), null) .then(dbRes => populateEmojis(dbRes.map(p => p.name), null)
.then(p => p.map(x => EmojiConverter.encode(x)) .then(p => p.map(x => EmojiConverter.encode(x))
@ -230,7 +230,7 @@ export class MiscHelpers {
} }
public static getPreferences(user: ILocalUser): Promise<MastodonEntity.Preferences> { public static getPreferences(user: ILocalUser): Promise<MastodonEntity.Preferences> {
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(user)

View file

@ -147,7 +147,7 @@ export class NoteHelpers {
maxId, maxId,
minId minId
) )
.andWhere("reaction.noteId = :noteId", {noteId: note.id}) .andWhere("reaction.noteId = :noteId", { noteId: note.id })
.innerJoinAndSelect("reaction.user", "user"); .innerJoinAndSelect("reaction.user", "user");
return query.take(limit).getMany().then(async p => { return query.take(limit).getMany().then(async p => {
@ -168,7 +168,7 @@ export class NoteHelpers {
const cache = UserHelpers.getFreshAccountCache(); const cache = UserHelpers.getFreshAccountCache();
const account = Promise.resolve(note.user ?? await UserHelpers.getUserCached(note.userId, cache)) const account = Promise.resolve(note.user ?? await UserHelpers.getUserCached(note.userId, cache))
.then(p => UserConverter.encode(p, cache)); .then(p => UserConverter.encode(p, cache));
const edits = await NoteEdits.find({where: {noteId: note.id}, order: {id: "ASC"}}); const edits = await NoteEdits.find({ where: { noteId: note.id }, order: { id: "ASC" } });
const history: Promise<MastodonEntity.StatusEdit>[] = []; const history: Promise<MastodonEntity.StatusEdit>[] = [];
const curr = { const curr = {
@ -219,7 +219,7 @@ export class NoteHelpers {
maxId, maxId,
minId minId
) )
.andWhere("note.renoteId = :noteId", {noteId: note.id}) .andWhere("note.renoteId = :noteId", { noteId: note.id })
.andWhere("note.text IS NULL") // We don't want to count quotes as renotes .andWhere("note.text IS NULL") // We don't want to count quotes as renotes
.innerJoinAndSelect("note.user", "user"); .innerJoinAndSelect("note.user", "user");
@ -244,7 +244,7 @@ export class NoteHelpers {
const query = makePaginationQuery(Notes.createQueryBuilder("note")) const query = makePaginationQuery(Notes.createQueryBuilder("note"))
.andWhere( .andWhere(
"note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))",
{noteId, depth, limit}, { noteId, depth, limit },
); );
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);

View file

@ -2,7 +2,7 @@ import { ILocalUser } from "@/models/entities/user.js";
import { Notes, Notifications } from "@/models/index.js"; import { Notes, Notifications } from "@/models/index.js";
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; 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";
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<Notification[]> { public static async getNotifications(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined): Promise<Notification[]> {
@ -24,11 +24,11 @@ export class NotificationHelpers {
maxId, maxId,
minId minId
) )
.andWhere("notification.notifieeId = :userId", {userId: user.id}) .andWhere("notification.notifieeId = :userId", { userId: user.id })
.andWhere("notification.type IN (:...types)", {types: requestedTypes}); .andWhere("notification.type IN (:...types)", { types: requestedTypes });
if (accountId !== undefined) if (accountId !== undefined)
query.andWhere("notification.notifierId = :notifierId", {notifierId: accountId}); query.andWhere("notification.notifierId = :notifierId", { notifierId: accountId });
query.leftJoinAndSelect("notification.note", "note"); query.leftJoinAndSelect("notification.note", "note");
@ -36,22 +36,22 @@ export class NotificationHelpers {
} }
public static async getNotification(id: string, user: ILocalUser): Promise<Notification | null> { public static async getNotification(id: string, user: ILocalUser): Promise<Notification | null> {
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, user: ILocalUser): Promise<Notification> {
return this.getNotification(id, user).then(p => { return this.getNotification(id, user).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, user: ILocalUser): Promise<void> {
const result = await Notifications.update({id: id, notifieeId: user.id}, {isRead: true}); const result = await Notifications.update({ id: id, notifieeId: user.id }, { isRead: true });
} }
public static async clearAllNotifications(user: ILocalUser): Promise<void> { public static async clearAllNotifications(user: ILocalUser): Promise<void> {
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, user: ILocalUser): Promise<void> {
@ -68,7 +68,7 @@ export class NotificationHelpers {
.setParameter("conversationId", id) .setParameter("conversationId", id)
.setParameter("types", ['reply', 'mention']) .setParameter("types", ['reply', 'mention'])
.update() .update()
.set({isRead: true}) .set({ isRead: true })
.execute(); .execute();
} }

View file

@ -13,22 +13,22 @@ export class PaginationHelpers {
if (sinceId && minId) throw new Error("Can't user both sinceId and minId params"); if (sinceId && minId) throw new Error("Can't user both sinceId and minId params");
if (sinceId && maxId) { if (sinceId && maxId) {
q.andWhere(`${idField} > :sinceId`, {sinceId: sinceId}); q.andWhere(`${idField} > :sinceId`, { sinceId: sinceId });
q.andWhere(`${idField} < :maxId`, {maxId: maxId}); q.andWhere(`${idField} < :maxId`, { maxId: maxId });
q.orderBy(`${idField}`, "DESC"); q.orderBy(`${idField}`, "DESC");
} }
if (minId && maxId) { if (minId && maxId) {
q.andWhere(`${idField} > :minId`, {minId: minId}); q.andWhere(`${idField} > :minId`, { minId: minId });
q.andWhere(`${idField} < :maxId`, {maxId: maxId}); q.andWhere(`${idField} < :maxId`, { maxId: maxId });
q.orderBy(`${idField}`, "ASC"); q.orderBy(`${idField}`, "ASC");
} else if (sinceId) { } else if (sinceId) {
q.andWhere(`${idField} > :sinceId`, {sinceId: sinceId}); q.andWhere(`${idField} > :sinceId`, { sinceId: sinceId });
q.orderBy(`${idField}`, "DESC"); q.orderBy(`${idField}`, "DESC");
} else if (minId) { } else if (minId) {
q.andWhere(`${idField} > :minId`, {minId: minId}); q.andWhere(`${idField} > :minId`, { minId: minId });
q.orderBy(`${idField}`, "ASC"); q.orderBy(`${idField}`, "ASC");
} else if (maxId) { } else if (maxId) {
q.andWhere(`${idField} < :maxId`, {maxId: maxId}); q.andWhere(`${idField} < :maxId`, { maxId: maxId });
q.orderBy(`${idField}`, "DESC"); q.orderBy(`${idField}`, "DESC");
} else { } else {
q.orderBy(`${idField}`, "DESC"); q.orderBy(`${idField}`, "DESC");

View file

@ -10,7 +10,7 @@ import { deliver } from "@/queue/index.js";
import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js";
import renderVote from "@/remote/activitypub/renderer/vote.js"; import renderVote from "@/remote/activitypub/renderer/vote.js";
import { Not } from "typeorm"; import { Not } from "typeorm";
import {MastoApiError} from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.js";
export class PollHelpers { export class PollHelpers {
public static async getPoll(note: Note, user: ILocalUser | null): Promise<MastodonEntity.Poll> { public static async getPoll(note: Note, user: ILocalUser | null): Promise<MastodonEntity.Poll> {
@ -34,7 +34,7 @@ export class PollHelpers {
if (block) throw new Error('You are blocked by the poll author'); if (block) throw new Error('You are blocked by the poll author');
} }
const poll = await Polls.findOneByOrFail({noteId: note.id}); const poll = await Polls.findOneByOrFail({ noteId: note.id });
if (poll.expiresAt && poll.expiresAt < createdAt) throw new Error('Poll is expired'); if (poll.expiresAt && poll.expiresAt < createdAt) throw new Error('Poll is expired');

View file

@ -63,7 +63,10 @@ export class SearchHelpers {
if (!match) match = q.match(/^@(?<user>[a-zA-Z0-9_]+)$/) if (!match) match = q.match(/^@(?<user>[a-zA-Z0-9_]+)$/)
if (match) { if (match) {
// check if user is already in database // check if user is already in database
const dbResult = await Users.findOneBy({usernameLower: match.groups!.user.toLowerCase(), host: match.groups?.host ?? IsNull()}); const dbResult = await Users.findOneBy({
usernameLower: match.groups!.user.toLowerCase(),
host: match.groups?.host ?? IsNull()
});
if (dbResult) return [dbResult]; if (dbResult) return [dbResult];
const result = await resolveUser(match.groups!.user.toLowerCase(), match.groups?.host ?? null); const result = await resolveUser(match.groups!.user.toLowerCase(), match.groups?.host ?? null);
@ -89,23 +92,23 @@ export class SearchHelpers {
if (following) { if (following) {
const followingQuery = Followings.createQueryBuilder("following") const followingQuery = Followings.createQueryBuilder("following")
.select("following.followeeId") .select("following.followeeId")
.where("following.followerId = :followerId", {followerId: user.id}); .where("following.followerId = :followerId", { followerId: user.id });
query.andWhere( query.andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.where(`user.id IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id}); qb.where(`user.id IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, { meId: user.id });
}), }),
); );
} }
query.andWhere( query.andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.where("user.name ILIKE :q", {q: `%${sqlLikeEscape(q)}%`}); qb.where("user.name ILIKE :q", { q: `%${sqlLikeEscape(q)}%` });
qb.orWhere("user.usernameLower ILIKE :q", {q: `%${sqlLikeEscape(q)}%`}); qb.orWhere("user.usernameLower ILIKE :q", { q: `%${sqlLikeEscape(q)}%` });
}) })
); );
query.orderBy({'user.notesCount': 'DESC'}); query.orderBy({ 'user.notesCount': 'DESC' });
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);
} }
@ -184,8 +187,8 @@ export class SearchHelpers {
const chunk = ids.slice(start, start + chunkSize); const chunk = ids.slice(start, start + chunkSize);
const query = Notes.createQueryBuilder("note") const query = Notes.createQueryBuilder("note")
.where({id: In(chunk)}) .where({ id: In(chunk) })
.orderBy({id: "DESC"}) .orderBy({ id: "DESC" })
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
@ -197,11 +200,11 @@ export class SearchHelpers {
if (following) { if (following) {
const followingQuery = Followings.createQueryBuilder("following") const followingQuery = Followings.createQueryBuilder("following")
.select("following.followeeId") .select("following.followeeId")
.where("following.followerId = :followerId", {followerId: user.id}); .where("following.followerId = :followerId", { followerId: user.id });
query.andWhere( query.andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id}); qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, { meId: user.id });
}), }),
) )
} }
@ -267,8 +270,8 @@ export class SearchHelpers {
const chunk = ids.slice(start, start + chunkSize); const chunk = ids.slice(start, start + chunkSize);
const query = Notes.createQueryBuilder("note") const query = Notes.createQueryBuilder("note")
.where({id: In(chunk)}) .where({ id: In(chunk) })
.orderBy({id: "DESC"}) .orderBy({ id: "DESC" })
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
@ -356,23 +359,23 @@ export class SearchHelpers {
); );
if (accountId) { if (accountId) {
query.andWhere("note.userId = :userId", {userId: accountId}); query.andWhere("note.userId = :userId", { userId: accountId });
} }
if (following) { if (following) {
const followingQuery = Followings.createQueryBuilder("following") const followingQuery = Followings.createQueryBuilder("following")
.select("following.followeeId") .select("following.followeeId")
.where("following.followerId = :followerId", {followerId: user.id}); .where("following.followerId = :followerId", { followerId: user.id });
query.andWhere( query.andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id}); qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, { meId: user.id });
}), }),
) )
} }
query query
.andWhere("note.text ILIKE :q", {q: `%${sqlLikeEscape(q)}%`}) .andWhere("note.text ILIKE :q", { q: `%${sqlLikeEscape(q)}%` })
.leftJoinAndSelect("note.renote", "renote"); .leftJoinAndSelect("note.renote", "renote");
@ -390,8 +393,8 @@ export class SearchHelpers {
const tags = Hashtags.createQueryBuilder('tag') const tags = Hashtags.createQueryBuilder('tag')
.select('tag.name') .select('tag.name')
.distinctOn(['tag.name']) .distinctOn(['tag.name'])
.where("tag.name ILIKE :q", {q: `%${sqlLikeEscape(q)}%`}) .where("tag.name ILIKE :q", { q: `%${sqlLikeEscape(q)}%` })
.orderBy({'tag.name': 'ASC'}) .orderBy({ 'tag.name': 'ASC' })
.skip(offset ?? 0).take(limit).getMany(); .skip(offset ?? 0).take(limit).getMany();
return tags.then(p => p.map(tag => { return tags.then(p => p.map(tag => {

View file

@ -17,7 +17,7 @@ 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 { awaitAll } from "@/prelude/await-all.js"; import { awaitAll } from "@/prelude/await-all.js";
import { unique } from "@/prelude/array.js"; import { unique } from "@/prelude/array.js";
import {MastoApiError} from "@/server/api/mastodon/middleware/catch-errors.js"; import { MastoApiError } from "@/server/api/mastodon/middleware/catch-errors.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<Note[]> { public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
@ -25,7 +25,7 @@ export class TimelineHelpers {
const followingQuery = Followings.createQueryBuilder("following") const followingQuery = Followings.createQueryBuilder("following")
.select("following.followeeId") .select("following.followeeId")
.where("following.followerId = :followerId", {followerId: user.id}); .where("following.followerId = :followerId", { followerId: user.id });
const query = PaginationHelpers.makePaginationQuery( const query = PaginationHelpers.makePaginationQuery(
Notes.createQueryBuilder("note"), Notes.createQueryBuilder("note"),
@ -35,7 +35,7 @@ export class TimelineHelpers {
) )
.andWhere( .andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id}); qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, { meId: user.id });
}), }),
) )
.leftJoinAndSelect("note.renote", "renote"); .leftJoinAndSelect("note.renote", "renote");
@ -114,7 +114,7 @@ export class TimelineHelpers {
.andWhere(`note.userId IN (${listQuery.getQuery()})`) .andWhere(`note.userId IN (${listQuery.getQuery()})`)
.andWhere("note.visibility != 'specified'") .andWhere("note.visibility != 'specified'")
.leftJoinAndSelect("note.renote", "renote") .leftJoinAndSelect("note.renote", "renote")
.setParameters({listId: list.id}); .setParameters({ listId: list.id });
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
@ -137,11 +137,11 @@ export class TimelineHelpers {
minId minId
) )
.andWhere("note.visibility = 'public'") .andWhere("note.visibility = 'public'")
.andWhere("note.tags @> array[:tag]::varchar[]", {tag: tag}); .andWhere("note.tags @> array[:tag]::varchar[]", { tag: tag });
if (any.length > 0) query.andWhere("note.tags && array[:...any]::varchar[]", {any: any}); if (any.length > 0) query.andWhere("note.tags && array[:...any]::varchar[]", { any: any });
if (all.length > 0) query.andWhere("note.tags @> array[:...all]::varchar[]", {all: all}); if (all.length > 0) query.andWhere("note.tags @> array[:...all]::varchar[]", { all: all });
if (none.length > 0) query.andWhere("NOT(note.tags @> array[:...none]::varchar[])", {none: none}); if (none.length > 0) query.andWhere("NOT(note.tags @> array[:...none]::varchar[])", { none: none });
if (remote) query.andWhere("note.userHost IS NOT NULL"); if (remote) query.andWhere("note.userHost IS NOT NULL");
if (local) query.andWhere("note.userHost IS NULL"); if (local) query.andWhere("note.userHost IS NULL");
@ -168,7 +168,7 @@ export class TimelineHelpers {
.select("COALESCE(note.threadId, note.id)", "conversationId") .select("COALESCE(note.threadId, note.id)", "conversationId")
.addSelect("note.id", "latest") .addSelect("note.id", "latest")
.distinctOn(["COALESCE(note.threadId, note.id)"]) .distinctOn(["COALESCE(note.threadId, note.id)"])
.orderBy({"COALESCE(note.threadId, note.id)": minId ? "ASC" : "DESC", "note.id": "DESC"}) .orderBy({ "COALESCE(note.threadId, note.id)": minId ? "ASC" : "DESC", "note.id": "DESC" })
.andWhere("note.visibility = 'specified'") .andWhere("note.visibility = 'specified'")
.andWhere( .andWhere(
new Brackets(qb => { new Brackets(qb => {
@ -183,7 +183,7 @@ export class TimelineHelpers {
minId minId
) )
.innerJoin(`(${sq.getQuery()})`, "sq", "note.id = sq.latest") .innerJoin(`(${sq.getQuery()})`, "sq", "note.id = sq.latest")
.setParameters({userId: user.id}) .setParameters({ userId: user.id })
return query.take(limit).getMany().then(p => { return query.take(limit).getMany().then(p => {
if (minId !== undefined) p = p.reverse(); if (minId !== undefined) p = p.reverse();

View file

@ -67,8 +67,8 @@ 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, localUser: ILocalUser, reblogs: boolean, notify: boolean): Promise<MastodonEntity.Relationship> {
//FIXME: implement reblogs & notify params //FIXME: implement reblogs & notify params
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)
await createFollowing(localUser, target); await createFollowing(localUser, target);
@ -76,8 +76,8 @@ export class UserHelpers {
} }
public static async unfollowUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async unfollowUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
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)
await deleteFollowing(localUser, target); await deleteFollowing(localUser, target);
if (requested) if (requested)
@ -87,7 +87,7 @@ export class UserHelpers {
} }
public static async blockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async blockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
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);
@ -95,7 +95,7 @@ export class UserHelpers {
} }
public static async unblockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async unblockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
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);
@ -104,7 +104,7 @@ export class UserHelpers {
public static async muteUser(target: User, localUser: ILocalUser, notifications: boolean = true, duration: number = 0): Promise<MastodonEntity.Relationship> { public static async muteUser(target: User, localUser: ILocalUser, notifications: boolean = true, duration: number = 0): Promise<MastodonEntity.Relationship> {
//FIXME: respect notifications parameter //FIXME: respect notifications parameter
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({
id: genId(), id: genId(),
@ -126,7 +126,7 @@ export class UserHelpers {
} }
public static async unmuteUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async unmuteUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
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({
id: muting.id, id: muting.id,
@ -139,14 +139,14 @@ export class UserHelpers {
} }
public static async acceptFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> { public static async acceptFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
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, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
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);
@ -193,7 +193,7 @@ export class UserHelpers {
public static async verifyCredentials(user: ILocalUser): Promise<MastodonEntity.Account> { public static async verifyCredentials(user: ILocalUser): Promise<MastodonEntity.Account> {
const acct = UserConverter.encode(user); const acct = UserConverter.encode(user);
const profile = UserProfiles.findOneByOrFail({userId: user.id}); const profile = UserProfiles.findOneByOrFail({ userId: user.id });
const privacy = this.getDefaultNoteVisibility(user); const privacy = this.getDefaultNoteVisibility(user);
const fields = profile.then(profile => profile.fields.map(field => { const fields = profile.then(profile => profile.fields.map(field => {
return { return {
@ -222,7 +222,7 @@ export class UserHelpers {
public static async getUserFromAcct(acct: string): Promise<User> { public static async getUserFromAcct(acct: string): Promise<User> {
const split = acct.toLowerCase().split('@'); const split = acct.toLowerCase().split('@');
if (split.length > 2) throw new Error('Invalid acct'); if (split.length > 2) throw new Error('Invalid acct');
return Users.findOneBy({usernameLower: split[0], host: split[1] ?? IsNull()}) return Users.findOneBy({ usernameLower: split[0], host: split[1] ?? IsNull() })
.then(p => { .then(p => {
if (p) return p; if (p) return p;
throw new MastoApiError(404); throw new MastoApiError(404);
@ -239,7 +239,7 @@ export class UserHelpers {
minId minId
); );
query.andWhere("muting.muterId = :userId", {userId: user.id}) query.andWhere("muting.muterId = :userId", { userId: user.id })
.innerJoinAndSelect("muting.mutee", "mutee"); .innerJoinAndSelect("muting.mutee", "mutee");
return query.take(limit).getMany().then(async p => { return query.take(limit).getMany().then(async p => {
@ -275,7 +275,7 @@ export class UserHelpers {
minId minId
); );
query.andWhere("blocking.blockerId = :userId", {userId: user.id}) query.andWhere("blocking.blockerId = :userId", { userId: user.id })
.innerJoinAndSelect("blocking.blockee", "blockee"); .innerJoinAndSelect("blocking.blockee", "blockee");
return query.take(limit).getMany().then(p => { return query.take(limit).getMany().then(p => {
@ -302,7 +302,7 @@ export class UserHelpers {
minId minId
); );
query.andWhere("request.followeeId = :userId", {userId: user.id}) query.andWhere("request.followeeId = :userId", { userId: user.id })
.innerJoinAndSelect("request.follower", "follower"); .innerJoinAndSelect("request.follower", "follower");
return query.take(limit).getMany().then(p => { return query.take(limit).getMany().then(p => {
@ -356,7 +356,7 @@ export class UserHelpers {
new Brackets(qb => { new Brackets(qb => {
qb.where("note.replyId IS NULL") qb.where("note.replyId IS NULL")
.orWhere(new Brackets(qb => { .orWhere(new Brackets(qb => {
qb.where('note.mentions = :mentions', {mentions: []}) qb.where('note.mentions = :mentions', { mentions: [] })
.andWhere('thread.userId = :userId') .andWhere('thread.userId = :userId')
})); }));
})); }));
@ -375,7 +375,7 @@ export class UserHelpers {
query.andWhere("note.visibility != 'hidden'"); query.andWhere("note.visibility != 'hidden'");
query.andWhere("note.visibility != 'specified'"); query.andWhere("note.visibility != 'specified'");
query.setParameters({userId: user.id}); query.setParameters({ userId: user.id });
return PaginationHelpers.execQuery(query, limit, minId !== undefined); return PaginationHelpers.execQuery(query, limit, minId !== undefined);
} }
@ -389,7 +389,7 @@ export class UserHelpers {
maxId, maxId,
minId minId
) )
.andWhere("favorite.userId = :meId", {meId: localUser.id}) .andWhere("favorite.userId = :meId", { meId: localUser.id })
.leftJoinAndSelect("favorite.note", "note"); .leftJoinAndSelect("favorite.note", "note");
generateVisibilityQuery(query, localUser); generateVisibilityQuery(query, localUser);
@ -413,7 +413,7 @@ export class UserHelpers {
maxId, maxId,
minId minId
) )
.andWhere("reaction.userId = :meId", {meId: localUser.id}) .andWhere("reaction.userId = :meId", { meId: localUser.id })
.leftJoinAndSelect("reaction.note", "note"); .leftJoinAndSelect("reaction.note", "note");
generateVisibilityQuery(query, localUser); generateVisibilityQuery(query, localUser);
@ -431,11 +431,11 @@ 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, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
if (limit > 80) limit = 80; if (limit > 80) limit = 80;
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: [] };
} else if (profile.ffVisibility === "followers") { } else if (profile.ffVisibility === "followers") {
if (!localUser) return {data: []}; if (!localUser) return { data: [] };
if (user.id !== localUser.id) { if (user.id !== localUser.id) {
const isFollowed = await Followings.exist({ const isFollowed = await Followings.exist({
where: { where: {
@ -443,7 +443,7 @@ export class UserHelpers {
followerId: localUser.id, followerId: localUser.id,
}, },
}); });
if (!isFollowed) return {data: []}; if (!isFollowed) return { data: [] };
} }
} }
@ -455,10 +455,10 @@ export class UserHelpers {
); );
if (type === "followers") { if (type === "followers") {
query.andWhere("following.followeeId = :userId", {userId: user.id}) query.andWhere("following.followeeId = :userId", { userId: user.id })
.innerJoinAndSelect("following.follower", "follower"); .innerJoinAndSelect("following.follower", "follower");
} else { } else {
query.andWhere("following.followerId = :userId", {userId: user.id}) query.andWhere("following.followerId = :userId", { userId: user.id })
.innerJoinAndSelect("following.followee", "followee"); .innerJoinAndSelect("following.followee", "followee");
} }
@ -538,6 +538,11 @@ export class UserHelpers {
} }
public static async getDefaultNoteVisibility(user: ILocalUser): Promise<IceshrimpVisibility> { public static async getDefaultNoteVisibility(user: ILocalUser): Promise<IceshrimpVisibility> {
return RegistryItems.findOneBy({domain: IsNull(), userId: user.id, key: 'defaultNoteVisibility', scope: '{client,base}'}).then(p => p?.value ?? 'public') return RegistryItems.findOneBy({
domain: IsNull(),
userId: user.id,
key: 'defaultNoteVisibility',
scope: '{client,base}'
}).then(p => p?.value ?? 'public')
} }
} }

View file

@ -19,12 +19,11 @@ export function auth(required: boolean, scopes: string[] = []) {
return; return;
} }
if (!AuthConverter.decode(scopes).every(p => ctx.scopes.includes(p))) { if (!AuthConverter.decode(scopes).every(p => ctx.scopes.includes(p))) {
if (required) { if (required) {
ctx.status = 403; ctx.status = 403;
ctx.body = {error: "This action is outside the authorized scopes"}; ctx.body = { error: "This action is outside the authorized scopes" };
} } else {
else {
ctx.user = null; ctx.user = null;
ctx.scopes = []; ctx.scopes = [];
} }

View file

@ -1,9 +1,10 @@
import { MastoContext, logger } from "@/server/api/mastodon/index.js"; import { logger, MastoContext } from "@/server/api/mastodon/index.js";
import { IdentifiableError } from "@/misc/identifiable-error.js"; import { IdentifiableError } from "@/misc/identifiable-error.js";
import { ApiError } from "@/server/api/error.js"; import { ApiError } from "@/server/api/error.js";
export class MastoApiError extends Error { export class MastoApiError extends Error {
statusCode: number; statusCode: number;
constructor(statusCode: number, message?: string) { constructor(statusCode: number, message?: string) {
if (message == null) { if (message == null) {
switch (statusCode) { switch (statusCode) {
@ -26,20 +27,16 @@ export async function CatchErrorsMiddleware(ctx: MastoContext, next: () => Promi
} catch (e: any) { } catch (e: any) {
if (e instanceof MastoApiError) { if (e instanceof MastoApiError) {
ctx.status = e.statusCode; ctx.status = e.statusCode;
} } else if (e instanceof IdentifiableError) {
else if (e instanceof IdentifiableError) {
ctx.status = 400; ctx.status = 400;
} } else if (e instanceof ApiError) {
else if (e instanceof ApiError) {
ctx.status = e.httpStatusCode ?? 500; ctx.status = e.httpStatusCode ?? 500;
} } else {
else {
logger.error(`Error occured in ${ctx.method} ${ctx.path}:`); logger.error(`Error occured in ${ctx.method} ${ctx.path}:`);
if (e instanceof Error) { if (e instanceof Error) {
if (e.stack) logger.error(e.stack); if (e.stack) logger.error(e.stack);
else logger.error(`${e.name}: ${e.message}`); else logger.error(`${e.name}: ${e.message}`);
} } else {
else {
logger.error(e); logger.error(e);
} }
ctx.status = 500; ctx.status = 500;

View file

@ -5,7 +5,7 @@ export async function NormalizeQueryMiddleware(ctx: MastoContext, next: () => Pr
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) { if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
ctx.request.body = ctx.request.query; ctx.request.body = ctx.request.query;
} else { } else {
ctx.request.body = {...ctx.request.body, ...ctx.request.query}; ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
} }
} }
await next(); await next();