This commit is contained in:
KanariKanaru 2024-10-11 17:51:44 +09:00 committed by kakkokari-gtyih
parent 824c51a19f
commit 88cf83b9cf
10 changed files with 105 additions and 2 deletions

16
locales/index.d.ts vendored
View File

@ -5170,6 +5170,22 @@ export interface Locale extends ILocale {
* CAPTCHAのテストを目的とした機能です<strong>使</strong> * CAPTCHAのテストを目的とした機能です<strong>使</strong>
*/ */
"testCaptchaWarning": string; "testCaptchaWarning": string;
/**
*
*/
"prohibitedPartialScreenNames": string;
/**
* 使
*/
"prohibitedPartialScreenNamesDescription": string;
/**
*
*/
"screenNameContainsProhibitedWords": string;
/**
* 使
*/
"screenNameContainsProhibitedWordsDescription": string;
"_abuseUserReport": { "_abuseUserReport": {
/** /**
* *

View File

@ -1288,6 +1288,10 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証
messageToFollower: "フォロワーへのメッセージ" messageToFollower: "フォロワーへのメッセージ"
target: "対象" target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>" testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
prohibitedPartialScreenNames: "禁止するユーザー名に含まれる文字列"
prohibitedPartialScreenNamesDescription: "ユーザー名に含まれる文字列がこのリストに含まれる場合、そのユーザー名は使用できません。"
screenNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
screenNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。本名である等の理由でこの名前を使用したい場合は、サーバー管理者にお問い合わせください。"
_abuseUserReport: _abuseUserReport:
forward: "転送" forward: "転送"

View File

@ -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"`);
}
}

View File

@ -81,6 +81,11 @@ export class MiMeta {
}) })
public prohibitedWords: string[]; public prohibitedWords: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public prohibitedPartialScreenNames: string[];
@Column('varchar', { @Column('varchar', {
length: 1024, array: true, default: '{}', length: 1024, array: true, default: '{}',
}) })

View File

@ -177,6 +177,13 @@ export const meta = {
type: 'string', type: 'string',
}, },
}, },
prohibitedPartialScreenNames: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
bannedEmailDomains: { bannedEmailDomains: {
type: 'array', type: 'array',
optional: true, nullable: false, optional: true, nullable: false,
@ -586,6 +593,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mediaSilencedHosts: instance.mediaSilencedHosts, mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords, sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords, prohibitedWords: instance.prohibitedWords,
prohibitedPartialScreenNames: instance.prohibitedPartialScreenNames,
preservedUsernames: instance.preservedUsernames, preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey, hcaptchaSecretKey: instance.hcaptchaSecretKey,
mcaptchaSecretKey: instance.mcaptchaSecretKey, mcaptchaSecretKey: instance.mcaptchaSecretKey,

View File

@ -46,6 +46,11 @@ export const paramDef = {
type: 'string', type: 'string',
}, },
}, },
prohibitedPartialScreenNames: {
type: 'array', nullable: true, items: {
type: 'string',
},
},
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true }, mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true },
@ -214,6 +219,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.prohibitedWords)) { if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean); set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
} }
if (Array.isArray(ps.prohibitedPartialScreenNames)) {
set.prohibitedPartialScreenNames = ps.prohibitedPartialScreenNames.filter(Boolean);
}
if (Array.isArray(ps.silencedHosts)) { if (Array.isArray(ps.silencedHosts)) {
let lastValue = ''; let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => { set.silencedHosts = ps.silencedHosts.sort().filter((h) => {

View File

@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.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 type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.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 { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { HashtagService } from '@/core/HashtagService.js'; import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js'; import { RolePolicies, RoleService } from '@/core/RoleService.js';
@ -114,6 +115,13 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE', code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad', 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: { res: {
@ -223,6 +231,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.meta)
private instanceMeta: MiMeta,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@ -247,6 +258,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService, private cacheService: CacheService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService, private avatarDecorationService: AvatarDecorationService,
private utilityService: UtilityService,
) { ) {
super(meta, paramDef, async (ps, _user, token) => { super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@ -445,6 +457,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let tags = [] as string[]; let tags = [] as string[];
const newName = updates.name === undefined ? user.name : updates.name; 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 newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
@ -545,4 +564,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// なにもしない // なにもしない
} }
} }
private checkScreennameProhibitedWordsContain(name: string, prohibitedPartialScreenNames: string[]) {
if (this.utilityService.isKeyWordIncluded(name, prohibitedPartialScreenNames)) {
return true;
}
return false;
}
} }

View File

@ -10,6 +10,7 @@ import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js'; import type { Form, GetFormResultType } from '@/scripts/form.js';
import type { MenuItem } from '@/types/menu.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -22,7 +23,6 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue';
import type { MenuItem } from '@/types/menu.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js'; import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
@ -77,6 +77,9 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
} else if (err.message.startsWith('Unexpected token')) { } else if (err.message.startsWith('Unexpected token')) {
title = i18n.ts.gotInvalidResponseError; title = i18n.ts.gotInvalidResponseError;
text = i18n.ts.gotInvalidResponseErrorDescription; text = i18n.ts.gotInvalidResponseErrorDescription;
} else if (err.code === 'SCREEN_NAME_CONTAINS_PROHIBITED_WORDS') {
title = i18n.ts.screenNameContainsProhibitedWords;
text = i18n.ts.screenNameContainsProhibitedWordsDescription;
} }
alert({ alert({
type: 'error', type: 'error',

View File

@ -57,6 +57,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-x"></i></template>
<template #label>{{ i18n.ts.prohibitedPartialScreenNames }}</template>
<div class="_gaps">
<MkTextarea v-model="prohibitedPartialScreenNames">
<template #caption>{{ i18n.ts.prohibitedPartialScreenNamesDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedPartialScreenNames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template> <template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.hiddenTags }}</template> <template #label>{{ i18n.ts.hiddenTags }}</template>
@ -131,6 +143,7 @@ const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false); const emailRequiredForSignup = ref<boolean>(false);
const sensitiveWords = ref<string>(''); const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>(''); const prohibitedWords = ref<string>('');
const prohibitedPartialScreenNames = ref<string>('');
const hiddenTags = ref<string>(''); const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>(''); const preservedUsernames = ref<string>('');
const blockedHosts = ref<string>(''); const blockedHosts = ref<string>('');
@ -143,6 +156,7 @@ async function init() {
emailRequiredForSignup.value = meta.emailRequiredForSignup; emailRequiredForSignup.value = meta.emailRequiredForSignup;
sensitiveWords.value = meta.sensitiveWords.join('\n'); sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n');
prohibitedPartialScreenNames.value = meta.prohibitedPartialScreenNames.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n');
blockedHosts.value = meta.blockedHosts.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() { function save_hiddenTags() {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
hiddenTags: hiddenTags.value.split('\n'), hiddenTags: hiddenTags.value.split('\n'),

View File

@ -5124,6 +5124,7 @@ export type operations = {
blockedHosts: string[]; blockedHosts: string[];
sensitiveWords: string[]; sensitiveWords: string[];
prohibitedWords: string[]; prohibitedWords: string[];
prohibitedPartialScreenNames: string[];
bannedEmailDomains?: string[]; bannedEmailDomains?: string[];
preservedUsernames: string[]; preservedUsernames: string[];
hcaptchaSecretKey: string | null; hcaptchaSecretKey: string | null;
@ -9461,6 +9462,7 @@ export type operations = {
blockedHosts?: string[] | null; blockedHosts?: string[] | null;
sensitiveWords?: string[] | null; sensitiveWords?: string[] | null;
prohibitedWords?: string[] | null; prohibitedWords?: string[] | null;
prohibitedPartialScreenNames?: string[] | null;
themeColor?: string | null; themeColor?: string | null;
mascotImageUrl?: string | null; mascotImageUrl?: string | null;
bannerUrl?: string | null; bannerUrl?: string | null;