diff --git a/locales/en-US.yml b/locales/en-US.yml index 67d7dd8a87..26cf271a5a 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1664,6 +1664,7 @@ _role: gtlAvailable: "Can view the global timeline" ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" + canInitiateConversation: "Can mention, reply or quote" canCreateContent: "Can create contents" canUpdateContent: "Can edit contents" canDeleteContent: "Can delete contents" diff --git a/locales/index.d.ts b/locales/index.d.ts index ff6b22aaad..bb7a00edea 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6558,6 +6558,10 @@ export interface Locale extends ILocale { * パブリック投稿の許可 */ "canPublicNote": string; + /** + * メンション、リプライ、引用の許可 + */ + "canInitiateConversation": string; /** * コンテンツの作成 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2766d0a913..31a4567ff9 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1697,6 +1697,7 @@ _role: gtlAvailable: "グローバルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" + canInitiateConversation: "メンション、リプライ、引用の許可" canCreateContent: "コンテンツの作成" canUpdateContent: "コンテンツの編集" canDeleteContent: "コンテンツの削除" diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index b775344666..6e442451b9 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -259,13 +259,14 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; const meta = await this.metaService.fetch(); + const policies = await this.roleService.getUserPolicies(user.id); if (data.visibility === 'public' && data.channel == null) { const sensitiveWords = meta.sensitiveWords; if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; this.logger.warn('Visibility changed to home because sensitive words are included', { user: user.id, note: data }); - } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { + } else if (policies.canPublicNote === false) { data.visibility = 'home'; } } @@ -379,6 +380,18 @@ export class NoteCreateService implements OnApplicationShutdown { } } + if (policies.canInitiateConversation === false) { + if ( + mentionedUsers.some(u => u.id !== user.id) + || (data.reply && data.reply.replyUserId !== user.id) + || (data.visibility === 'specified' && data.visibleUsers?.some(u => u.id !== user.id)) + || (this.isQuote(data) && data.renote.userId !== user.id) + ) { + this.logger.error('Request rejected because user has no permission to initiate conversation', { user: user.id, note: data }); + throw new IdentifiableError('332dd91b-6a00-430a-ac39-620cf60ad34b', 'Notes including mentions, replies, or renotes are not allowed.'); + } + } + tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 6c3582fbd8..d8797040f8 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -36,6 +36,7 @@ export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; + canInitiateConversation: boolean; canCreateContent: boolean; canUpdateContent: boolean; canDeleteContent: boolean; @@ -69,6 +70,7 @@ export const DEFAULT_POLICIES: RolePolicies = { gtlAvailable: true, ltlAvailable: true, canPublicNote: true, + canInitiateConversation: true, canCreateContent: true, canUpdateContent: true, canDeleteContent: true, @@ -338,6 +340,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)), + canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)), canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)), canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)), canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index e081f3b7a9..ed83676a2e 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -393,7 +393,7 @@ export class UserEntityService implements OnModuleInit { bannerBlurhash: user.bannerBlurhash, isLocked: user.isLocked, isSilenced: !policies?.canPublicNote, - isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent), + isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation), isSuspended: user.isSuspended, description: profile!.description, location: profile!.location, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 02797476b4..b9cc7a555d 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -212,7 +212,7 @@ export default class extends Endpoint { // eslint- const policies = await this.roleService.getUserPolicies(user.id); const isModerator = await this.roleService.isModerator(user); - const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent); + const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation); const isSilenced = !policies.canPublicNote; const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 5d517ad9c4..02674ef88c 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -75,6 +75,7 @@ export const ROLE_POLICIES = [ 'gtlAvailable', 'ltlAvailable', 'canPublicNote', + 'canInitiateConversation', 'canCreateContent', 'canUpdateContent', 'canDeleteContent', @@ -131,7 +132,7 @@ export const MFM_PARAMS: Record = { position: ['x=', 'y='], fg: ['color='], bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], + border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], blur: [], rainbow: ['speed=', 'delay='], diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index b4052fe335..8ee1958fcb 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+