summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts19
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/GetterService.ts11
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts105
-rw-r--r--packages/backend/src/server/api/SigninService.ts26
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts7
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts97
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/create.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts57
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-account.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts55
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts58
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/flash/featured.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts36
-rw-r--r--packages/backend/src/server/api/endpoints/notes/show.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/notes/versions.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts6
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