diff --git a/locales/index.d.ts b/locales/index.d.ts index 2dca73bfa6..41d711b6e3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5170,6 +5170,22 @@ export interface Locale extends ILocale { * CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。 */ "testCaptchaWarning": string; + /** + * 禁止するユーザー名に含まれる文字列 + */ + "prohibitedPartialScreenNames": string; + /** + * ユーザー名に含まれる文字列がこのリストに含まれる場合、そのユーザー名は使用できません。 + */ + "prohibitedPartialScreenNamesDescription": string; + /** + * 変更しようとした名前に禁止された文字列が含まれています + */ + "screenNameContainsProhibitedWords": string; + /** + * 名前に禁止されている文字列が含まれています。本名である等の理由でこの名前を使用したい場合は、サーバー管理者にお問い合わせください。 + */ + "screenNameContainsProhibitedWordsDescription": string; "_abuseUserReport": { /** * 転送 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 440ffa9306..52bb4fb616 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1288,6 +1288,10 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証 messageToFollower: "フォロワーへのメッセージ" target: "対象" testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。本番環境で使用しないでください。" +prohibitedPartialScreenNames: "禁止するユーザー名に含まれる文字列" +prohibitedPartialScreenNamesDescription: "ユーザー名に含まれる文字列がこのリストに含まれる場合、そのユーザー名は使用できません。" +screenNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています" +screenNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。本名である等の理由でこの名前を使用したい場合は、サーバー管理者にお問い合わせください。" _abuseUserReport: forward: "転送" diff --git a/packages/backend/migration/1728634286056-prohibitedPartialScreenNames.js b/packages/backend/migration/1728634286056-prohibitedPartialScreenNames.js new file mode 100644 index 0000000000..1958186221 --- /dev/null +++ b/packages/backend/migration/1728634286056-prohibitedPartialScreenNames.js @@ -0,0 +1,9 @@ +export class ProhibitedPartialScreenNames1728634286056 { + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedPartialScreenNames" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedPartialScreenNames"`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index fd007de6c6..1711858b88 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -81,6 +81,11 @@ export class MiMeta { }) public prohibitedWords: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public prohibitedPartialScreenNames: string[]; + @Column('varchar', { length: 1024, array: true, default: '{}', }) diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index abb3c17be3..a00b68019b 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -177,6 +177,13 @@ export const meta = { type: 'string', }, }, + prohibitedPartialScreenNames: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, bannedEmailDomains: { type: 'array', optional: true, nullable: false, @@ -586,6 +593,7 @@ export default class extends Endpoint { // eslint- mediaSilencedHosts: instance.mediaSilencedHosts, sensitiveWords: instance.sensitiveWords, prohibitedWords: instance.prohibitedWords, + prohibitedPartialScreenNames: instance.prohibitedPartialScreenNames, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, mcaptchaSecretKey: instance.mcaptchaSecretKey, 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 e97ac4e2b9..efe1f8ebfc 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', }, }, + prohibitedPartialScreenNames: { + 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 }, @@ -214,6 +219,9 @@ export default class extends Endpoint { // eslint- if (Array.isArray(ps.prohibitedWords)) { set.prohibitedWords = ps.prohibitedWords.filter(Boolean); } + if (Array.isArray(ps.prohibitedPartialScreenNames)) { + set.prohibitedPartialScreenNames = ps.prohibitedPartialScreenNames.filter(Boolean); + } if (Array.isArray(ps.silencedHosts)) { let lastValue = ''; set.silencedHosts = ps.silencedHosts.sort().filter((h) => { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 798bd98cf1..d5cd14ae52 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; @@ -22,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import { UtilityService } from '@/core/UtilityService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; import { RolePolicies, RoleService } from '@/core/RoleService.js'; @@ -114,6 +115,13 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '8feff0ba-5ab5-585b-31f4-4df816663fad', }, + + screenNameContainsProhibitedWords: { + message: 'Screen name contains prohibited words.', + code: 'SCREEN_NAME_CONTAINS_PROHIBITED_WORDS', + id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191', + httpStatusCode: 422, + }, }, res: { @@ -223,6 +231,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private instanceMeta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -247,6 +258,7 @@ export default class extends Endpoint { // 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; @@ -445,6 +457,13 @@ export default class extends Endpoint { // eslint- let tags = [] as string[]; const newName = updates.name === undefined ? user.name : updates.name; + if (newName != null) { + const hasProhibitedWords = this.checkScreennameProhibitedWordsContain(newName, this.instanceMeta.prohibitedPartialScreenNames); + if (hasProhibitedWords) { + throw new ApiError(meta.errors.screenNameContainsProhibitedWords); + } + } + const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; @@ -545,4 +564,11 @@ export default class extends Endpoint { // eslint- // なにもしない } } + + private checkScreennameProhibitedWordsContain(name: string, prohibitedPartialScreenNames: string[]) { + if (this.utilityService.isKeyWordIncluded(name, prohibitedPartialScreenNames)) { + return true; + } + return false; + } } diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 60e4218a48..7b4bedd47a 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -10,6 +10,7 @@ import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/scripts/form.js'; +import type { MenuItem } from '@/types/menu.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -22,7 +23,6 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; -import type { MenuItem } from '@/types/menu.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; @@ -77,6 +77,9 @@ export const apiWithDialog = ( + + + + +
+ + + + {{ i18n.ts.save }} +
+
+ @@ -131,6 +143,7 @@ const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); const sensitiveWords = ref(''); const prohibitedWords = ref(''); +const prohibitedPartialScreenNames = ref(''); const hiddenTags = ref(''); const preservedUsernames = ref(''); const blockedHosts = ref(''); @@ -143,6 +156,7 @@ async function init() { emailRequiredForSignup.value = meta.emailRequiredForSignup; sensitiveWords.value = meta.sensitiveWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n'); + prohibitedPartialScreenNames.value = meta.prohibitedPartialScreenNames.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); blockedHosts.value = meta.blockedHosts.join('\n'); @@ -190,6 +204,14 @@ function save_prohibitedWords() { }); } +function save_prohibitedPartialScreenNames() { + os.apiWithDialog('admin/update-meta', { + prohibitedPartialScreenNames: prohibitedPartialScreenNames.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + function save_hiddenTags() { os.apiWithDialog('admin/update-meta', { hiddenTags: hiddenTags.value.split('\n'), diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index e40cb050fd..c55bc23595 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5124,6 +5124,7 @@ export type operations = { blockedHosts: string[]; sensitiveWords: string[]; prohibitedWords: string[]; + prohibitedPartialScreenNames: string[]; bannedEmailDomains?: string[]; preservedUsernames: string[]; hcaptchaSecretKey: string | null; @@ -9461,6 +9462,7 @@ export type operations = { blockedHosts?: string[] | null; sensitiveWords?: string[] | null; prohibitedWords?: string[] | null; + prohibitedPartialScreenNames?: string[] | null; themeColor?: string | null; mascotImageUrl?: string | null; bannerUrl?: string | null;