diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-12-15 17:27:12 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-12-15 17:27:12 +0000 |
| commit | e2352839e4639b09e2e52b2ada1399097fad1d8d (patch) | |
| tree | 9268cda477b8c1dcfb2c78eaabcb173a1566a469 /packages/backend/src/server/api | |
| parent | merge: Fix rate limits under multi-node environments (!809) (diff) | |
| parent | upstream merge checklist: remember to check federated profile fields (diff) | |
| download | sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.tar.gz sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.tar.bz2 sharkey-e2352839e4639b09e2e52b2ada1399097fad1d8d.zip | |
merge: upstream changes for 2024.11 (!742)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/742
Closes #645 and #646
Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
Diffstat (limited to 'packages/backend/src/server/api')
27 files changed, 547 insertions, 104 deletions
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index ac3b982742..0d77309537 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -119,25 +119,30 @@ export class ApiServerService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'frc-captcha-solution'?: string; + 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; } }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); fastify.post<{ Body: { username: string; - password: string; + password?: string; token?: string; - signature?: string; - authenticatorData?: string; - clientDataJSON?: string; - credentialId?: string; - challengeId?: string; + credential?: AuthenticationResponseJSON; + 'hcaptcha-response'?: string; + 'g-recaptcha-response'?: string; + 'turnstile-response'?: string; + 'frc-captcha-solution'?: string; + 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; }; - }>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); + }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply)); fastify.post<{ Body: { credential?: AuthenticationResponseJSON; + context?: string; }; }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply)); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index c478bebdaf..e319d6e0a4 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js'; +import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js'; import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; @@ -473,6 +475,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default }; const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default }; const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default }; +const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default }; +const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default }; const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default }; const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default }; const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default }; @@ -882,6 +886,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_relays_remove, $admin_resetPassword, $admin_resolveAbuseUserReport, + $admin_forwardAbuseUserReport, + $admin_updateAbuseUserReport, $admin_sendEmail, $admin_serverInfo, $admin_showModerationLogs, @@ -1285,6 +1291,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_relays_remove, $admin_resetPassword, $admin_resolveAbuseUserReport, + $admin_forwardAbuseUserReport, + $admin_updateAbuseUserReport, $admin_sendEmail, $admin_serverInfo, $admin_showModerationLogs, diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 8643be0f30..419017aaf4 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -42,6 +42,17 @@ export class GetterService { return note; } + @bindThis + public async getNoteWithUser(noteId: MiNote['id']) { + const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); + + if (note == null) { + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + } + + return note; + } + /** * Get note for API processing */ diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1a4ce0a54c..fa9155d82d 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -6,12 +6,14 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import * as argon2 from 'argon2'; -import * as OTPAuth from 'otpauth'; import { IsNull } from 'typeorm'; +import * as Misskey from 'misskey-js'; import { DI } from '@/di-symbols.js'; import type { + MiMeta, SigninsRepository, UserProfilesRepository, + UserSecurityKeysRepository, UsersRepository, } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -21,8 +23,9 @@ import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { CaptchaService } from '@/core/CaptchaService.js'; +import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { isSystemAccount } from '@/misc/is-system-account.js'; -import type { MiMeta } from '@/models/_.js'; import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js'; import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; import { SigninService } from './SigninService.js'; @@ -44,6 +47,9 @@ export class SigninApiService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.userSecurityKeysRepository) + private userSecurityKeysRepository: UserSecurityKeysRepository, + @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, @@ -52,6 +58,7 @@ export class SigninApiService { private signinService: SigninService, private userAuthService: UserAuthService, private webAuthnService: WebAuthnService, + private captchaService: CaptchaService, ) { } @@ -60,9 +67,15 @@ export class SigninApiService { request: FastifyRequest<{ Body: { username: string; - password: string; + password?: string; token?: string; credential?: AuthenticationResponseJSON; + 'hcaptcha-response'?: string; + 'g-recaptcha-response'?: string; + 'turnstile-response'?: string; + 'frc-captcha-solution'?: string; + 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; }; }>, reply: FastifyReply, @@ -101,11 +114,6 @@ export class SigninApiService { return; } - if (typeof password !== 'string') { - reply.code(400); - return; - } - if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -136,6 +144,27 @@ export class SigninApiService { } const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1); + + if (password == null) { + reply.code(200); + if (profile.twoFactorEnabled) { + return { + finished: false, + next: 'password', + } satisfies Misskey.entities.SigninFlowResponse; + } else { + return { + finished: false, + next: 'captcha', + } satisfies Misskey.entities.SigninFlowResponse; + } + } + + if (typeof password !== 'string') { + reply.code(400); + return; + } if (!user.approved && this.meta.approvalRequiredForSignup) { reply.code(403); @@ -151,7 +180,7 @@ export class SigninApiService { // Compare password const same = await argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!); - const fail = async (status?: number, failure?: { id: string }) => { + const fail = async (status?: number, failure?: { id: string; }) => { // Append signin history await this.signinsRepository.insert({ id: this.idService.gen(), @@ -165,6 +194,44 @@ export class SigninApiService { }; if (!profile.twoFactorEnabled) { + if (process.env.NODE_ENV !== 'test') { + if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableFC && this.meta.fcSecretKey) { + await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { + await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableTestcaptcha) { + await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + } + if (same) { if (profile.password!.startsWith('$2')) { const newHash = await argon2.hash(password); @@ -223,7 +290,7 @@ export class SigninApiService { id: '93b86c4b-72f9-40eb-9815-798928603d1e', }); } - } else { + } else if (securityKeysAvailable) { if (!same && !profile.usePasswordLessLogin) { return await fail(403, { id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', @@ -233,7 +300,23 @@ export class SigninApiService { const authRequest = await this.webAuthnService.initiateAuthentication(user.id); reply.code(200); - return authRequest; + return { + finished: false, + next: 'passkey', + authRequest, + } satisfies Misskey.entities.SigninFlowResponse; + } else { + if (!same || !profile.twoFactorEnabled) { + return await fail(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } else { + reply.code(200); + return { + finished: false, + next: 'totp', + } satisfies Misskey.entities.SigninFlowResponse; + } } // never get here } diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 70306c3113..640356b50c 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -4,13 +4,16 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import * as Misskey from 'misskey-js'; import { DI } from '@/di-symbols.js'; -import type { SigninsRepository } from '@/models/_.js'; +import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import type { MiLocalUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { SigninEntityService } from '@/core/entities/SigninEntityService.js'; import { bindThis } from '@/decorators.js'; +import { EmailService } from '@/core/EmailService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -19,7 +22,12 @@ export class SigninService { @Inject(DI.signinsRepository) private signinsRepository: SigninsRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private signinEntityService: SigninEntityService, + private emailService: EmailService, + private notificationService: NotificationService, private idService: IdService, private globalEventService: GlobalEventService, ) { @@ -28,7 +36,8 @@ export class SigninService { @bindThis public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { setImmediate(async () => { - // Append signin history + this.notificationService.createNotification(user.id, 'login', {}); + const record = await this.signinsRepository.insertOne({ id: this.idService.gen(), userId: user.id, @@ -37,15 +46,22 @@ export class SigninService { success: true, }); - // Publish signin event this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + if (profile.email && profile.emailVerified) { + this.emailService.sendEmail(profile.email, 'New login / ログインがありました', + 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。', + 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / 新しいログインがありました。このログインに心当たりがない場合は、パスワードを変更するなど、アカウントのセキュリティ状態を更新してください。'); + } }); reply.code(200); return { + finished: true, id: user.id, - i: user.token, - }; + i: user.token!, + } satisfies Misskey.entities.SigninFlowResponse; } } diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index db860d710a..7aea6a0e56 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -73,6 +73,7 @@ export class SignupApiService { 'turnstile-response'?: string; 'm-captcha-response'?: string; 'frc-captcha-solution'?: string; + 'testcaptcha-response'?: string; } }>, reply: FastifyReply, @@ -111,6 +112,12 @@ export class SignupApiService { throw new FastifyReplyError(400, err); }); } + + if (this.meta.enableTestcaptcha) { + await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } } const username = body['username']; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 269afbf14b..b4f36234f0 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -75,6 +75,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js'; import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js'; import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js'; +import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js'; +import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js'; import * as ep___admin_sendEmail from './endpoints/admin/send-email.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; @@ -478,6 +480,8 @@ const eps = [ ['admin/relays/remove', ep___admin_relays_remove], ['admin/reset-password', ep___admin_resetPassword], ['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], + ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport], + ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport], ['admin/send-email', ep___admin_sendEmail], ['admin/server-info', ep___admin_serverInfo], ['admin/show-moderation-logs', ep___admin_showModerationLogs], diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts index cf3f257ca6..0dbfaae054 100644 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -71,9 +71,22 @@ export const meta = { }, assignee: { type: 'object', - nullable: true, optional: true, + nullable: true, optional: false, ref: 'UserDetailedNotMe', }, + forwarded: { + type: 'boolean', + nullable: false, optional: false, + }, + resolvedAs: { + type: 'string', + nullable: true, optional: false, + enum: ['accept', 'reject', null], + }, + moderationNote: { + type: 'string', + nullable: false, optional: false, + }, }, }, }, @@ -88,7 +101,6 @@ export const paramDef = { state: { type: 'string', nullable: true, default: null }, reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, - forwarded: { type: 'boolean', default: false }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 7754899b95..53b1c4c4ec 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -3,33 +3,36 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsersRepository } from '@/models/_.js'; import { MiAccessToken, MiUser } from '@/models/_.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import { ApiError } from '@/server/api/error.js'; import { Packed } from '@/misc/json-schema.js'; import { RoleService } from '@/core/RoleService.js'; -import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['admin'], - res: { - type: 'object', - optional: false, nullable: false, - ref: 'MeDetailed', - properties: { - token: { - type: 'string', - optional: false, nullable: false, - }, + errors: { + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '1fb7cb09-d46a-4fff-b8df-057708cce513', + }, + + wrongInitialPassword: { + message: 'Initial password is incorrect.', + code: 'INCORRECT_INITIAL_PASSWORD', + id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62', }, - }, - errors: { // From ApiCallService.ts noCredential: { message: 'Credential required.', @@ -51,6 +54,18 @@ export const meta = { }, }, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'MeDetailed', + properties: { + token: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, + // Required token permissions, but we need to check them manually. // ApiCallService checks access in a way that would prevent creating the first account. softPermissions: [ @@ -64,6 +79,7 @@ export const paramDef = { properties: { username: localUsernameSchema, password: passwordSchema, + setupPassword: { type: 'string', nullable: true }, }, required: ['username', 'password'], } as const; @@ -71,13 +87,49 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private roleService: RoleService, private userEntityService: UserEntityService, private signupService: SignupService, private instanceActorService: InstanceActorService, ) { super(meta, paramDef, async (ps, _me, token) => { - await this.ensurePermissions(_me, token); + const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; + const realUsers = await this.instanceActorService.realLocalUsersPresent(); + + if (!realUsers && me == null && token == null) { + // 初回セットアップの場合 + if (this.config.setupPassword != null) { + // 初期パスワードが設定されている場合 + if (ps.setupPassword !== this.config.setupPassword) { + // 初期パスワードが違う場合 + throw new ApiError(meta.errors.wrongInitialPassword); + } + } else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') { + // 初期パスワードが設定されていないのに初期パスワードが入力された場合 + throw new ApiError(meta.errors.wrongInitialPassword); + } + } else { + if (token && !meta.softPermissions.every(p => token.permission.includes(p))) { + // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check. + throw new ApiError(meta.errors.noPermission); + } + + if (me && !await this.roleService.isAdministrator(me)) { + // Only administrators (including root) can create users. + throw new ApiError(meta.errors.noAdmin); + } + + // Anonymous access is only allowed for initial instance setup (this check may be redundant) + if (!me && realUsers) { + throw new ApiError(meta.errors.noCredential); + } + } const { account, secret } = await this.signupService.signup({ username: ps.username, @@ -96,21 +148,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return res; }); } - - private async ensurePermissions(me: MiUser | null, token: MiAccessToken | null): Promise<void> { - // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check. - if (token && !meta.softPermissions.every(p => token.permission.includes(p))) { - throw new ApiError(meta.errors.noPermission); - } - - // Only administrators (including root) can create users. - if (me && !await this.roleService.isAdministrator(me)) { - throw new ApiError(meta.errors.noAdmin); - } - - // Anonymous access is only allowed for initial instance setup. - if (!me && await this.instanceActorService.realLocalUsersPresent()) { - throw new ApiError(meta.errors.noCredential); - } - } } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 01dea703a3..ece1984cff 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('cannot delete a root account'); } - await this.deleteAccoountService.deleteAccount(user); + await this.deleteAccoountService.deleteAccount(user, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 2dae1df87d..b8bfda73a4 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -55,7 +55,7 @@ export const paramDef = { properties: { title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, - imageUrl: { type: 'string', nullable: true, minLength: 1 }, + imageUrl: { type: 'string', nullable: true, minLength: 0 }, icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' }, display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' }, forExistingUsers: { type: 'boolean', default: false }, @@ -76,7 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- updatedAt: null, title: ps.title, text: ps.text, - imageUrl: ps.imageUrl, + /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ + imageUrl: ps.imageUrl || null, icon: ps.icon, display: ps.display, forExistingUsers: ps.forExistingUsers, diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index fd21309818..87d80cbe80 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['admin'], @@ -13,6 +14,49 @@ export const meta = { requireCredential: true, requireRolePolicy: 'canManageAvatarDecorations', kind: 'write:admin:avatar-decorations', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: false, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + roleIdsThatCanBeUsedThisDecoration: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, + }, + }, } as const; export const paramDef = { @@ -32,14 +76,25 @@ export const paramDef = { export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( private avatarDecorationService: AvatarDecorationService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - await this.avatarDecorationService.create({ + const created = await this.avatarDecorationService.create({ name: ps.name, description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, }, me); + + return { + id: created.id, + createdAt: this.idService.parse(created.id).date.toISOString(), + updatedAt: null, + name: created.name, + description: created.description, + url: created.url, + roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index aee90023e1..d785f085ac 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -4,10 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js'; -import type { MiAnnouncement } from '@/models/Announcement.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts index b6f0f22d60..9065a71f6a 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts @@ -33,13 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private deleteAccountService: DeleteAccountService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneByOrFail({ id: ps.userId }); if (user.isDeleted) { return; } - await this.deleteAccountService.deleteAccount(user); + await this.deleteAccountService.deleteAccount(user, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 3caa0f84a3..071ddbef18 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiEmoji } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -79,25 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); } - let emojiId; - if (ps.id) { - emojiId = ps.id; - const emoji = await this.customEmojiService.getEmojiById(ps.id); - if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); - if (nameNfc && (nameNfc !== emoji.name)) { - const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc); - if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); - } - } else { - if (!nameNfc) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.'); - const emoji = await this.customEmojiService.getEmojiByName(nameNfc); - if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); - emojiId = emoji.id; - } + // JSON schemeのanyOfの型変換がうまくいっていないらしい + const required = { id: ps.id, name: nameNfc } as + | { id: MiEmoji['id']; name?: string } + | { id?: MiEmoji['id']; name: string }; - await this.customEmojiService.update(emojiId, { + const error = await this.customEmojiService.update({ + ...required, driveFile, - name: nameNfc, category: ps.category?.normalize('NFC'), aliases: ps.aliases?.map(a => a.normalize('NFC')), license: ps.license, @@ -105,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, }, me); + + switch (error) { + case null: return; + case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji); + case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists); + } + // 網羅性チェック + const mustBeNever: never = error; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts new file mode 100644 index 0000000000..3e42c91fed --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:resolve-abuse-user-report', + + errors: { + noSuchAbuseReport: { + message: 'No such abuse report.', + code: 'NO_SUCH_ABUSE_REPORT', + id: '8763e21b-d9bc-40be-acf6-54c1a6986493', + kind: 'server', + httpStatusCode: 404, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + reportId: { type: 'string', format: 'misskey:id' }, + }, + required: ['reportId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + private abuseReportService: AbuseReportService, + ) { + super(meta, paramDef, async (ps, me) => { + const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); + if (!report) { + throw new ApiError(meta.errors.noSuchAbuseReport); + } + + await this.abuseReportService.forward(report.id, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 6e368eff43..6495e3b7da 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -81,6 +81,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableTestcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -189,6 +193,13 @@ export const meta = { type: 'string', }, }, + prohibitedWordsForNameOfUser: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, bannedEmailDomains: { type: 'array', optional: true, nullable: false, @@ -368,6 +379,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableStatsForFederatedInstances: { + type: 'boolean', + optional: false, nullable: false, + }, enableServerMachineStats: { type: 'boolean', optional: false, nullable: false, @@ -614,6 +629,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- turnstileSiteKey: instance.turnstileSiteKey, enableFC: instance.enableFC, fcSiteKey: instance.fcSiteKey, + enableTestcaptcha: instance.enableTestcaptcha, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, @@ -642,6 +658,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- mediaSilencedHosts: instance.mediaSilencedHosts, sensitiveWords: instance.sensitiveWords, prohibitedWords: instance.prohibitedWords, + prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser, preservedUsernames: instance.preservedUsernames, bubbleInstances: instance.bubbleInstances, hcaptchaSecretKey: instance.hcaptchaSecretKey, @@ -688,6 +705,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- truemailAuthKey: instance.truemailAuthKey, enableChartsForRemoteUser: instance.enableChartsForRemoteUser, enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances, + enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances, enableServerMachineStats: instance.enableServerMachineStats, enableAchievements: instance.enableAchievements, enableIdenticonGeneration: instance.enableIdenticonGeneration, diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 9b79100fcf..554d324ff2 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -32,7 +32,7 @@ export const paramDef = { type: 'object', properties: { reportId: { type: 'string', format: 'misskey:id' }, - forward: { type: 'boolean', default: false }, + resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true }, }, required: ['reportId'], } as const; @@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchAbuseReport); } - await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me); + await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 5f16519403..cc65ed2cf0 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -72,13 +72,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- break; } case 'moderator': { - const moderatorIds = await this.roleService.getModeratorIds(false); + const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false }); if (moderatorIds.length === 0) return []; query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds }); break; } case 'adminOrModerator': { - const adminOrModeratorIds = await this.roleService.getModeratorIds(); + const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true }); if (adminOrModeratorIds.length === 0) return []; query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds }); break; diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts new file mode 100644 index 0000000000..73d4b843f0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:resolve-abuse-user-report', + + errors: { + noSuchAbuseReport: { + message: 'No such abuse report.', + code: 'NO_SUCH_ABUSE_REPORT', + id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662', + kind: 'server', + httpStatusCode: 404, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + reportId: { type: 'string', format: 'misskey:id' }, + moderationNote: { type: 'string' }, + }, + required: ['reportId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + private abuseReportService: AbuseReportService, + ) { + super(meta, paramDef, async (ps, me) => { + const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); + if (!report) { + throw new ApiError(meta.errors.noSuchAbuseReport); + } + + await this.abuseReportService.update(report.id, { + moderationNote: ps.moderationNote, + }, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 98760bbcc3..72f428d85f 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -46,6 +46,11 @@ export const paramDef = { type: 'string', }, }, + prohibitedWordsForNameOfUser: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, @@ -84,6 +89,7 @@ export const paramDef = { enableFC: { type: 'boolean' }, fcSiteKey: { type: 'string', nullable: true }, fcSecretKey: { type: 'string', nullable: true }, + enableTestcaptcha: { type: 'boolean' }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, @@ -140,6 +146,7 @@ export const paramDef = { truemailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, + enableStatsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, enableAchievements: { type: 'boolean' }, enableIdenticonGeneration: { type: 'boolean' }, @@ -230,6 +237,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (Array.isArray(ps.prohibitedWords)) { set.prohibitedWords = ps.prohibitedWords.filter(Boolean); } + if (Array.isArray(ps.prohibitedWordsForNameOfUser)) { + set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean); + } if (Array.isArray(ps.silencedHosts)) { let lastValue = ''; set.silencedHosts = ps.silencedHosts.sort().filter((h) => { @@ -390,6 +400,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.enableFC = ps.enableFC; } + if (ps.enableTestcaptcha !== undefined) { + set.enableTestcaptcha = ps.enableTestcaptcha; + } + if (ps.fcSiteKey !== undefined) { set.fcSiteKey = ps.fcSiteKey; } @@ -610,6 +624,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances; } + if (ps.enableStatsForFederatedInstances !== undefined) { + set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances; + } + if (ps.enableServerMachineStats !== undefined) { set.enableServerMachineStats = ps.enableServerMachineStats; } @@ -709,7 +727,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (Array.isArray(ps.federationHosts)) { - set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); + set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); } const before = await this.metaService.fetch(true); diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 4232bc6e39..616a77e337 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -137,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (local != null) return local; } + // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない return await this.mergePack( me, isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null, diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts index 2e8cbffe2a..ad1f35055a 100644 --- a/packages/backend/src/server/api/endpoints/flash/featured.ts +++ b/packages/backend/src/server/api/endpoints/flash/featured.ts @@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { DI } from '@/di-symbols.js'; +import { FlashService } from '@/core/FlashService.js'; export const meta = { tags: ['flash'], @@ -33,26 +34,25 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + offset: { type: 'integer', minimum: 0, default: 0 }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, required: [], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.flashsRepository) - private flashsRepository: FlashsRepository, - + private flashService: FlashService, private flashEntityService: FlashEntityService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.flashsRepository.createQueryBuilder('flash') - .andWhere('flash.likedCount > 0') - .orderBy('flash.likedCount', 'DESC'); - - const flashs = await query.limit(10).getMany(); - - return await this.flashEntityService.packMany(flashs, me); + const result = await this.flashService.featured({ + offset: ps.offset, + limit: ps.limit, + }); + return await this.flashEntityService.packMany(result, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c22cbe5d4b..bb0f5aa11a 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; @@ -22,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; import { RolePolicies, RoleService } from '@/core/RoleService.js'; @@ -126,6 +127,13 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '8feff0ba-5ab5-585b-31f4-4df816663fad', }, + + nameContainsProhibitedWords: { + message: 'Your new name contains prohibited words.', + code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS', + id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191', + httpStatusCode: 422, + }, }, res: { @@ -187,6 +195,9 @@ export const paramDef = { noCrawle: { type: 'boolean' }, preventAiLearning: { type: 'boolean' }, noindex: { type: 'boolean' }, + requireSigninToViewContents: { type: 'boolean' }, + makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true }, + makeNotesHiddenBefore: { type: 'integer', nullable: true }, enableRss: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, @@ -242,6 +253,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private instanceMeta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -266,6 +280,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private cacheService: CacheService, private httpRequestService: HttpRequestService, private avatarDecorationService: AvatarDecorationService, + private utilityService: UtilityService, ) { super(meta, paramDef, async (ps, _user, token) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; @@ -343,6 +358,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; + if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents; + if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore; + if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; @@ -485,8 +503,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const newName = updates.name === undefined ? user.name : updates.name; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; + const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage; if (newName != null) { + let hasProhibitedWords = false; + if (!await this.roleService.isModerator(user)) { + hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser); + } + if (hasProhibitedWords) { + throw new ApiError(meta.errors.nameContainsProhibitedWords); + } + const tokens = mfm.parseSimple(newName); emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); } @@ -506,6 +533,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ]); } + if (newFollowedMessage != null) { + const tokens = mfm.parse(newFollowedMessage); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens)); + } + updates.emojis = emojis; updates.tags = tags; @@ -589,7 +621,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // these two methods need to be kept in sync with // `ApRendererService.renderPerson` private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial<MiUser>): boolean { - const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss']; + const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss', 'requireSigninToViewContents', 'makeNotesFollowersOnlyBefore', 'makeNotesHiddenBefore']; for (const field of basicFields) { if ((field in newUser) && oldUser[field] !== newUser[field]) { return true; diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 49c51cb33c..f0c9db38b4 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -28,6 +28,12 @@ export const meta = { code: 'NO_SUCH_NOTE', id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, + + signinRequired: { + message: 'Signin required.', + code: 'SIGNIN_REQUIRED', + id: '8e75455b-738c-471d-9f80-62693f33372e', + }, }, // 2 calls per second @@ -56,7 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = await this.notesRepository.createQueryBuilder('note') - .where('note.id = :noteId', { noteId: ps.noteId }); + .where('note.id = :noteId', { noteId: ps.noteId }) + .innerJoinAndSelect('note.user', 'user'); this.queryService.generateVisibilityQuery(query, me); if (me) { @@ -69,6 +76,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchNote); } + if (note.user!.requireSigninToViewContents && me == null) { + throw new ApiError(meta.errors.signinRequired); + } + return await this.noteEntityService.pack(note, me, { detail: true, }); diff --git a/packages/backend/src/server/api/endpoints/notes/versions.ts b/packages/backend/src/server/api/endpoints/notes/versions.ts index 343417f0e2..9b98d19fb1 100644 --- a/packages/backend/src/server/api/endpoints/notes/versions.ts +++ b/packages/backend/src/server/api/endpoints/notes/versions.ts @@ -27,6 +27,12 @@ export const meta = { code: 'NO_SUCH_NOTE', id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, + + signinRequired: { + message: 'Signin required.', + code: 'SIGNIN_REQUIRED', + id: '8e75455b-738c-471d-9f80-62693f33372e', + }, }, // 10 calls per 5 seconds @@ -55,10 +61,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = await this.notesRepository.createQueryBuilder('note') - .select('note.id') - .where('note.id = :noteId', { noteId: ps.noteId }); + .where('note.id = :noteId', { noteId: ps.noteId }) + .innerJoinAndSelect('note.user', 'user'); this.queryService.generateVisibilityQuery(query, me); + if (me) { + this.queryService.generateBlockedUserQuery(query, me); + } const note = await query.getOne(); @@ -66,6 +75,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchNote); } + if (note.user!.requireSigninToViewContents && me == null) { + throw new ApiError(meta.errors.signinRequired); + } + const edits = await this.getterService.getEdits(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 92d8032fa6..6416e43ff1 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -43,6 +43,12 @@ export const meta = { code: 'BOTH_WITH_REPLIES_AND_WITH_FILES', id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', }, + + signinRequired: { + message: 'Signin required.', + code: 'SIGNIN_REQUIRED', + id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2', + }, }, // 5 calls per second |