summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2024-12-31 02:30:13 +0000
committerJulia <julia@insertdomain.name>2024-12-31 02:30:13 +0000
commit4c0bbddd0fba7e0d76fb484312e691ee29fe5858 (patch)
tree4bb1a3a2a79c679ac021a2199bd526be469524d4 /packages/backend/src/server/api/endpoints
parentmerge: fixes for 2024.9.4 (if we want to) (!770) (diff)
parentBump version (diff)
downloadsharkey-4c0bbddd0fba7e0d76fb484312e691ee29fe5858.tar.gz
sharkey-4c0bbddd0fba7e0d76fb484312e691ee29fe5858.tar.bz2
sharkey-4c0bbddd0fba7e0d76fb484312e691ee29fe5858.zip
merge: Bump stable version (!842)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/842
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts97
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/announcements/create.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts57
-rw-r--r--packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-account.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts55
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/stats.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts58
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/charts/active-users.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/ap-request.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/drive.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/federation.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/instance.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/notes.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/user/drive.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/user/following.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/user/notes.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/user/pv.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/user/reactions.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/charts/users.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/emojis.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/endpoint.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/flash/featured.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts97
-rw-r--r--packages/backend/src/server/api/endpoints/notes/following.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/create.ts372
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/delete.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/list.ts131
-rw-r--r--packages/backend/src/server/api/endpoints/notes/show.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/notes/versions.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/users/notes.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/users/report-abuse.ts4
40 files changed, 1089 insertions, 146 deletions
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index cf3f257ca6..0dbfaae054 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -71,9 +71,22 @@ export const meta = {
},
assignee: {
type: 'object',
- nullable: true, optional: true,
+ nullable: true, optional: false,
ref: 'UserDetailedNotMe',
},
+ forwarded: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
+ resolvedAs: {
+ type: 'string',
+ nullable: true, optional: false,
+ enum: ['accept', 'reject', null],
+ },
+ moderationNote: {
+ type: 'string',
+ nullable: false, optional: false,
+ },
},
},
},
@@ -88,7 +101,6 @@ export const paramDef = {
state: { type: 'string', nullable: true, default: null },
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
- forwarded: { type: 'boolean', default: false },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 7754899b95..53b1c4c4ec 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -3,33 +3,36 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
import { MiAccessToken, MiUser } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
-import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin'],
- res: {
- type: 'object',
- optional: false, nullable: false,
- ref: 'MeDetailed',
- properties: {
- token: {
- type: 'string',
- optional: false, nullable: false,
- },
+ errors: {
+ accessDenied: {
+ message: 'Access denied.',
+ code: 'ACCESS_DENIED',
+ id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+ },
+
+ wrongInitialPassword: {
+ message: 'Initial password is incorrect.',
+ code: 'INCORRECT_INITIAL_PASSWORD',
+ id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62',
},
- },
- errors: {
// From ApiCallService.ts
noCredential: {
message: 'Credential required.',
@@ -51,6 +54,18 @@ export const meta = {
},
},
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'MeDetailed',
+ properties: {
+ token: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+
// Required token permissions, but we need to check them manually.
// ApiCallService checks access in a way that would prevent creating the first account.
softPermissions: [
@@ -64,6 +79,7 @@ export const paramDef = {
properties: {
username: localUsernameSchema,
password: passwordSchema,
+ setupPassword: { type: 'string', nullable: true },
},
required: ['username', 'password'],
} as const;
@@ -71,13 +87,49 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
private roleService: RoleService,
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, _me, token) => {
- await this.ensurePermissions(_me, token);
+ const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
+ const realUsers = await this.instanceActorService.realLocalUsersPresent();
+
+ if (!realUsers && me == null && token == null) {
+ // 初回セットアップの場合
+ if (this.config.setupPassword != null) {
+ // 初期パスワードが設定されている場合
+ if (ps.setupPassword !== this.config.setupPassword) {
+ // 初期パスワードが違う場合
+ throw new ApiError(meta.errors.wrongInitialPassword);
+ }
+ } else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') {
+ // 初期パスワードが設定されていないのに初期パスワードが入力された場合
+ throw new ApiError(meta.errors.wrongInitialPassword);
+ }
+ } else {
+ if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
+ // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
+ throw new ApiError(meta.errors.noPermission);
+ }
+
+ if (me && !await this.roleService.isAdministrator(me)) {
+ // Only administrators (including root) can create users.
+ throw new ApiError(meta.errors.noAdmin);
+ }
+
+ // Anonymous access is only allowed for initial instance setup (this check may be redundant)
+ if (!me && realUsers) {
+ throw new ApiError(meta.errors.noCredential);
+ }
+ }
const { account, secret } = await this.signupService.signup({
username: ps.username,
@@ -96,21 +148,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return res;
});
}
-
- private async ensurePermissions(me: MiUser | null, token: MiAccessToken | null): Promise<void> {
- // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
- if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
- throw new ApiError(meta.errors.noPermission);
- }
-
- // Only administrators (including root) can create users.
- if (me && !await this.roleService.isAdministrator(me)) {
- throw new ApiError(meta.errors.noAdmin);
- }
-
- // Anonymous access is only allowed for initial instance setup.
- if (!me && await this.instanceActorService.realLocalUsersPresent()) {
- throw new ApiError(meta.errors.noCredential);
- }
- }
}
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
index 01dea703a3..ece1984cff 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
@@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot delete a root account');
}
- await this.deleteAccoountService.deleteAccount(user);
+ await this.deleteAccoountService.deleteAccount(user, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
index 2dae1df87d..b8bfda73a4 100644
--- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts
@@ -55,7 +55,7 @@ export const paramDef = {
properties: {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
- imageUrl: { type: 'string', nullable: true, minLength: 1 },
+ imageUrl: { type: 'string', nullable: true, minLength: 0 },
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' },
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
forExistingUsers: { type: 'boolean', default: false },
@@ -76,7 +76,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updatedAt: null,
title: ps.title,
text: ps.text,
- imageUrl: ps.imageUrl,
+ /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
+ imageUrl: ps.imageUrl || null,
icon: ps.icon,
display: ps.display,
forExistingUsers: ps.forExistingUsers,
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
index fd21309818..87d80cbe80 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
+import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['admin'],
@@ -13,6 +14,49 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ createdAt: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'date-time',
+ },
+ updatedAt: {
+ type: 'string',
+ optional: false, nullable: true,
+ format: 'date-time',
+ },
+ name: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ description: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ url: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ roleIdsThatCanBeUsedThisDecoration: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ },
+ },
+ },
} as const;
export const paramDef = {
@@ -32,14 +76,25 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
+ private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
- await this.avatarDecorationService.create({
+ const created = await this.avatarDecorationService.create({
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
+
+ return {
+ id: created.id,
+ createdAt: this.idService.parse(created.id).date.toISOString(),
+ updatedAt: null,
+ name: created.name,
+ description: created.description,
+ url: created.url,
+ roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
+ };
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
index aee90023e1..d785f085ac 100644
--- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts
@@ -4,10 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
-import type { MiAnnouncement } from '@/models/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts
index b6f0f22d60..9065a71f6a 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-account.ts
@@ -33,13 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private deleteAccountService: DeleteAccountService,
) {
- super(meta, paramDef, async (ps) => {
+ super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) {
return;
}
- await this.deleteAccountService.deleteAccount(user);
+ await this.deleteAccountService.deleteAccount(user, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 3caa0f84a3..071ddbef18 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@@ -79,25 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
- let emojiId;
- if (ps.id) {
- emojiId = ps.id;
- const emoji = await this.customEmojiService.getEmojiById(ps.id);
- if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
- if (nameNfc && (nameNfc !== emoji.name)) {
- const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
- if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
- }
- } else {
- if (!nameNfc) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
- const emoji = await this.customEmojiService.getEmojiByName(nameNfc);
- if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
- emojiId = emoji.id;
- }
+ // JSON schemeのanyOfの型変換がうまくいっていないらしい
+ const required = { id: ps.id, name: nameNfc } as
+ | { id: MiEmoji['id']; name?: string }
+ | { id?: MiEmoji['id']; name: string };
- await this.customEmojiService.update(emojiId, {
+ const error = await this.customEmojiService.update({
+ ...required,
driveFile,
- name: nameNfc,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,
@@ -105,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}, me);
+
+ switch (error) {
+ case null: return;
+ case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
+ case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
+ }
+ // 網羅性チェック
+ const mustBeNever: never = error;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.forward(report.id, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6e368eff43..6495e3b7da 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -81,6 +81,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ enableTestcaptcha: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -189,6 +193,13 @@ export const meta = {
type: 'string',
},
},
+ prohibitedWordsForNameOfUser: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ },
+ },
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
@@ -368,6 +379,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ enableStatsForFederatedInstances: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
enableServerMachineStats: {
type: 'boolean',
optional: false, nullable: false,
@@ -614,6 +629,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
turnstileSiteKey: instance.turnstileSiteKey,
enableFC: instance.enableFC,
fcSiteKey: instance.fcSiteKey,
+ enableTestcaptcha: instance.enableTestcaptcha,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@@ -642,6 +658,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
+ prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser,
preservedUsernames: instance.preservedUsernames,
bubbleInstances: instance.bubbleInstances,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
@@ -688,6 +705,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
truemailAuthKey: instance.truemailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
+ enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index d7f9e4eaa3..e2bd38aac6 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
+import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, ScheduleNotePostQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -55,6 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
+ @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
) {
super(meta, paramDef, async (ps, me) => {
const deliverJobCounts = await this.deliverQueue.getJobCounts();
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 9b79100fcf..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
- forward: { type: 'boolean', default: false },
+ resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
},
required: ['reportId'],
} as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport);
}
- await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
+ await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 5f16519403..cc65ed2cf0 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -72,13 +72,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}
case 'moderator': {
- const moderatorIds = await this.roleService.getModeratorIds(false);
+ const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
if (moderatorIds.length === 0) return [];
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
break;
}
case 'adminOrModerator': {
- const adminOrModeratorIds = await this.roleService.getModeratorIds();
+ const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
if (adminOrModeratorIds.length === 0) return [];
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
break;
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ moderationNote: { type: 'string' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.update(report.id, {
+ moderationNote: ps.moderationNote,
+ }, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 98760bbcc3..72f428d85f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -46,6 +46,11 @@ export const paramDef = {
type: 'string',
},
},
+ prohibitedWordsForNameOfUser: {
+ type: 'array', nullable: true, items: {
+ type: 'string',
+ },
+ },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
@@ -84,6 +89,7 @@ export const paramDef = {
enableFC: { type: 'boolean' },
fcSiteKey: { type: 'string', nullable: true },
fcSecretKey: { type: 'string', nullable: true },
+ enableTestcaptcha: { type: 'boolean' },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -140,6 +146,7 @@ export const paramDef = {
truemailAuthKey: { type: 'string', nullable: true },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
+ enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
enableIdenticonGeneration: { type: 'boolean' },
@@ -230,6 +237,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
}
+ if (Array.isArray(ps.prohibitedWordsForNameOfUser)) {
+ set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean);
+ }
if (Array.isArray(ps.silencedHosts)) {
let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
@@ -390,6 +400,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableFC = ps.enableFC;
}
+ if (ps.enableTestcaptcha !== undefined) {
+ set.enableTestcaptcha = ps.enableTestcaptcha;
+ }
+
if (ps.fcSiteKey !== undefined) {
set.fcSiteKey = ps.fcSiteKey;
}
@@ -610,6 +624,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
+ if (ps.enableStatsForFederatedInstances !== undefined) {
+ set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances;
+ }
+
if (ps.enableServerMachineStats !== undefined) {
set.enableServerMachineStats = ps.enableServerMachineStats;
}
@@ -709,7 +727,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (Array.isArray(ps.federationHosts)) {
- set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+ set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
const before = await this.metaService.fetch(true);
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 4232bc6e39..616a77e337 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -137,6 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (local != null) return local;
}
+ // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
return await this.mergePack(
me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts
index f6c0c045df..dcdcf46d0b 100644
--- a/packages/backend/src/server/api/endpoints/charts/active-users.ts
+++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts
index 4c5c0d5d20..28c64229e7 100644
--- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts
+++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts
index 8210ec8fe7..69ff3c5d7a 100644
--- a/packages/backend/src/server/api/endpoints/charts/drive.ts
+++ b/packages/backend/src/server/api/endpoints/charts/drive.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts
index 56a5dbea31..bd870cc3d9 100644
--- a/packages/backend/src/server/api/endpoints/charts/federation.ts
+++ b/packages/backend/src/server/api/endpoints/charts/federation.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts
index 7f79e1356d..765bf024ee 100644
--- a/packages/backend/src/server/api/endpoints/charts/instance.ts
+++ b/packages/backend/src/server/api/endpoints/charts/instance.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts
index b3660b558b..ecac436311 100644
--- a/packages/backend/src/server/api/endpoints/charts/notes.ts
+++ b/packages/backend/src/server/api/endpoints/charts/notes.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
index 716c41f385..98ec40ade2 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts
index b67b5ca338..cb3dd36bab 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/following.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
index e5587cab86..0742a21210 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts
index cbae3a21c1..a220381b00 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
index d734240742..3bb33622c2 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts
index 6e1a8ebd4f..b5452517ab 100644
--- a/packages/backend/src/server/api/endpoints/charts/users.ts
+++ b/packages/backend/src/server/api/endpoints/charts/users.ts
@@ -17,10 +17,11 @@ export const meta = {
allowGet: true,
cacheSec: 60 * 60,
- // 10 calls per 5 seconds
+ // Burst up to 100, then 2/sec average
limit: {
- duration: 1000 * 5,
- max: 10,
+ type: 'bucket',
+ size: 100,
+ dripRate: 500,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index d5a14ca8f4..3cc7f89ab9 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -56,16 +56,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const emojis = await this.emojisRepository.find({
- where: {
- host: IsNull(),
- },
- order: {
- category: 'ASC',
- name: 'ASC',
- },
- });
-
+ const emojis = await this.emojisRepository.createQueryBuilder()
+ .where('host IS NULL')
+ .orderBy('LOWER(category)', 'ASC')
+ .orderBy('LOWER(name)', 'ASC')
+ .getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),
};
diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts
index 7629cd7a67..a1dbb26431 100644
--- a/packages/backend/src/server/api/endpoints/endpoint.ts
+++ b/packages/backend/src/server/api/endpoints/endpoint.ts
@@ -29,10 +29,13 @@ export const meta = {
},
},
- // 5 calls per second
+ // 1000 max @ 1/10ms drip = 10/sec average.
+ // Large bucket is ok because this is a fairly lightweight endpoint.
limit: {
- duration: 1000,
- max: 5,
+ type: 'bucket',
+
+ size: 1000,
+ dripRate: 10,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index 2e8cbffe2a..ad1f35055a 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
@@ -33,26 +34,25 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ offset: { type: 'integer', minimum: 0, default: 0 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.flashsRepository)
- private flashsRepository: FlashsRepository,
-
+ private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.flashsRepository.createQueryBuilder('flash')
- .andWhere('flash.likedCount > 0')
- .orderBy('flash.likedCount', 'DESC');
-
- const flashs = await query.limit(10).getMany();
-
- return await this.flashEntityService.packMany(flashs, me);
+ const result = await this.flashService.featured({
+ offset: ps.offset,
+ limit: ps.limit,
+ });
+ return await this.flashEntityService.packMany(result, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 8994c3fff6..09c06a108d 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import RE2 from 're2';
import * as mfm from '@transfem-org/sfm-js';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
@@ -11,7 +10,7 @@ import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
-import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
+import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
@@ -22,6 +21,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js';
@@ -126,6 +126,13 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
},
+
+ nameContainsProhibitedWords: {
+ message: 'Your new name contains prohibited words.',
+ code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS',
+ id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
+ httpStatusCode: 422,
+ },
},
res: {
@@ -187,6 +194,10 @@ export const paramDef = {
noCrawle: { type: 'boolean' },
preventAiLearning: { type: 'boolean' },
noindex: { type: 'boolean' },
+ requireSigninToViewContents: { type: 'boolean' },
+ makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true },
+ makeNotesHiddenBefore: { type: 'integer', nullable: true },
+ enableRss: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
speakAsCat: { type: 'boolean' },
@@ -241,6 +252,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private instanceMeta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -265,6 +279,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService,
+ private utilityService: UtilityService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@@ -309,7 +324,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
- new RE2(regexp[1], regexp[2]);
+ new RegExp(regexp[1], regexp[2]);
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
@@ -337,10 +352,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.noindex === 'boolean') updates.noindex = ps.noindex;
+ if (typeof ps.enableRss === 'boolean') updates.enableRss = ps.enableRss;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
+ if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents;
+ if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore;
+ if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
@@ -483,8 +502,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
+ const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) {
+ let hasProhibitedWords = false;
+ if (!await this.roleService.isModerator(user)) {
+ hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser);
+ }
+ if (hasProhibitedWords) {
+ throw new ApiError(meta.errors.nameContainsProhibitedWords);
+ }
+
const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
@@ -504,6 +532,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]);
}
+ if (newFollowedMessage != null) {
+ const tokens = mfm.parse(newFollowedMessage);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
+ }
+
updates.emojis = emojis;
updates.tags = tags;
@@ -539,7 +572,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// フォロワーにUpdateを配信
- this.accountUpdateService.publishToFollowers(user.id);
+ if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) {
+ this.accountUpdateService.publishToFollowers(user.id);
+ }
const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://'));
for (const url of urls) {
@@ -581,4 +616,58 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// なにもしない
}
}
+
+ // these two methods need to be kept in sync with
+ // `ApRendererService.renderPerson`
+ private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial<MiUser>): boolean {
+ const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss', 'requireSigninToViewContents', 'makeNotesFollowersOnlyBefore', 'makeNotesHiddenBefore'];
+ for (const field of basicFields) {
+ if ((field in newUser) && oldUser[field] !== newUser[field]) {
+ return true;
+ }
+ }
+
+ const arrayFields: (keyof MiUser)[] = ['emojis', 'tags'];
+ for (const arrayField of arrayFields) {
+ if ((arrayField in newUser) !== (arrayField in oldUser)) {
+ return true;
+ }
+
+ const oldArray = oldUser[arrayField] ?? [];
+ const newArray = newUser[arrayField] ?? [];
+ if (!Array.isArray(oldArray) || !Array.isArray(newArray)) {
+ return true;
+ }
+ if (oldArray.join('\0') !== newArray.join('\0')) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private profileNeedsPublishing(oldProfile: MiUserProfile, newProfile: Partial<MiUserProfile>): boolean {
+ const basicFields: (keyof MiUserProfile)[] = ['description', 'followedMessage', 'birthday', 'location', 'listenbrainz'];
+ for (const field of basicFields) {
+ if ((field in newProfile) && oldProfile[field] !== newProfile[field]) {
+ return true;
+ }
+ }
+
+ const arrayFields: (keyof MiUserProfile)[] = ['fields'];
+ for (const arrayField of arrayFields) {
+ if ((arrayField in newProfile) !== (arrayField in oldProfile)) {
+ return true;
+ }
+
+ const oldArray = oldProfile[arrayField] ?? [];
+ const newArray = newProfile[arrayField] ?? [];
+ if (!Array.isArray(oldArray) || !Array.isArray(newArray)) {
+ return true;
+ }
+ if (oldArray.join('\0') !== newArray.join('\0')) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts
index 0d9eec463b..228793fbf6 100644
--- a/packages/backend/src/server/api/endpoints/notes/following.ts
+++ b/packages/backend/src/server/api/endpoints/notes/following.ts
@@ -109,6 +109,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
sub.andWhere('latest.is_quote = false');
}
+ // Select the appropriate collection of users
+ if (ps.list === 'followers') {
+ addFollower(sub);
+ } else if (ps.list === 'following') {
+ addFollowee(sub);
+ } else {
+ addMutual(sub);
+ }
+
return sub;
},
'latest',
@@ -124,15 +133,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.channel', 'channel')
;
- // Select the appropriate collection of users
- if (ps.list === 'followers') {
- addFollower(query);
- } else if (ps.list === 'following') {
- addFollowee(query);
- } else {
- addMutual(query);
- }
-
// Limit to files, if requested
if (ps.filesOnly) {
query.andWhere('note."fileIds" != \'{}\'');
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
new file mode 100644
index 0000000000..c6032fbdae
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
@@ -0,0 +1,372 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import ms from 'ms';
+import { In } from 'typeorm';
+import { Inject, Injectable } from '@nestjs/common';
+import moment from 'moment';
+import { isPureRenote } from '@/misc/is-renote.js';
+import type { MiUser } from '@/models/User.js';
+import type {
+ UsersRepository,
+ 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';
+import { IdService } from '@/core/IdService.js';
+import { MiScheduleNoteType } from '@/models/NoteSchedule.js';
+import { RoleService } from '@/core/RoleService.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+
+ prohibitMoved: true,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 300,
+ },
+
+ kind: 'write:notes-schedule',
+
+ errors: {
+ scheduleNoteMax: {
+ message: 'Schedule note max.',
+ code: 'SCHEDULE_NOTE_MAX',
+ id: '168707c3-e7da-4031-989e-f42aa3a274b2',
+ },
+ noSuchRenoteTarget: {
+ message: 'No such renote target.',
+ code: 'NO_SUCH_RENOTE_TARGET',
+ id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4',
+ },
+
+ cannotReRenote: {
+ message: 'You can not Renote a pure Renote.',
+ code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
+ id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
+ },
+
+ cannotRenoteDueToVisibility: {
+ message: 'You can not Renote due to target visibility.',
+ code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
+ id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
+ },
+
+ noSuchReplyTarget: {
+ message: 'No such reply target.',
+ code: 'NO_SUCH_REPLY_TARGET',
+ id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
+ },
+
+ cannotReplyToPureRenote: {
+ message: 'You can not reply to a pure Renote.',
+ code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
+ id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
+ },
+
+ cannotCreateAlreadyExpiredPoll: {
+ message: 'Poll is already expired.',
+ code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
+ id: '04da457d-b083-4055-9082-955525eda5a5',
+ },
+
+ cannotCreateAlreadyExpiredSchedule: {
+ message: 'Schedule is already expired.',
+ code: 'CANNOT_CREATE_ALREADY_EXPIRED_SCHEDULE',
+ id: '8a9bfb90-fc7e-4878-a3e8-d97faaf5fb07',
+ },
+
+ noSuchChannel: {
+ message: 'No such channel.',
+ code: 'NO_SUCH_CHANNEL',
+ id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb',
+ },
+ noSuchSchedule: {
+ message: 'No such schedule.',
+ code: 'NO_SUCH_SCHEDULE',
+ id: '44dee229-8da1-4a61-856d-e3a4bbc12032',
+ },
+ youHaveBeenBlocked: {
+ message: 'You have been blocked by this user.',
+ code: 'YOU_HAVE_BEEN_BLOCKED',
+ id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
+ },
+
+ noSuchFile: {
+ message: 'Some files are not found.',
+ code: 'NO_SUCH_FILE',
+ id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
+ },
+
+ cannotRenoteOutsideOfChannel: {
+ message: 'Cannot renote outside of channel.',
+ code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
+ id: '33510210-8452-094c-6227-4a6c05d99f00',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
+ visibleUserIds: { type: 'array', uniqueItems: true, items: {
+ type: 'string', format: 'misskey:id',
+ } },
+ cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
+ reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
+ noExtractMentions: { type: 'boolean', default: false },
+ noExtractHashtags: { type: 'boolean', default: false },
+ noExtractEmojis: { type: 'boolean', default: false },
+ replyId: { type: 'string', format: 'misskey:id', nullable: true },
+ renoteId: { type: 'string', format: 'misskey:id', nullable: true },
+
+ // anyOf内にバリデーションを書いても最初の一つしかチェックされない
+ // See https://github.com/misskey-dev/misskey/pull/10082
+ text: {
+ type: 'string',
+ minLength: 1,
+ nullable: true,
+ },
+ fileIds: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ maxItems: 16,
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ mediaIds: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 1,
+ maxItems: 16,
+ items: { type: 'string', format: 'misskey:id' },
+ },
+ poll: {
+ type: 'object',
+ nullable: true,
+ properties: {
+ choices: {
+ type: 'array',
+ uniqueItems: true,
+ minItems: 2,
+ maxItems: 10,
+ items: { type: 'string', minLength: 1, maxLength: 50 },
+ },
+ multiple: { type: 'boolean' },
+ expiresAt: { type: 'integer', nullable: true },
+ expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
+ },
+ required: ['choices'],
+ },
+ scheduleNote: {
+ type: 'object',
+ nullable: false,
+ properties: {
+ scheduledAt: { type: 'integer', nullable: false },
+ },
+ },
+ },
+ // (re)note with text, files and poll are optional
+ anyOf: [
+ { required: ['text'] },
+ { required: ['renoteId'] },
+ { required: ['fileIds'] },
+ { required: ['mediaIds'] },
+ { required: ['poll'] },
+ ],
+ required: ['scheduleNote'],
+} as const;
+
+@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.notesRepository)
+ private notesRepository: NotesRepository,
+
+ @Inject(DI.noteScheduleRepository)
+ private noteScheduleRepository: NoteScheduleRepository,
+
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
+
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
+ @Inject(DI.channelsRepository)
+ private channelsRepository: ChannelsRepository,
+
+ private queueService: QueueService,
+ private roleService: RoleService,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const scheduleNoteCount = await this.noteScheduleRepository.countBy({ userId: me.id });
+ const scheduleNoteMax = (await this.roleService.getUserPolicies(me.id)).scheduleNoteMax;
+ if (scheduleNoteCount >= scheduleNoteMax) {
+ throw new ApiError(meta.errors.scheduleNoteMax);
+ }
+ let visibleUsers: MiUser[] = [];
+ if (ps.visibleUserIds) {
+ visibleUsers = await this.usersRepository.findBy({
+ id: In(ps.visibleUserIds),
+ });
+ }
+
+ let files: MiDriveFile[] = [];
+ const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
+ if (fileIds != null) {
+ files = await this.driveFilesRepository.createQueryBuilder('file')
+ .where('file.userId = :userId AND file.id IN (:...fileIds)', {
+ userId: me.id,
+ fileIds,
+ })
+ .orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
+ .setParameters({ fileIds })
+ .getMany();
+
+ if (files.length !== fileIds.length) {
+ throw new ApiError(meta.errors.noSuchFile);
+ }
+ }
+
+ let renote: MiNote | null = null;
+ if (ps.renoteId != null) {
+ // Fetch renote to note
+ renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
+
+ if (renote == null) {
+ throw new ApiError(meta.errors.noSuchRenoteTarget);
+ } else if (isPureRenote(renote)) {
+ throw new ApiError(meta.errors.cannotReRenote);
+ }
+
+ // Check blocking
+ if (renote.userId !== me.id) {
+ const blockExist = await this.blockingsRepository.exist({
+ where: {
+ blockerId: renote.userId,
+ blockeeId: me.id,
+ },
+ });
+ if (blockExist) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
+
+ if (renote.visibility === 'followers' && renote.userId !== me.id) {
+ // 他人のfollowers noteはreject
+ throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
+ } else if (renote.visibility === 'specified') {
+ // specified / direct noteはreject
+ throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
+ }
+ }
+
+ let reply: MiNote | null = null;
+ if (ps.replyId != null) {
+ // Fetch reply
+ reply = await this.notesRepository.findOneBy({ id: ps.replyId });
+
+ if (reply == null) {
+ throw new ApiError(meta.errors.noSuchReplyTarget);
+ } else if (isPureRenote(reply)) {
+ throw new ApiError(meta.errors.cannotReplyToPureRenote);
+ }
+
+ // Check blocking
+ if (reply.userId !== me.id) {
+ const blockExist = await this.blockingsRepository.exists({
+ where: {
+ blockerId: reply.userId,
+ blockeeId: me.id,
+ },
+ });
+ if (blockExist) {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ }
+ }
+ }
+
+ if (ps.poll) {
+ let scheduleNote_scheduledAt = Date.now();
+ if (typeof ps.scheduleNote.scheduledAt === 'number') {
+ scheduleNote_scheduledAt = moment.utc(ps.scheduleNote.scheduledAt).local().valueOf();
+ }
+ if (typeof ps.poll.expiresAt === 'number') {
+ if (ps.poll.expiresAt < scheduleNote_scheduledAt) {
+ throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
+ }
+ } else if (typeof ps.poll.expiredAfter === 'number') {
+ ps.poll.expiresAt = scheduleNote_scheduledAt + ps.poll.expiredAfter;
+ }
+ }
+ if (typeof ps.scheduleNote.scheduledAt === 'number') {
+ if (moment.utc(ps.scheduleNote.scheduledAt).local().valueOf() < Date.now()) {
+ throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule);
+ }
+ } else {
+ throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule);
+ }
+ const note: MiScheduleNoteType = {
+ files: files.map(f => f.id),
+ poll: ps.poll ? {
+ choices: ps.poll.choices,
+ multiple: ps.poll.multiple ?? false,
+ expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt).toISOString() : null,
+ } : undefined,
+ text: ps.text ?? undefined,
+ reply: reply?.id,
+ renote: renote?.id,
+ cw: ps.cw,
+ localOnly: false,
+ reactionAcceptance: ps.reactionAcceptance,
+ visibility: ps.visibility,
+ visibleUsers,
+ apMentions: ps.noExtractMentions ? [] : undefined,
+ apHashtags: ps.noExtractHashtags ? [] : undefined,
+ apEmojis: ps.noExtractEmojis ? [] : undefined,
+ };
+
+ if (ps.scheduleNote.scheduledAt) {
+ me.token = null;
+ const noteId = this.idService.gen(new Date().getTime());
+ const schedNoteLocalTime = moment.utc(ps.scheduleNote.scheduledAt).local().valueOf();
+ await this.noteScheduleRepository.insert({
+ id: noteId,
+ note: note,
+ userId: me.id,
+ scheduledAt: new Date(schedNoteLocalTime),
+ });
+
+ const delay = new Date(schedNoteLocalTime).getTime() - Date.now();
+ await this.queueService.ScheduleNotePostQueue.add(String(delay), {
+ scheduleNoteId: noteId,
+ }, {
+ delay,
+ removeOnComplete: true,
+ jobId: `schedNote:${noteId}`,
+ });
+ }
+
+ return '';
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts
new file mode 100644
index 0000000000..628fd89926
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import ms from 'ms';
+import { Inject, Injectable } from '@nestjs/common';
+import type { NoteScheduleRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { QueueService } from '@/core/QueueService.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+ kind: 'write:notes-schedule',
+
+ limit: {
+ duration: ms('1hour'),
+ max: 300,
+ },
+
+ errors: {
+ noSuchNote: {
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: 'a58056ba-8ba1-4323-8ebf-e0b585bc244f',
+ },
+ permissionDenied: {
+ message: 'Permission denied.',
+ code: 'PERMISSION_DENIED',
+ id: 'c0da2fed-8f61-4c47-a41d-431992607b5c',
+ httpStatusCode: 403,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ noteId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['noteId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.noteScheduleRepository)
+ private noteScheduleRepository: NoteScheduleRepository,
+ private queueService: QueueService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const note = await this.noteScheduleRepository.findOneBy({ id: ps.noteId });
+ if (note === null) {
+ throw new ApiError(meta.errors.noSuchNote);
+ }
+ if (note.userId !== me.id) {
+ throw new ApiError(meta.errors.permissionDenied);
+ }
+ await this.noteScheduleRepository.delete({ id: ps.noteId });
+ await this.queueService.ScheduleNotePostQueue.remove(`schedNote:${ps.noteId}`);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
new file mode 100644
index 0000000000..4895733d4e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
@@ -0,0 +1,131 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+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 { UserEntityService } from '@/core/entities/UserEntityService.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';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: true,
+ kind: 'read:notes-schedule',
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: { type: 'string', format: 'misskey:id', optional: false, nullable: false },
+ note: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ createdAt: { type: 'string', optional: false, nullable: false },
+ text: { type: 'string', optional: true, nullable: false },
+ cw: { type: 'string', optional: true, nullable: true },
+ fileIds: { type: 'array', optional: false, nullable: false, items: { type: 'string', format: 'misskey:id', optional: false, nullable: false } },
+ visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], optional: false, nullable: false },
+ visibleUsers: {
+ type: 'array', optional: false, nullable: false, items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ user: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'User',
+ },
+ reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
+ isSchedule: { type: 'boolean', optional: false, nullable: false },
+ },
+ },
+ userId: { type: 'string', optional: false, nullable: false },
+ scheduledAt: { type: 'string', optional: false, nullable: false },
+ },
+ },
+ },
+ limit: {
+ duration: ms('1hour'),
+ max: 300,
+ },
+
+ errors: {
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.noteScheduleRepository)
+ private noteScheduleRepository: NoteScheduleRepository,
+
+ private userEntityService: UserEntityService,
+ private driveFileEntityService: DriveFileEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.noteScheduleRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
+ .andWhere('note.userId = :userId', { userId: me.id });
+ const scheduleNotes = await query.limit(ps.limit).getMany();
+ const user = await this.userEntityService.pack(me, me);
+ const scheduleNotesPack: {
+ id: string;
+ note: {
+ text?: string;
+ cw?: string|null;
+ fileIds: string[];
+ visibility: typeof noteVisibilities[number];
+ visibleUsers: Packed<'UserLite'>[];
+ reactionAcceptance: MiNote['reactionAcceptance'];
+ user: Packed<'User'>;
+ createdAt: string;
+ isSchedule: boolean;
+ };
+ userId: string;
+ scheduledAt: string;
+ }[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => {
+ return {
+ ...item,
+ scheduledAt: item.scheduledAt.toISOString(),
+ note: {
+ ...item.note,
+ text: item.note.text ?? '',
+ 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) : [],
+ fileIds: item.note.files ? item.note.files : [],
+ files: await this.driveFileEntityService.packManyByIds(item.note.files),
+ createdAt: item.scheduledAt.toISOString(),
+ isSchedule: true,
+ id: item.id,
+ },
+ };
+ }));
+
+ return scheduleNotesPack;
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index 49c51cb33c..f0c9db38b4 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -28,6 +28,12 @@ export const meta = {
code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
},
+
+ signinRequired: {
+ message: 'Signin required.',
+ code: 'SIGNIN_REQUIRED',
+ id: '8e75455b-738c-471d-9f80-62693f33372e',
+ },
},
// 2 calls per second
@@ -56,7 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = await this.notesRepository.createQueryBuilder('note')
- .where('note.id = :noteId', { noteId: ps.noteId });
+ .where('note.id = :noteId', { noteId: ps.noteId })
+ .innerJoinAndSelect('note.user', 'user');
this.queryService.generateVisibilityQuery(query, me);
if (me) {
@@ -69,6 +76,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchNote);
}
+ if (note.user!.requireSigninToViewContents && me == null) {
+ throw new ApiError(meta.errors.signinRequired);
+ }
+
return await this.noteEntityService.pack(note, me, {
detail: true,
});
diff --git a/packages/backend/src/server/api/endpoints/notes/versions.ts b/packages/backend/src/server/api/endpoints/notes/versions.ts
index 343417f0e2..9b98d19fb1 100644
--- a/packages/backend/src/server/api/endpoints/notes/versions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/versions.ts
@@ -27,6 +27,12 @@ export const meta = {
code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
},
+
+ signinRequired: {
+ message: 'Signin required.',
+ code: 'SIGNIN_REQUIRED',
+ id: '8e75455b-738c-471d-9f80-62693f33372e',
+ },
},
// 10 calls per 5 seconds
@@ -55,10 +61,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = await this.notesRepository.createQueryBuilder('note')
- .select('note.id')
- .where('note.id = :noteId', { noteId: ps.noteId });
+ .where('note.id = :noteId', { noteId: ps.noteId })
+ .innerJoinAndSelect('note.user', 'user');
this.queryService.generateVisibilityQuery(query, me);
+ if (me) {
+ this.queryService.generateBlockedUserQuery(query, me);
+ }
const note = await query.getOne();
@@ -66,6 +75,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchNote);
}
+ if (note.user!.requireSigninToViewContents && me == null) {
+ throw new ApiError(meta.errors.signinRequired);
+ }
+
const edits = await this.getterService.getEdits(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 92d8032fa6..6416e43ff1 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -43,6 +43,12 @@ export const meta = {
code: 'BOTH_WITH_REPLIES_AND_WITH_FILES',
id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222',
},
+
+ signinRequired: {
+ message: 'Signin required.',
+ code: 'SIGNIN_REQUIRED',
+ id: 'd1588a9e-4b4d-4c07-807f-16f1486577a2',
+ },
},
// 5 calls per second
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index f811020645..81c0c526f0 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -72,10 +72,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReportYourself);
}
- if (await this.roleService.isAdministrator(targetUser)) {
- throw new ApiError(meta.errors.cannotReportAdmin);
- }
-
await this.abuseReportService.report([{
targetUserId: targetUser.id,
targetUserHost: targetUser.host,