Skip to content

Commit

Permalink
cherry-pick: ユーザーの名前に禁止ワードを設定できるように (#318)
Browse files Browse the repository at this point in the history
feat: ユーザーの名前に禁止ワードを設定できるように (# 14756)

* wip

* 🎨

* Enhance: モデレーター以上は制限の影響を受けないように

* refactor

* better error handling

* fix

* Revert "better error handling"

This reverts commit 5670b29.

* error handling

* エラーが出ないのを修正

* translation

* Update Changelog

* status code

* ✌️

* モデレーター以上は影響ないことを明記

* 🎨

* update changelog

* spdx

* Update update.ts

* refactor

* eliminate `screen name`

* remove untracked file

---------

Co-authored-by: かっこかり <[email protected]>
Co-authored-by: KanariKanaru <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2024
1 parent fc1314b commit fad8e68
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Unreleased

### General
- Feat: ユーザーの名前に禁止ワードを設定できるように

### Client
- Enhance: l10nの更新
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
Expand Down
16 changes: 16 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5206,6 +5206,22 @@ export interface Locale extends ILocale {
* CAPTCHAのテストを目的とした機能です。***本番環境で使用しないでください。***
*/
"testCaptchaWarning": string;
/**
* 禁止ワード(ユーザーの名前)
*/
"prohibitedWordsForNameOfUser": string;
/**
* このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。
*/
"prohibitedWordsForNameOfUserDescription": string;
/**
* 変更しようとした名前に禁止された文字列が含まれています
*/
"yourNameContainsProhibitedWords": string;
/**
* 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。
*/
"yourNameContainsProhibitedWordsDescription": string;
"_abuseUserReport": {
/**
* 転送
Expand Down
4 changes: 4 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,10 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。***本番環境で使用しないでください。***"
prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"

_abuseUserReport:
forward: "転送"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class ProhibitedWordsForNameOfUser1728634286056 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`);
}
}
5 changes: 5 additions & 0 deletions packages/backend/src/models/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export class MiMeta {
})
public prohibitedWords: string[];

@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public prohibitedWordsForNameOfUser: string[];

@Column('varchar', {
length: 1024, array: true, default: '{}',
})
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ export const meta = {
type: 'string',
},
},
prohibitedWordsForNameOfUser: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
Expand Down Expand Up @@ -569,6 +576,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,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
mcaptchaSecretKey: instance.mcaptchaSecretKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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 },
Expand Down Expand Up @@ -199,6 +204,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) => {
Expand Down
22 changes: 21 additions & 1 deletion packages/backend/src/server/api/endpoints/i/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,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 } from '@/models/_.js';
import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
Expand All @@ -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';
Expand Down Expand Up @@ -108,6 +109,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: {
Expand Down Expand Up @@ -211,6 +219,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,

Expand All @@ -232,6 +243,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;
Expand Down Expand Up @@ -402,6 +414,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;

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));
}
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend/src/os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { APIError as MisskeyApiError } from 'misskey-js/api.js';
import type { UserDetailed, DriveFile, DriveFolder } from 'misskey-js/entities.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';
Expand All @@ -24,7 +25,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 { 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';
Expand All @@ -35,6 +35,7 @@ export const apiWithDialog = (<E extends keyof MisskeyEndpoints = keyof MisskeyE
endpoint: E,
data: P,
token?: string | null | undefined,
customErrors?: Record<string, { title?: string; text: string; }>,
) => {
const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => {
Expand Down Expand Up @@ -77,6 +78,9 @@ export const apiWithDialog = (<E extends keyof MisskeyEndpoints = keyof MisskeyE
} else if (err.message.startsWith('Unexpected token')) {
title = i18n.ts.gotInvalidResponseError;
text = i18n.ts.gotInvalidResponseErrorDescription;
} else if (customErrors && customErrors[err.id] != null) {
title = customErrors[err.id].title;
text = customErrors[err.id].text;
}
alert({
type: 'error',
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/pages/admin/moderation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.prohibitedWords }}</template>
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>

<MkTextarea v-model="prohibitedWordsForNameOfUser">
<template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
</div>
</FormSuspense>
</MkSpacer>
Expand Down Expand Up @@ -83,6 +88,7 @@ const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>('');
const prohibitedWordsForNameOfUser = ref<string>('');
const preservedUsernames = ref<string>('');
const tosUrl = ref<string | null>(null);
const privacyPolicyUrl = ref<string | null>(null);
Expand All @@ -98,6 +104,8 @@ async function init() {
tosUrl.value = meta.tosUrl;
privacyPolicyUrl.value = meta.privacyPolicyUrl;
inquiryUrl.value = meta.inquiryUrl;
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
}

function save() {
Expand All @@ -110,6 +118,7 @@ function save() {
sensitiveWords: sensitiveWords.value.split('\n'),
prohibitedWords: prohibitedWords.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
prohibitedWordsForNameOfUser: prohibitedWordsForNameOfUser.value.split('\n'),
}).then(() => {
fetchInstance(true);
});
Expand Down
11 changes: 10 additions & 1 deletion packages/frontend/src/pages/settings/profile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,16 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d

const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));

function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
return lang != null && lang in langmap;
}

const profile = reactive({
name: $i.name,
description: $i.description,
location: $i.location,
birthday: $i.birthday,
lang: $i.lang,
lang: assertVaildLang($i.lang) ? $i.lang : null,
isBot: $i.isBot ?? false,
});

Expand Down Expand Up @@ -188,6 +192,11 @@ function save() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
lang: (profile.lang || null) as keyof typeof langmap | null,
isBot: !!profile.isBot,
}, undefined, {
'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
title: i18n.ts.yourNameContainsProhibitedWords,
text: i18n.ts.yourNameContainsProhibitedWordsDescription,
},
});
globalEvents.emit('requestClearPageCache');
}
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend/src/scripts/get-note-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ export function getNoteMenu(props: {
function togglePin(pin: boolean): void {
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
noteId: appearNote.id,
}, undefined);
}, undefined, {
'72dab508-c64d-498f-8740-a8eec1ba385a': {
text: i18n.ts.pinLimitExceeded,
},
});
}

async function unclip(): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions packages/misskey-js/src/autogen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3913,6 +3913,7 @@ export type operations = {
blockedHosts: string[];
sensitiveWords: string[];
prohibitedWords: string[];
prohibitedWordsForNameOfUser: string[];
bannedEmailDomains?: string[];
preservedUsernames: string[];
hcaptchaSecretKey: string | null;
Expand Down Expand Up @@ -7219,6 +7220,7 @@ export type operations = {
blockedHosts?: string[] | null;
sensitiveWords?: string[] | null;
prohibitedWords?: string[] | null;
prohibitedWordsForNameOfUser?: string[] | null;
themeColor?: string | null;
mascotImageUrl?: string | null;
bannerUrl?: string | null;
Expand Down

0 comments on commit fad8e68

Please sign in to comment.