summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2025-03-02 18:57:27 +0000
committerdakkar <dakkar@thenautilus.net>2025-03-02 18:57:27 +0000
commit0b5e197afb5f9c2519c22324706c3b27d5d3eea3 (patch)
treec73d3940938e4fd8cc515377f9334c4a48679c7b /packages/backend/src/server/api/endpoints
parentmerge: pin corepack version (!885) (diff)
parentmerge: Add/fix moderation logs for many endpoints (resolves #911 and #969) (!... (diff)
downloadsharkey-0b5e197afb5f9c2519c22324706c3b27d5d3eea3.tar.gz
sharkey-0b5e197afb5f9c2519c22324706c3b27d5d3eea3.tar.bz2
sharkey-0b5e197afb5f9c2519c22324706c3b27d5d3eea3.zip
Merge branch 'develop' into release/2025.2.2
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/captcha/current.ts77
-rw-r--r--packages/backend/src/server/api/endpoints/admin/captcha/save.ts129
-rw-r--r--packages/backend/src/server/api/endpoints/admin/cw-user.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/copy.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts14
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/invite/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/nsfw-user.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/admin/promo/create.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/admin/reject-quotes.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/add.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/remove.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/silence-user.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unsilence-user.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts92
-rw-r--r--packages/backend/src/server/api/endpoints/channels/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/delete.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/update.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/update.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/emojis.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/federation/update-remote-user.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/flash/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/i.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/password-less.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/update-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/apps.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/change-password.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/claim-achievement.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/delete-account.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/regenerate-token.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/mute/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/edit.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/favorites/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/like.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/create.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/list.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/pages/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/pages/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/pages/update.ts25
-rw-r--r--packages/backend/src/server/api/endpoints/server-info.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts126
74 files changed, 956 insertions, 128 deletions
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 53b1c4c4ec..1a47f56bc6 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -6,7 +6,6 @@
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';
@@ -16,6 +15,7 @@ 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 { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
@@ -138,6 +139,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
approved: true,
});
+ if (me) {
+ await this.moderationLogService.log(me, 'createAccount', {
+ userId: account.id,
+ userUsername: account.username,
+ });
+ }
+
const res = await this.userEntityService.pack(account, account, {
schema: 'MeDetailed',
includeSecrets: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts
new file mode 100644
index 0000000000..41192c1926
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
+
+export const meta = {
+ tags: ['admin', 'captcha'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ // 実態はmetaの取得であるため
+ kind: 'read:admin:meta',
+
+ res: {
+ type: 'object',
+ properties: {
+ provider: {
+ type: 'string',
+ enum: supportedCaptchaProviders,
+ },
+ hcaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ mcaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ instanceUrl: { type: 'string', nullable: true },
+ },
+ },
+ recaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ turnstile: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ fc: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private captchaService: CaptchaService,
+ ) {
+ super(meta, paramDef, async () => {
+ return this.captchaService.get();
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts
new file mode 100644
index 0000000000..98ec278ebe
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts
@@ -0,0 +1,129 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['admin', 'captcha'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ // 実態はmetaの更新であるため
+ kind: 'write:admin:meta',
+
+ errors: {
+ invalidProvider: {
+ message: 'Invalid provider.',
+ code: 'INVALID_PROVIDER',
+ id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
+ httpStatusCode: 400,
+ },
+ invalidParameters: {
+ message: 'Invalid parameters.',
+ code: 'INVALID_PARAMETERS',
+ id: '26654194-410e-44e2-b42e-460ff6f92476',
+ httpStatusCode: 400,
+ },
+ noResponseProvided: {
+ message: 'No response provided.',
+ code: 'NO_RESPONSE_PROVIDED',
+ id: '40acbba8-0937-41fb-bb3f-474514d40afe',
+ httpStatusCode: 400,
+ },
+ requestFailed: {
+ message: 'Request failed.',
+ code: 'REQUEST_FAILED',
+ id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
+ httpStatusCode: 500,
+ },
+ verificationFailed: {
+ message: 'Verification failed.',
+ code: 'VERIFICATION_FAILED',
+ id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
+ httpStatusCode: 400,
+ },
+ unknown: {
+ message: 'unknown',
+ code: 'UNKNOWN',
+ id: 'f868d509-e257-42a9-99c1-42614b031a97',
+ httpStatusCode: 500,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ provider: {
+ type: 'string',
+ enum: supportedCaptchaProviders,
+ },
+ captchaResult: {
+ type: 'string', nullable: true,
+ },
+ sitekey: {
+ type: 'string', nullable: true,
+ },
+ secret: {
+ type: 'string', nullable: true,
+ },
+ instanceUrl: {
+ type: 'string', nullable: true,
+ },
+ },
+ required: ['provider'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private captchaService: CaptchaService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const result = await this.captchaService.save(ps.provider, {
+ sitekey: ps.sitekey,
+ secret: ps.secret,
+ instanceUrl: ps.instanceUrl,
+ captchaResult: ps.captchaResult,
+ });
+
+ if (!result.success) {
+ switch (result.error.code) {
+ case captchaErrorCodes.invalidProvider:
+ throw new ApiError({
+ ...meta.errors.invalidProvider,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.invalidParameters:
+ throw new ApiError({
+ ...meta.errors.invalidParameters,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.noResponseProvided:
+ throw new ApiError({
+ ...meta.errors.noResponseProvided,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.requestFailed:
+ throw new ApiError({
+ ...meta.errors.requestFailed,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.verificationFailed:
+ throw new ApiError({
+ ...meta.errors.verificationFailed,
+ message: result.error.message,
+ });
+ default:
+ throw new ApiError(meta.errors.unknown);
+ }
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/cw-user.ts b/packages/backend/src/server/api/endpoints/admin/cw-user.ts
new file mode 100644
index 0000000000..bdcfa6a0d9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/cw-user.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:cw-user',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ cw: { type: 'string', nullable: true },
+ },
+ required: ['userId', 'cw'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
+
+ private readonly globalEventService: GlobalEventService,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
+
+ // Skip if there's nothing to do
+ if (user.mandatoryCW === ps.cw) return;
+
+ // Log event first.
+ // This ensures that we don't "lose" the log if an error occurs
+ await this.moderationLogService.log(me, 'setMandatoryCW', {
+ newCW: ps.cw,
+ oldCW: user.mandatoryCW,
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
+ await this.usersRepository.update(ps.userId, {
+ // Collapse empty strings to null
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ mandatoryCW: ps.cw || null,
+ });
+
+ // Synchronize caches and other processes
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
index 747c9f48d0..8b4a450ccb 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
@@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
export const meta = {
tags: ['admin'],
@@ -30,14 +32,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
const files = await this.driveFilesRepository.findBy({
userId: ps.userId,
});
+ await this.moderationLogService.log(me, 'clearUserFiles', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file, false, me);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
index d420a929bd..9a7b3d5d62 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -25,9 +26,11 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.createCleanRemoteFilesJob();
+ await this.moderationLogService.log(me, 'clearRemoteFiles', {});
+ await this.queueService.createCleanRemoteFilesJob();
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
index d612572e2e..f5d20366cf 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
@@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -29,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -37,6 +38,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: IsNull(),
});
+ await this.moderationLogService.log(me, 'clearOwnerlessFiles', {
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
index f4fc79bdb3..795b579041 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ addAliases: ps.aliases,
+ });
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index b45a3c7156..1c5316a002 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -9,6 +9,7 @@ import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { FILE_TYPE_IMAGE } from '@/const.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -24,6 +25,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
+ unsupportedFileType: {
+ message: 'Unsupported file type.',
+ code: 'UNSUPPORTED_FILE_TYPE',
+ id: 'f7599d96-8750-af68-1633-9575d625c1a7',
+ },
duplicateName: {
message: 'Duplicate name.',
code: 'DUPLICATE_NAME',
@@ -47,15 +53,21 @@ export const paramDef = {
nullable: true,
description: 'Use `null` to reset the category.',
},
- aliases: { type: 'array', items: {
- type: 'string',
- } },
+ aliases: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
- roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
- type: 'string',
- } },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
},
required: ['name', 'fileId'],
} as const;
@@ -67,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
private customEmojiService: CustomEmojiService,
-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -78,11 +88,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
+ if (!FILE_TYPE_IMAGE.includes(driveFile.type)) throw new ApiError(meta.errors.unsupportedFileType);
if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null });
const emoji = await this.customEmojiService.add({
- driveFile,
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: ps.category?.normalize('NFC') ?? null,
aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [],
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index acd2494131..07ffa0b1c7 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -4,7 +4,6 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
@@ -88,10 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
const addedEmoji = await this.customEmojiService.add({
- driveFile,
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: emoji.category?.normalize('NFC') ?? null,
- aliases: emoji.aliases?.map(a => a.normalize('NFC')),
+ aliases: emoji.aliases.map(a => a.normalize('NFC')),
host: null,
license: emoji.license,
isSensitive: emoji.isSensitive,
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
index 8e5f69c894..ee7706f31a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
@@ -3,9 +3,12 @@
* 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 { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import type { DriveFilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
secure: true,
@@ -25,9 +28,16 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
+ @Inject(DI.driveFilesRepository)
+ private readonly driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.createImportCustomEmojisJob(me, ps.fileId);
+ const file = await driveFilesRepository.findOneByOrFail({ id: ps.fileId });
+ await this.moderationLogService.log(me, 'importCustomEmojis', {
+ fileName: file.name,
+ });
+ await this.queueService.createImportCustomEmojisJob(me, ps.fileId);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
index e78620eac1..066eb1c7d9 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ delAliases: ps.aliases,
+ });
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
index 69fc8e0cb5..8980ef0c86 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ setAliases: ps.aliases,
+ });
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
index 679a9f9c42..2510349210 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ category: ps.category,
+ });
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
index 4ba99faab7..a0205ae24a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ license: ps.license,
+ });
await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
});
}
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 071ddbef18..fd6db9c4ab 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -86,7 +86,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const error = await this.customEmojiService.update({
...required,
- driveFile,
+ originalUrl: driveFile != null ? driveFile.url : undefined,
+ publicUrl: driveFile != null ? (driveFile.webpublicUrl ?? driveFile.url) : undefined,
+ fileType: driveFile != null ? (driveFile.webpublicType ?? driveFile.type) : undefined,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
index 4a54c26009..89fd4be99c 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -30,7 +31,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -38,6 +39,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userHost: ps.host,
});
+ await this.moderationLogService.log(me, 'clearInstanceFiles', {
+ host: ps.host,
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 601c898f52..e5d85e1d57 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -35,6 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const followings = await this.followingsRepository.findBy([
@@ -51,6 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
+ await this.moderationLogService.log(me, 'severFollowRelations', {
+ host: ps.host,
+ });
+
this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true })));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
index daf19c4435..24d0b8527c 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -27,6 +27,7 @@ export const paramDef = {
isNSFW: { type: 'boolean' },
rejectReports: { type: 'boolean' },
moderationNote: { type: 'string' },
+ rejectQuotes: { type: 'boolean' },
},
required: ['host'],
} as const;
@@ -59,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
suspensionState,
isNSFW: ps.isNSFW,
rejectReports: ps.rejectReports,
+ rejectQuotes: ps.rejectQuotes,
moderationNote: ps.moderationNote,
});
@@ -92,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
+ if (ps.rejectQuotes != null && instance.rejectQuotes !== ps.rejectQuotes) {
+ const message = ps.rejectReports ? 'rejectQuotesInstance' : 'acceptQuotesInstance';
+ this.moderationLogService.log(me, message, {
+ id: instance.id,
+ host: instance.host,
+ });
+ }
+
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,
diff --git a/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts b/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts
new file mode 100644
index 0000000000..5695866265
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: marie and sharkey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import webpush from 'web-push';
+const { generateVAPIDKeys } = webpush;
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:meta',
+} as const;
+
+export const paramDef = {} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const keys = await generateVAPIDKeys();
+
+ return { public: keys.publicKey, private: keys.privateKey };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
index 5ecae3161a..e52b177e2b 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
@@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
+ createdBy: me,
+ createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6495e3b7da..436dcf27cb 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -391,6 +391,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ robotsTxt: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
enableIdenticonGeneration: {
type: 'boolean',
optional: false, nullable: false,
@@ -708,6 +712,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
+ robotsTxt: instance.robotsTxt,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
bannedEmailDomains: instance.bannedEmailDomains,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
index d3fa4251dd..194e793eda 100644
--- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,22 +29,25 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'nsfwUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: true,
});
+
+ await this.cacheService.userProfileCache.refresh(ps.userId);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
index 1d32c6cc00..63fe2988ee 100644
--- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
@@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { PromoNotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -46,7 +48,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.promoNotesRepository)
private promoNotesRepository: PromoNotesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -61,6 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.alreadyPromoted);
}
+ const user = await this.cacheService.findUserById(note.userId);
+ await this.moderationLogService.log(me, 'createPromo', {
+ noteId: note.id,
+ noteUserId: user.id,
+ noteUserUsername: user.username,
+ noteUserHost: user.host,
+ });
+
await this.promoNotesRepository.insert({
noteId: note.id,
expiresAt: new Date(ps.expiresAt),
diff --git a/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts b/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts
new file mode 100644
index 0000000000..78f94ceeff
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:reject-quotes',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ rejectQuotes: { type: 'boolean', nullable: false },
+ },
+ required: ['userId', 'rejectQuotes'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
+
+ private readonly globalEventService: GlobalEventService,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
+
+ // Skip if there's nothing to do
+ if (user.rejectQuotes === ps.rejectQuotes) return;
+
+ // Log event first.
+ // This ensures that we don't "lose" the log if an error occurs
+ await this.moderationLogService.log(me, ps.rejectQuotes ? 'rejectQuotesUser' : 'acceptQuotesUser', {
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
+ await this.usersRepository.update(ps.userId, {
+ rejectQuotes: ps.rejectQuotes,
+ });
+
+ // Synchronize caches and other processes
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
index 3d7bc4567e..129f69aca9 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
import { ApiError } from '../../../error.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -64,6 +65,7 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
try {
@@ -72,6 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.invalidUrl);
}
+ await this.moderationLogService.log(me, 'addRelay', {
+ inbox: ps.inbox,
+ });
+
return await this.relayService.addRelay(ps.inbox);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
index 1f6e773cd4..292f21fde9 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,9 +28,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- return await this.relayService.removeRelay(ps.inbox);
+ await this.moderationLogService.log(me, 'removeRelay', {
+ inbox: ps.inbox,
+ });
+ await this.relayService.removeRelay(ps.inbox);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
index 7e6045049a..eed21c6576 100644
--- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
@@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -29,24 +32,32 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private roleService: RoleService,
+ private readonly usersRepository: UsersRepository,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly roleService: RoleService,
+ private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
+ const user = await this.cacheService.findUserById(ps.userId);
if (await this.roleService.isModerator(user)) {
throw new Error('cannot silence moderator account');
}
+ await this.moderationLogService.log(me, 'silenceUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
await this.usersRepository.update(user.id, {
isSilenced: true,
});
+
+ this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
+ id: user.id,
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
index 26588365e1..52a0c076be 100644
--- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,18 +29,19 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'unNsfwUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: false,
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
index f92be0d8e0..9318943785 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
@@ -7,6 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -28,18 +31,27 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
+ private readonly usersRepository: UsersRepository,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'unSilenceUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.usersRepository.update(user.id, {
isSilenced: false,
});
+
+ this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
+ id: user.id,
+ });
});
}
}
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 72f428d85f..b3733d3d39 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -149,6 +149,7 @@ export const paramDef = {
enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
+ robotsTxt: { type: 'string', nullable: true },
enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } },
bannedEmailDomains: { type: 'array', items: { type: 'string' } },
@@ -636,6 +637,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableAchievements = ps.enableAchievements;
}
+ if (ps.robotsTxt !== undefined) {
+ set.robotsTxt = ps.robotsTxt;
+ }
+
if (ps.enableIdenticonGeneration !== undefined) {
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
}
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 616a77e337..22bec8ef95 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -4,11 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
+import { isActor, isPost, getApId, getNullableApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
@@ -18,7 +17,10 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApiError } from '../../error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
export const meta = {
tags: ['federation'],
@@ -26,12 +28,38 @@ export const meta = {
requireCredential: true,
kind: 'read:account',
+ // Up to 30 calls, then 1 per 1/2 second
limit: {
- duration: ms('1minute'),
max: 30,
+ dripRate: 500,
},
errors: {
+ federationNotAllowed: {
+ message: 'Federation for this host is not allowed.',
+ code: 'FEDERATION_NOT_ALLOWED',
+ id: '974b799e-1a29-4889-b706-18d4dd93e266',
+ },
+ uriInvalid: {
+ message: 'URI is invalid.',
+ code: 'URI_INVALID',
+ id: '1a5eab56-e47b-48c2-8d5e-217b897d70db',
+ },
+ requestFailed: {
+ message: 'Request failed.',
+ code: 'REQUEST_FAILED',
+ id: '81b539cf-4f57-4b29-bc98-032c33c0792e',
+ },
+ responseInvalid: {
+ message: 'Response from remote server is invalid.',
+ code: 'RESPONSE_INVALID',
+ id: '70193c39-54f3-4813-82f0-70a680f7495b',
+ },
+ responseInvalidIdHostNotMatch: {
+ message: 'Requested URI and response URI host does not match.',
+ code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
+ id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
+ },
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
@@ -94,6 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
private apNoteService: ApNoteService,
+ private readonly apRequestService: ApRequestService,
+ private readonly instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, me) => {
const object = await this.fetchAny(ps.uri, me);
@@ -110,7 +140,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
- if (!this.utilityService.isFederationAllowedUri(uri)) return null;
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
+ throw new ApiError(meta.errors.federationNotAllowed);
+ }
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@@ -118,6 +150,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]));
if (local != null) return local;
+ // No local object found with that uri.
+ // Before we fetch, resolve the URI in case it has a cross-origin redirect or anything like that.
+ // Resolver.resolve() uses strict verification, which is overly paranoid for a user-provided lookup.
+ uri = await this.resolveCanonicalUri(uri); // eslint-disable-line no-param-reassign
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
+ throw new ApiError(meta.errors.federationNotAllowed);
+ }
+
const host = this.utilityService.extractDbHost(uri);
// local object, not found in db? fail
@@ -125,7 +165,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
- const object = await resolver.resolve(uri) as any;
+ const object = await resolver.resolve(uri).catch((err) => {
+ if (err instanceof IdentifiableError) {
+ switch (err.id) {
+ // resolve
+ case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
+ throw new ApiError(meta.errors.uriInvalid);
+ case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
+ case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
+ throw new ApiError(meta.errors.requestFailed);
+ case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
+ throw new ApiError(meta.errors.federationNotAllowed);
+ case '72180409-793c-4973-868e-5a118eb5519b':
+ case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
+ throw new ApiError(meta.errors.responseInvalid);
+ case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
+ throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
+
+ // resolveLocal
+ case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
+ throw new ApiError(meta.errors.uriInvalid);
+ case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
+ case '06ae3170-1796-4d93-a697-2611ea6d83b6':
+ throw new ApiError(meta.errors.noSuchObject);
+ case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
+ throw new ApiError(meta.errors.responseInvalid);
+ }
+ }
+
+ throw new ApiError(meta.errors.requestFailed);
+ });
+
+ if (object.id == null) {
+ throw new ApiError(meta.errors.responseInvalid);
+ }
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索
@@ -167,4 +240,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return null;
}
+
+ /**
+ * Resolves an arbitrary URI to its canonical, post-redirect form.
+ */
+ private async resolveCanonicalUri(uri: string): Promise<string> {
+ const user = await this.instanceActorService.getInstanceActor();
+ const res = await this.apRequestService.signedGet(uri, user, true);
+ return getNullableApId(res) ?? uri;
+ }
}
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index d2a75225ed..7cca688fda 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['channels'],
diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts
index 8e821da0da..5df212415d 100644
--- a/packages/backend/src/server/api/endpoints/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
-import { Brackets } from 'typeorm';
export const meta = {
tags: ['drive'],
@@ -45,7 +45,7 @@ export const paramDef = {
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
- searchQuery: { type: 'string', default: '' }
+ searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
index 7a009b12a1..3065bb6711 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -11,7 +11,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 94a0e673a3..306a646785 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -11,7 +11,6 @@ import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
import type { Config } from '@/config.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts
index 1245706b0d..525cb8c5d6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders.ts
@@ -42,7 +42,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
- searchQuery: { type: 'string', default: '' }
+ searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index cd47c0fc68..8d51d09ea6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -10,7 +10,6 @@ import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityServi
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index 3cc7f89ab9..4909c948e3 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -59,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const emojis = await this.emojisRepository.createQueryBuilder()
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
- .orderBy('LOWER(name)', 'ASC')
+ .addOrderBy('LOWER(name)', 'ASC')
.getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),
diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
index 3ec9522c44..5217f79065 100644
--- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
+++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
@@ -13,10 +13,11 @@ export const meta = {
requireCredential: false,
- // 2 calls per second
+ // Up to 10 calls, then 4 / second.
+ // This allows for reliable automation.
limit: {
- duration: 1000,
- max: 2,
+ max: 10,
+ dripRate: 250,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts
index 1010567113..271a44f8d5 100644
--- a/packages/backend/src/server/api/endpoints/flash/delete.ts
+++ b/packages/backend/src/server/api/endpoints/flash/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['flashs'],
@@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
- flash,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
index 68478ba55c..9854358e3e 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['gallery'],
@@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
- post,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index 9347c9ca27..48a2e3b40a 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -31,10 +31,12 @@ export const meta = {
},
},
- // 3 calls per second
+ // up to 20 calls, then 1 per second.
+ // This handles bursty traffic when all tabs reload as a group
limit: {
- duration: 1000,
- max: 3,
+ max: 20,
+ dripSize: 1,
+ dripRate: 1000,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index 34b6907338..12d5ef443d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -5,12 +5,12 @@
import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 084d4af658..370d9915a3 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -14,7 +15,6 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
@@ -63,8 +63,8 @@ export const paramDef = {
required: ['password', 'name', 'credential'],
} as const;
-// eslint-disable-next-line import/no-default-export
@Injectable()
+// eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index a1c965f603..cd520cff0f 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index 6ab50a57c9..893ea30391 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -6,13 +6,13 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index 888d0fc6ef..d27c14c69b 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -8,13 +8,13 @@ import * as argon2 from 'argon2';
import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 614fd0c498..b01e452056 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 2773825373..2fe4fdc4c0 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
@@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
index eb8a63b3dc..4a41c7b984 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -5,13 +5,13 @@
//import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 661fa257a6..f290ff6844 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
- permission: token.permission,
+ permission: token.app ? token.app.permission : token.permission,
})));
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index f131c7e9d1..4069683740 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -6,11 +6,11 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
index ebb25ebf1c..52f96e5bbd 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import type { MiMeta } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index 565eaaafc0..10fb923d4f 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index 814ffb5488..38328bb7d4 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 09c06a108d..f74452e2af 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -133,6 +133,12 @@ export const meta = {
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
httpStatusCode: 422,
},
+
+ maxCwLength: {
+ message: 'You tried setting a default content warning which is too long.',
+ code: 'MAX_CW_LENGTH',
+ id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
+ },
},
res: {
@@ -243,6 +249,12 @@ export const paramDef = {
uniqueItems: true,
items: { type: 'string' },
},
+ defaultCW: { type: 'string', nullable: true },
+ defaultCWPriority: {
+ type: 'string',
+ enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
+ nullable: false,
+ },
},
} as const;
@@ -494,6 +506,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
}
+ let defaultCW = ps.defaultCW;
+ if (defaultCW !== undefined) {
+ if (defaultCW === '') defaultCW = null;
+ if (defaultCW && defaultCW.length > this.config.maxCwLength) {
+ throw new ApiError(meta.errors.maxCwLength);
+ }
+
+ profileUpdates.defaultCW = defaultCW;
+ }
+ if (ps.defaultCWPriority !== undefined) {
+ profileUpdates.defaultCWPriority = ps.defaultCWPriority;
+ }
+
//#region emojis/tags
let emojis = [] as string[];
@@ -592,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
- const doc = window.document;
+ const doc: Document = window.document;
const myLink = `${this.config.url}/@${user.username}`;
diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts
index 1e14bafc87..ccc98c4b10 100644
--- a/packages/backend/src/server/api/endpoints/mute/delete.ts
+++ b/packages/backend/src/server/api/endpoints/mute/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['account'],
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index d1cf0123dc..b0f32bfda8 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -143,6 +143,12 @@ export const meta = {
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
+
+ quoteDisabledForUser: {
+ message: 'You do not have permission to create quote posts.',
+ code: 'QUOTE_DISABLED_FOR_USER',
+ id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
+ },
},
} as const;
@@ -415,6 +421,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
+ throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index dc94c78e75..cc2293c5d6 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -176,6 +176,12 @@ export const meta = {
id: '33510210-8452-094c-6227-4a6c05d99f02',
},
+ quoteDisabledForUser: {
+ message: 'You do not have permission to create quote posts.',
+ code: 'QUOTE_DISABLED_FOR_USER',
+ id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
+ },
+
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
@@ -469,6 +475,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
+ throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
index 19a6a5af54..742c872d45 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
@@ -4,12 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import type { NoteFavoritesRepository } from '@/models/_.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['notes', 'favorites'],
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index c45fcd7c5c..0f2592bd78 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -12,8 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
-import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts
index 9068de2865..d6511faf95 100644
--- a/packages/backend/src/server/api/endpoints/notes/like.ts
+++ b/packages/backend/src/server/api/endpoints/notes/like.ts
@@ -1,5 +1,5 @@
-import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ReactionService } from '@/core/ReactionService.js';
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
index c6032fbdae..0c2e00ee8d 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
@@ -14,12 +14,10 @@ import type {
NotesRepository,
BlockingsRepository,
DriveFilesRepository,
- ChannelsRepository,
NoteScheduleRepository,
} from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
-import type { MiChannel } from '@/models/Channel.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
@@ -210,9 +208,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- @Inject(DI.channelsRepository)
- private channelsRepository: ChannelsRepository,
-
private queueService: QueueService,
private roleService: RoleService,
private idService: IdService,
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
index 4895733d4e..4dd3d7a81a 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
@@ -7,12 +7,14 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
-import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js';
+import type { MiNote, MiUser, MiNoteSchedule, NoteScheduleRepository, NotesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { QueryService } from '@/core/QueryService.js';
import { Packed } from '@/misc/json-schema.js';
import { noteVisibilities } from '@/types.js';
+import { bindThis } from '@/decorators.js';
export const meta = {
tags: ['notes'],
@@ -81,7 +83,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.noteScheduleRepository)
private noteScheduleRepository: NoteScheduleRepository,
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
private userEntityService: UserEntityService,
+ private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
private queryService: QueryService,
) {
@@ -106,6 +112,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: string;
scheduledAt: string;
}[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => {
+ const renote = await this.fetchNote(item.note.renote, me);
+ const reply = await this.fetchNote(item.note.reply, me);
+
return {
...item,
scheduledAt: item.scheduledAt.toISOString(),
@@ -115,12 +124,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
user: user,
visibility: item.note.visibility ?? 'public',
reactionAcceptance: item.note.reactionAcceptance ?? null,
- visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
+ visibleUsers: item.note.visibleUsers ? await this.userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
fileIds: item.note.files ? item.note.files : [],
files: await this.driveFileEntityService.packManyByIds(item.note.files),
createdAt: item.scheduledAt.toISOString(),
isSchedule: true,
id: item.id,
+ renote, reply,
+ renoteId: item.note.renote,
+ replyId: item.note.reply,
},
};
}));
@@ -128,4 +140,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return scheduleNotesPack;
});
}
+
+ @bindThis
+ private async fetchNote(
+ id: MiNote['id'] | null | undefined,
+ me: MiUser,
+ ): Promise<Packed<'Note'> | null> {
+ if (id) {
+ const note = await this.notesRepository.findOneBy({ id });
+ if (note) {
+ note.reactionAndUserPairCache ??= [];
+ return this.noteEntityService.pack(note, me);
+ }
+ }
+ return null;
+ }
}
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 227ac0ebbf..6bba7bf37e 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere('note.visibility = \'public\'')
+ .andWhere("note.visibility IN ('public', 'home')") // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()`
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index eca55cd085..f46f4d2adb 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { SearchService } from '@/core/SearchService.js';
+import { fileTypeCategories, SearchService } from '@/core/SearchService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
@@ -52,7 +52,11 @@ export const paramDef = {
type: 'string',
description: 'The local host is represented with `.`.',
},
- filetype: { type: 'string', nullable: true },
+ filetype: {
+ type: 'string',
+ nullable: true,
+ enum: fileTypeCategories,
+ },
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
order: { type: 'string' },
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index fa03b0b457..6de5fe3d44 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -7,7 +7,7 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
-import { MiPage } from '@/models/Page.js';
+import { MiPage, pageNameSchema } from '@/models/Page.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -51,7 +51,7 @@ export const paramDef = {
type: 'object',
properties: {
title: { type: 'string' },
- name: { type: 'string', minLength: 1 },
+ name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts
index c2c3215f49..c95f8ecf6b 100644
--- a/packages/backend/src/server/api/endpoints/pages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/pages/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['pages'],
@@ -79,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
- page,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index f11bbbcb1a..a6aeb6002e 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -10,6 +10,7 @@ import type { PagesRepository, DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { pageNameSchema } from '@/models/Page.js';
export const meta = {
tags: ['pages'],
@@ -31,13 +32,11 @@ export const meta = {
code: 'NO_SUCH_PAGE',
id: '21149b9e-3616-4778-9592-c4ce89f5a864',
},
-
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '3c15cd52-3b4b-4274-967d-6456fc4f792b',
},
-
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
@@ -56,7 +55,7 @@ export const paramDef = {
properties: {
pageId: { type: 'string', format: 'misskey:id' },
title: { type: 'string' },
- name: { type: 'string', minLength: 1 },
+ name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
@@ -102,15 +101,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
- await this.pagesRepository.findBy({
- id: Not(ps.pageId),
- userId: me.id,
- name: ps.name,
- }).then(result => {
- if (result.length > 0) {
- throw new ApiError(meta.errors.nameAlreadyExists);
- }
- });
+ if (ps.name != null) {
+ await this.pagesRepository.findBy({
+ id: Not(ps.pageId),
+ userId: me.id,
+ name: ps.name,
+ }).then(result => {
+ if (result.length > 0) {
+ throw new ApiError(meta.errors.nameAlreadyExists);
+ }
+ });
+ }
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index e765163f8e..528de76707 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -64,10 +64,11 @@ export const meta = {
},
},
- // 2 calls per second
+ // 24 calls, then 7 per second-ish (1 for each type of server info graph)
limit: {
- duration: 1000,
- max: 2,
+ max: 24,
+ dripSize: 7,
+ dripRate: 900,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index e659c46713..c7016d8d32 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -58,6 +58,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isInstanceMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ memo: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
{
@@ -103,6 +111,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isInstanceMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ memo: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
},
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 7ebca78a7d..118362149d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -57,10 +57,10 @@ export const meta = {
},
},
- // 5 calls per 2 seconds
+ // up to 50 calls @ 4 per second
limit: {
- duration: 1000 * 2,
- max: 5,
+ max: 50,
+ dripRate: 250,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
new file mode 100644
index 0000000000..9426318e34
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canManageCustomEmojis',
+ kind: 'read:admin:emoji',
+
+ res: {
+ type: 'object',
+ properties: {
+ emojis: {
+ type: 'array',
+ items: {
+ type: 'object',
+ ref: 'EmojiDetailedAdmin',
+ },
+ },
+ count: { type: 'integer' },
+ allCount: { type: 'integer' },
+ allPages: { type: 'integer' },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'object',
+ nullable: true,
+ properties: {
+ updatedAtFrom: { type: 'string' },
+ updatedAtTo: { type: 'string' },
+ name: { type: 'string' },
+ host: { type: 'string' },
+ uri: { type: 'string' },
+ publicUrl: { type: 'string' },
+ originalUrl: { type: 'string' },
+ type: { type: 'string' },
+ aliases: { type: 'string' },
+ category: { type: 'string' },
+ license: { type: 'string' },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ hostType: {
+ type: 'string',
+ enum: fetchEmojisHostTypes,
+ default: 'all',
+ },
+ roleIds: {
+ type: 'array',
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ },
+ },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ page: { type: 'integer' },
+ sortKeys: {
+ type: 'array',
+ default: ['-id'],
+ items: {
+ type: 'string',
+ enum: fetchEmojisSortKeys,
+ },
+ },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private customEmojiService: CustomEmojiService,
+ private emojiEntityService: EmojiEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const q = ps.query;
+ const result = await this.customEmojiService.fetchEmojis(
+ {
+ query: {
+ updatedAtFrom: q?.updatedAtFrom,
+ updatedAtTo: q?.updatedAtTo,
+ name: q?.name,
+ host: q?.host,
+ uri: q?.uri,
+ publicUrl: q?.publicUrl,
+ type: q?.type,
+ aliases: q?.aliases,
+ category: q?.category,
+ license: q?.license,
+ isSensitive: q?.isSensitive,
+ localOnly: q?.localOnly,
+ hostType: q?.hostType,
+ roleIds: q?.roleIds,
+ },
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ },
+ {
+ limit: ps.limit,
+ page: ps.page,
+ sortKeys: ps.sortKeys,
+ },
+ );
+
+ return {
+ emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis),
+ count: result.count,
+ allCount: result.allCount,
+ allPages: result.allPages,
+ };
+ });
+ }
+}