Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
		@@ -379,6 +379,10 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) {
 | 
			
		||||
			throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
 | 
			
		||||
 | 
			
		||||
		setImmediate('post created', { signal: this.#shutdownController.signal }).then(
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ export type RolePolicies = {
 | 
			
		||||
	gtlAvailable: boolean;
 | 
			
		||||
	ltlAvailable: boolean;
 | 
			
		||||
	canPublicNote: boolean;
 | 
			
		||||
	mentionLimit: number;
 | 
			
		||||
	canInvite: boolean;
 | 
			
		||||
	inviteLimit: number;
 | 
			
		||||
	inviteLimitCycle: number;
 | 
			
		||||
@@ -62,6 +63,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
	gtlAvailable: true,
 | 
			
		||||
	ltlAvailable: true,
 | 
			
		||||
	canPublicNote: true,
 | 
			
		||||
	mentionLimit: 20,
 | 
			
		||||
	canInvite: false,
 | 
			
		||||
	inviteLimit: 0,
 | 
			
		||||
	inviteLimitCycle: 60 * 24 * 7,
 | 
			
		||||
@@ -328,6 +330,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
			gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
 | 
			
		||||
			ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
 | 
			
		||||
			canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
 | 
			
		||||
			mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
 | 
			
		||||
			canInvite: calc('canInvite', vs => vs.some(v => v === true)),
 | 
			
		||||
			inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
 | 
			
		||||
			inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
 | 
			
		||||
 
 | 
			
		||||
@@ -160,6 +160,10 @@ export const packedRolePoliciesSchema = {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		mentionLimit: {
 | 
			
		||||
			type: 'integer',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canInvite: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -126,6 +126,12 @@ export const meta = {
 | 
			
		||||
			code: 'CONTAINS_PROHIBITED_WORDS',
 | 
			
		||||
			id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		containsTooManyMentions: {
 | 
			
		||||
			message: 'Cannot post because it exceeds the allowed number of mentions.',
 | 
			
		||||
			code: 'CONTAINS_TOO_MANY_MENTIONS',
 | 
			
		||||
			id: '4de0363a-3046-481b-9b0f-feff3e211025',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@@ -386,9 +392,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
 | 
			
		||||
				if (e instanceof IdentifiableError) {
 | 
			
		||||
					if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
 | 
			
		||||
					if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
 | 
			
		||||
						throw new ApiError(meta.errors.containsProhibitedWords);
 | 
			
		||||
					} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
 | 
			
		||||
						throw new ApiError(meta.errors.containsTooManyMentions);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				throw e;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -761,6 +761,171 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(note1.status, 400);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('メンションの数が上限を超えるとエラーになる', async () => {
 | 
			
		||||
			const res = await api('admin/roles/create', {
 | 
			
		||||
				name: 'test',
 | 
			
		||||
				description: '',
 | 
			
		||||
				color: null,
 | 
			
		||||
				iconUrl: null,
 | 
			
		||||
				displayOrder: 0,
 | 
			
		||||
				target: 'manual',
 | 
			
		||||
				condFormula: {},
 | 
			
		||||
				isAdministrator: false,
 | 
			
		||||
				isModerator: false,
 | 
			
		||||
				isPublic: false,
 | 
			
		||||
				isExplorable: false,
 | 
			
		||||
				asBadge: false,
 | 
			
		||||
				canEditMembersByModerator: false,
 | 
			
		||||
				policies: {
 | 
			
		||||
					mentionLimit: {
 | 
			
		||||
						useDefault: false,
 | 
			
		||||
						priority: 1,
 | 
			
		||||
						value: 0,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const note = await api('/notes/create', {
 | 
			
		||||
				text: '@bob potentially annoying text',
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(note.status, 400);
 | 
			
		||||
			assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('ダイレクト投稿もエラーになる', async () => {
 | 
			
		||||
			const res = await api('admin/roles/create', {
 | 
			
		||||
				name: 'test',
 | 
			
		||||
				description: '',
 | 
			
		||||
				color: null,
 | 
			
		||||
				iconUrl: null,
 | 
			
		||||
				displayOrder: 0,
 | 
			
		||||
				target: 'manual',
 | 
			
		||||
				condFormula: {},
 | 
			
		||||
				isAdministrator: false,
 | 
			
		||||
				isModerator: false,
 | 
			
		||||
				isPublic: false,
 | 
			
		||||
				isExplorable: false,
 | 
			
		||||
				asBadge: false,
 | 
			
		||||
				canEditMembersByModerator: false,
 | 
			
		||||
				policies: {
 | 
			
		||||
					mentionLimit: {
 | 
			
		||||
						useDefault: false,
 | 
			
		||||
						priority: 1,
 | 
			
		||||
						value: 0,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const note = await api('/notes/create', {
 | 
			
		||||
				text: 'potentially annoying text',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [ bob.id ],
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(note.status, 400);
 | 
			
		||||
			assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS');
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => {
 | 
			
		||||
			const res = await api('admin/roles/create', {
 | 
			
		||||
				name: 'test',
 | 
			
		||||
				description: '',
 | 
			
		||||
				color: null,
 | 
			
		||||
				iconUrl: null,
 | 
			
		||||
				displayOrder: 0,
 | 
			
		||||
				target: 'manual',
 | 
			
		||||
				condFormula: {},
 | 
			
		||||
				isAdministrator: false,
 | 
			
		||||
				isModerator: false,
 | 
			
		||||
				isPublic: false,
 | 
			
		||||
				isExplorable: false,
 | 
			
		||||
				asBadge: false,
 | 
			
		||||
				canEditMembersByModerator: false,
 | 
			
		||||
				policies: {
 | 
			
		||||
					mentionLimit: {
 | 
			
		||||
						useDefault: false,
 | 
			
		||||
						priority: 1,
 | 
			
		||||
						value: 1,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const note = await api('/notes/create', {
 | 
			
		||||
				text: '@bob potentially annoying text',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [ bob.id ],
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(note.status, 200);
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('notes/delete', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -75,6 +75,7 @@ export const ROLE_POLICIES = [
 | 
			
		||||
	'gtlAvailable',
 | 
			
		||||
	'ltlAvailable',
 | 
			
		||||
	'canPublicNote',
 | 
			
		||||
	'mentionLimit',
 | 
			
		||||
	'canInvite',
 | 
			
		||||
	'inviteLimit',
 | 
			
		||||
	'inviteLimitCycle',
 | 
			
		||||
 
 | 
			
		||||
@@ -160,6 +160,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
 | 
			
		||||
				<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
 | 
			
		||||
				<template #suffix>
 | 
			
		||||
					<span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
 | 
			
		||||
					<span v-else>{{ role.policies.mentionLimit.value }}</span>
 | 
			
		||||
					<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.mentionLimit)"></i></span>
 | 
			
		||||
				</template>
 | 
			
		||||
				<div class="_gaps">
 | 
			
		||||
					<MkSwitch v-model="role.policies.mentionLimit.useDefault" :readonly="readonly">
 | 
			
		||||
						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 | 
			
		||||
					</MkSwitch>
 | 
			
		||||
					<MkInput v-model="role.policies.mentionLimit.value" :disabled="role.policies.mentionLimit.useDefault" type="number" :readonly="readonly">
 | 
			
		||||
					</MkInput>
 | 
			
		||||
					<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 : ''">
 | 
			
		||||
						<template #label>{{ i18n.ts._role.priority }}</template>
 | 
			
		||||
					</MkRange>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
 | 
			
		||||
			<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
 | 
			
		||||
				<template #label>{{ i18n.ts._role._options.canInvite }}</template>
 | 
			
		||||
				<template #suffix>
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
							</MkSwitch>
 | 
			
		||||
						</MkFolder>
 | 
			
		||||
 | 
			
		||||
						<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
 | 
			
		||||
							<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
 | 
			
		||||
							<template #suffix>{{ policies.mentionLimit }}</template>
 | 
			
		||||
							<MkInput v-model="policies.mentionLimit" type="number">
 | 
			
		||||
							</MkInput>
 | 
			
		||||
						</MkFolder>
 | 
			
		||||
 | 
			
		||||
						<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
 | 
			
		||||
							<template #label>{{ i18n.ts._role._options.canInvite }}</template>
 | 
			
		||||
							<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -4652,6 +4652,7 @@ export type components = {
 | 
			
		||||
      gtlAvailable: boolean;
 | 
			
		||||
      ltlAvailable: boolean;
 | 
			
		||||
      canPublicNote: boolean;
 | 
			
		||||
      mentionLimit: number;
 | 
			
		||||
      canInvite: boolean;
 | 
			
		||||
      inviteLimit: number;
 | 
			
		||||
      inviteLimitCycle: number;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user