Skip to content

Commit 26d4c5f

Browse files
yuriha-chantai-cha
andauthored
メンションの最大数をロールごとに設定可能にする (#13343)
* Add new role policy: maximum mentions per note * fix * Reviewを反映 * fix * Add ChangeLog * Update type definitions * Add E2E test * CHANGELOG に説明を追加 --------- Co-authored-by: taichan <[email protected]>
1 parent b9bcced commit 26d4c5f

File tree

12 files changed

+223
-2
lines changed

12 files changed

+223
-2
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
- Fix: 破損した通知をクライアントに送信しないように
3838
* 通知欄が無限にリロードされる問題が改善する可能性があります
3939
- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正
40+
- Feat: 投稿者のロールに応じて、一つのノートに含むことのできるメンションとダイレクト投稿の宛先の人数に上限を設定できるように
41+
* デフォルトのメンション上限は20アカウントに設定されます。(管理者はベースロールの設定で変更可能です。)
42+
* 連合の問い合わせに応答しないサーバーのリモートユーザーへのメンションは、上限の人数に含めない実装になっています。
4043
- Fix: 自分がフォローしていないアカウントのフォロワー限定ノートが閲覧できることがある問題を修正
4144
- Fix: タイムラインのオプションで「リノートを表示」を無効にしている際、投票のみの引用リノートが流れてこない問題を修正
4245
- Fix: エンドポイント`admin/emoji/update`の各種修正

locales/index.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6442,6 +6442,10 @@ export interface Locale extends ILocale {
64426442
* パブリック投稿の許可
64436443
*/
64446444
"canPublicNote": string;
6445+
/**
6446+
* ノート内の最大メンション数
6447+
*/
6448+
"mentionMax": string;
64456449
/**
64466450
* サーバー招待コードの発行
64476451
*/

locales/ja-JP.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1665,6 +1665,7 @@ _role:
16651665
gtlAvailable: "グローバルタイムラインの閲覧"
16661666
ltlAvailable: "ローカルタイムラインの閲覧"
16671667
canPublicNote: "パブリック投稿の許可"
1668+
mentionMax: "ノート内の最大メンション数"
16681669
canInvite: "サーバー招待コードの発行"
16691670
inviteLimit: "招待コードの作成可能数"
16701671
inviteLimitCycle: "招待コードの発行間隔"

packages/backend/src/core/NoteCreateService.ts

+4
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ export class NoteCreateService implements OnApplicationShutdown {
379379
}
380380
}
381381

382+
if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
383+
throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
384+
}
385+
382386
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
383387

384388
setImmediate('post created', { signal: this.#shutdownController.signal }).then(

packages/backend/src/core/RoleService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type RolePolicies = {
3535
gtlAvailable: boolean;
3636
ltlAvailable: boolean;
3737
canPublicNote: boolean;
38+
mentionLimit: number;
3839
canInvite: boolean;
3940
inviteLimit: number;
4041
inviteLimitCycle: number;
@@ -62,6 +63,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
6263
gtlAvailable: true,
6364
ltlAvailable: true,
6465
canPublicNote: true,
66+
mentionLimit: 20,
6567
canInvite: false,
6668
inviteLimit: 0,
6769
inviteLimitCycle: 60 * 24 * 7,
@@ -328,6 +330,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
328330
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
329331
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
330332
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
333+
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
331334
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
332335
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
333336
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

packages/backend/src/models/json-schema/role.ts

+4
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ export const packedRolePoliciesSchema = {
160160
type: 'boolean',
161161
optional: false, nullable: false,
162162
},
163+
mentionLimit: {
164+
type: 'integer',
165+
optional: false, nullable: false,
166+
},
163167
canInvite: {
164168
type: 'boolean',
165169
optional: false, nullable: false,

packages/backend/src/server/api/endpoints/notes/create.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export const meta = {
126126
code: 'CONTAINS_PROHIBITED_WORDS',
127127
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
128128
},
129+
130+
containsTooManyMentions: {
131+
message: 'Cannot post because it exceeds the allowed number of mentions.',
132+
code: 'CONTAINS_TOO_MANY_MENTIONS',
133+
id: '4de0363a-3046-481b-9b0f-feff3e211025',
134+
},
129135
},
130136
} as const;
131137

@@ -386,9 +392,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
386392
} catch (e) {
387393
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
388394
if (e instanceof IdentifiableError) {
389-
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
395+
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
396+
throw new ApiError(meta.errors.containsProhibitedWords);
397+
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
398+
throw new ApiError(meta.errors.containsTooManyMentions);
399+
}
390400
}
391-
392401
throw e;
393402
}
394403
});

packages/backend/test/e2e/note.ts

+165
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,171 @@ describe('Note', () => {
761761

762762
assert.strictEqual(note1.status, 400);
763763
});
764+
765+
test('メンションの数が上限を超えるとエラーになる', async () => {
766+
const res = await api('admin/roles/create', {
767+
name: 'test',
768+
description: '',
769+
color: null,
770+
iconUrl: null,
771+
displayOrder: 0,
772+
target: 'manual',
773+
condFormula: {},
774+
isAdministrator: false,
775+
isModerator: false,
776+
isPublic: false,
777+
isExplorable: false,
778+
asBadge: false,
779+
canEditMembersByModerator: false,
780+
policies: {
781+
mentionLimit: {
782+
useDefault: false,
783+
priority: 1,
784+
value: 0,
785+
},
786+
},
787+
}, alice);
788+
789+
assert.strictEqual(res.status, 200);
790+
791+
await new Promise(x => setTimeout(x, 2));
792+
793+
const assign = await api('admin/roles/assign', {
794+
userId: alice.id,
795+
roleId: res.body.id,
796+
}, alice);
797+
798+
assert.strictEqual(assign.status, 204);
799+
800+
await new Promise(x => setTimeout(x, 2));
801+
802+
const note = await api('/notes/create', {
803+
text: '@bob potentially annoying text',
804+
}, alice);
805+
806+
assert.strictEqual(note.status, 400);
807+
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
808+
809+
await api('admin/roles/unassign', {
810+
userId: alice.id,
811+
roleId: res.body.id,
812+
});
813+
814+
await api('admin/roles/delete', {
815+
roleId: res.body.id,
816+
}, alice);
817+
});
818+
819+
test('ダイレクト投稿もエラーになる', async () => {
820+
const res = await api('admin/roles/create', {
821+
name: 'test',
822+
description: '',
823+
color: null,
824+
iconUrl: null,
825+
displayOrder: 0,
826+
target: 'manual',
827+
condFormula: {},
828+
isAdministrator: false,
829+
isModerator: false,
830+
isPublic: false,
831+
isExplorable: false,
832+
asBadge: false,
833+
canEditMembersByModerator: false,
834+
policies: {
835+
mentionLimit: {
836+
useDefault: false,
837+
priority: 1,
838+
value: 0,
839+
},
840+
},
841+
}, alice);
842+
843+
assert.strictEqual(res.status, 200);
844+
845+
await new Promise(x => setTimeout(x, 2));
846+
847+
const assign = await api('admin/roles/assign', {
848+
userId: alice.id,
849+
roleId: res.body.id,
850+
}, alice);
851+
852+
assert.strictEqual(assign.status, 204);
853+
854+
await new Promise(x => setTimeout(x, 2));
855+
856+
const note = await api('/notes/create', {
857+
text: 'potentially annoying text',
858+
visibility: 'specified',
859+
visibleUserIds: [ bob.id ],
860+
}, alice);
861+
862+
assert.strictEqual(note.status, 400);
863+
assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
864+
865+
await api('admin/roles/unassign', {
866+
userId: alice.id,
867+
roleId: res.body.id,
868+
});
869+
870+
await api('admin/roles/delete', {
871+
roleId: res.body.id,
872+
}, alice);
873+
});
874+
875+
test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => {
876+
const res = await api('admin/roles/create', {
877+
name: 'test',
878+
description: '',
879+
color: null,
880+
iconUrl: null,
881+
displayOrder: 0,
882+
target: 'manual',
883+
condFormula: {},
884+
isAdministrator: false,
885+
isModerator: false,
886+
isPublic: false,
887+
isExplorable: false,
888+
asBadge: false,
889+
canEditMembersByModerator: false,
890+
policies: {
891+
mentionLimit: {
892+
useDefault: false,
893+
priority: 1,
894+
value: 1,
895+
},
896+
},
897+
}, alice);
898+
899+
assert.strictEqual(res.status, 200);
900+
901+
await new Promise(x => setTimeout(x, 2));
902+
903+
const assign = await api('admin/roles/assign', {
904+
userId: alice.id,
905+
roleId: res.body.id,
906+
}, alice);
907+
908+
assert.strictEqual(assign.status, 204);
909+
910+
await new Promise(x => setTimeout(x, 2));
911+
912+
const note = await api('/notes/create', {
913+
text: '@bob potentially annoying text',
914+
visibility: 'specified',
915+
visibleUserIds: [ bob.id ],
916+
}, alice);
917+
918+
assert.strictEqual(note.status, 200);
919+
920+
await api('admin/roles/unassign', {
921+
userId: alice.id,
922+
roleId: res.body.id,
923+
});
924+
925+
await api('admin/roles/delete', {
926+
roleId: res.body.id,
927+
}, alice);
928+
});
764929
});
765930

766931
describe('notes/delete', () => {

packages/frontend/src/const.ts

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export const ROLE_POLICIES = [
7575
'gtlAvailable',
7676
'ltlAvailable',
7777
'canPublicNote',
78+
'mentionLimit',
7879
'canInvite',
7980
'inviteLimit',
8081
'inviteLimitCycle',

packages/frontend/src/pages/admin/roles.editor.vue

+19
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,25 @@ SPDX-License-Identifier: AGPL-3.0-only
160160
</div>
161161
</MkFolder>
162162

163+
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
164+
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
165+
<template #suffix>
166+
<span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
167+
<span v-else>{{ role.policies.mentionLimit.value }}</span>
168+
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span>
169+
</template>
170+
<div class="_gaps">
171+
<MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly">
172+
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
173+
</MkSwitch>
174+
<MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly">
175+
</MkInput>
176+
<MkRange v-model="role.policies.mentionLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
177+
<template #label>{{ i18n.ts._role.priority }}</template>
178+
</MkRange>
179+
</div>
180+
</MkFolder>
181+
163182
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
164183
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
165184
<template #suffix>

packages/frontend/src/pages/admin/roles.vue

+7
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ SPDX-License-Identifier: AGPL-3.0-only
4848
</MkSwitch>
4949
</MkFolder>
5050

51+
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
52+
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
53+
<template #suffix>{{ policies.mentionLimit }}</template>
54+
<MkInput v-model="policies.mentionLimit" type="number">
55+
</MkInput>
56+
</MkFolder>
57+
5158
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
5259
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
5360
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>

packages/misskey-js/src/autogen/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4652,6 +4652,7 @@ export type components = {
46524652
gtlAvailable: boolean;
46534653
ltlAvailable: boolean;
46544654
canPublicNote: boolean;
4655+
mentionLimit: number;
46554656
canInvite: boolean;
46564657
inviteLimit: number;
46574658
inviteLimitCycle: number;

0 commit comments

Comments
 (0)