diff --git a/locales/index.d.ts b/locales/index.d.ts index bb953a4a5e..8287a5f3b7 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9212,6 +9212,10 @@ export interface Locale extends ILocale { * Apply mandatory CW on users */ "write:admin:cw-user": string; + /** + * Apply mandatory CW on notes + */ + "write:admin:cw-note": string; /** * Silence users */ @@ -10948,6 +10952,10 @@ export interface Locale extends ILocale { * Set content warning for user */ "setMandatoryCW": string; + /** + * Set content warning for note + */ + "setMandatoryCWForNote": string; /** * Set remote instance as NSFW */ @@ -12088,6 +12096,10 @@ export interface Locale extends ILocale { * {name} is flagged: "{cw}" */ "userIsFlaggedAs": ParameterizedString<"name" | "cw">; + /** + * Note is flagged: "{cw}" + */ + "noteIsFlaggedAs": ParameterizedString<"cw">; /** * Mark all media from user as NSFW */ @@ -13038,9 +13050,17 @@ export interface Locale extends ILocale { */ "mandatoryCW": string; /** - * Applies a content warning to all posts created by this user. If the post already has a CW, then this is appended to the end. + * Applies a content warning to all posts created by this user. The forced warnings will appear like a word mute to distinguish them from the author's own content warnings. */ "mandatoryCWDescription": string; + /** + * Add content warning + */ + "mandatoryCWForNote": string; + /** + * Applies an additional content warning to this post. The new warning will appear like a word mute to distinguish it from the author's own content warning. + */ + "mandatoryCWForNoteDescription": string; /** * Fetch linked note */ diff --git a/packages/backend/migration/1751077195277-add-note-mandatoryCW.js b/packages/backend/migration/1751077195277-add-note-mandatoryCW.js new file mode 100644 index 0000000000..9ebe27d4dd --- /dev/null +++ b/packages/backend/migration/1751077195277-add-note-mandatoryCW.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddNoteMandatoryCW1751077195277 { + name = 'AddNoteMandatoryCW1751077195277' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "mandatoryCW" text`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "mandatoryCW"`); + } +} diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 74a8b79a89..96b06ac0c2 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -127,6 +127,7 @@ function generateDummyNote(override?: Partial): MiNote { renoteUserInstance: null, updatedAt: null, processErrors: [], + mandatoryCW: null, ...override, }; } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index a13abc6369..82c0fba46a 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -497,6 +497,9 @@ export class ApRendererService { let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; // Apply mandatory CW, if applicable + if (note.mandatoryCW) { + summary = appendContentWarning(summary, note.mandatoryCW); + } if (author.mandatoryCW) { summary = appendContentWarning(summary, author.mandatoryCW); } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 029a0b6ebb..3d329684ea 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -651,6 +651,7 @@ export class NoteEntityService implements OnModuleInit { user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me), text: text, cw: note.cw, + mandatoryCW: note.mandatoryCW, visibility: note.visibility, localOnly: note.localOnly, reactionAcceptance: note.reactionAcceptance, diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index be2d3ea98d..6080f469b5 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -23,6 +23,9 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { // Append mandatory CW, if applicable let cw = note.cw; + if (note.mandatoryCW) { + cw = appendContentWarning(cw, note.mandatoryCW); + } if (note.user.mandatoryCW) { cw = appendContentWarning(cw, note.user.mandatoryCW); } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index b064e95493..74e3630303 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -228,6 +228,15 @@ export class MiNote { }) public processErrors: string[] | null; + /** + * Specifies a Content Warning that should be forcibly attached to this note. + * Does not replace the user's own CW. + */ + @Column('text', { + nullable: true, + }) + public mandatoryCW: string | null; + //#region Denormalized fields @Column('varchar', { length: 128, nullable: true, diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index b57458235f..6d3b9fe15e 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -41,6 +41,10 @@ export const packedNoteSchema = { type: 'string', optional: true, nullable: true, }, + mandatoryCW: { + type: 'string', + optional: true, nullable: true, + }, userId: { type: 'string', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 962055052d..392b45eab6 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -34,6 +34,7 @@ export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decor export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js'; export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js'; export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js'; +export * as 'admin/cw-note' from './endpoints/admin/cw-note.js'; export * as 'admin/cw-user' from './endpoints/admin/cw-user.js'; export * as 'admin/decline-user' from './endpoints/admin/decline-user.js'; export * as 'admin/delete-account' from './endpoints/admin/delete-account.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/cw-note.ts b/packages/backend/src/server/api/endpoints/admin/cw-note.ts new file mode 100644 index 0000000000..498ad464ab --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/cw-note.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { MiNote, MiUser, NotesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:cw-note', +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + cw: { type: 'string', nullable: true }, + }, + required: ['noteId', 'cw'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private readonly notesRepository: NotesRepository, + + private readonly moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.notesRepository.findOneOrFail({ + where: { id: ps.noteId }, + relations: { user: true }, + }) as MiNote & { user: MiUser }; + + // Skip if there's nothing to do + if (note.mandatoryCW === ps.cw) return; + + // Log event first. + // This ensures that we don't "lose" the log if an error occurs + await this.moderationLogService.log(me, 'setMandatoryCWForNote', { + newCW: ps.cw, + oldCW: note.mandatoryCW, + noteId: note.id, + noteUserId: note.user.id, + noteUserUsername: note.user.username, + noteUserHost: note.user.host, + }); + + await this.notesRepository.update(ps.noteId, { + // Collapse empty strings to null + mandatoryCW: ps.cw || null, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/cw-user.ts b/packages/backend/src/server/api/endpoints/admin/cw-user.ts index 07f7027756..37480e9302 100644 --- a/packages/backend/src/server/api/endpoints/admin/cw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/cw-user.ts @@ -46,7 +46,6 @@ export default class extends Endpoint { // eslint- await this.usersRepository.update(ps.userId, { // Collapse empty strings to null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing mandatoryCW: ps.cw || null, }); diff --git a/packages/backend/src/server/api/mastodon/MastodonConverters.ts b/packages/backend/src/server/api/mastodon/MastodonConverters.ts index df8d68042a..2664a65b82 100644 --- a/packages/backend/src/server/api/mastodon/MastodonConverters.ts +++ b/packages/backend/src/server/api/mastodon/MastodonConverters.ts @@ -224,7 +224,13 @@ export class MastodonConverters { // TODO avoid re-packing files for each edit const files = await this.driveFileEntityService.packManyByIds(edit.fileIds); - const cw = appendContentWarning(edit.cw, noteUser.mandatoryCW) ?? ''; + let cw = edit.cw ?? ''; + if (note.mandatoryCW) { + cw = appendContentWarning(cw, note.mandatoryCW); + } + if (noteUser.mandatoryCW) { + cw = appendContentWarning(cw, noteUser.mandatoryCW); + } const isQuote = renote && (edit.cw || edit.newText || edit.fileIds.length > 0 || note.replyId); const quoteUri = isQuote @@ -299,7 +305,13 @@ export class MastodonConverters { ? quoteUri.then(quote => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quote) ?? escapeMFM(text)) : ''; - const cw = appendContentWarning(note.cw, noteUser.mandatoryCW) ?? ''; + let cw = note.cw ?? ''; + if (note.mandatoryCW) { + cw = appendContentWarning(cw, note.mandatoryCW); + } + if (noteUser.mandatoryCW) { + cw = appendContentWarning(cw, noteUser.mandatoryCW); + } const reblogged = await this.mastodonDataService.hasReblog(note.id, me); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 8aa047a3ab..0e1d36da55 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -106,6 +106,7 @@ export const moderationLogTypes = [ 'deleteUserAnnouncement', 'resetPassword', 'setMandatoryCW', + 'setMandatoryCWForNote', 'setRemoteInstanceNSFW', 'unsetRemoteInstanceNSFW', 'suspendRemoteInstance', @@ -294,6 +295,14 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + setMandatoryCWForNote: { + newCW: string | null; + oldCW: string | null; + noteId: string; + noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; + }; setRemoteInstanceNSFW: { id: string; host: string; diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts index 63e3795a84..8f241cf0c7 100644 --- a/packages/backend/test/unit/NoteCreateService.ts +++ b/packages/backend/test/unit/NoteCreateService.ts @@ -65,6 +65,7 @@ describe('NoteCreateService', () => { renoteUserHost: null, renoteUserInstance: null, processErrors: [], + mandatoryCW: null, }; const poll: IPoll = { diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts index b6cfa53466..c061cce3e6 100644 --- a/packages/backend/test/unit/misc/is-renote.ts +++ b/packages/backend/test/unit/misc/is-renote.ts @@ -48,6 +48,7 @@ const base: MiNote = { renoteUserHost: null, renoteUserInstance: null, processErrors: [], + mandatoryCW: null, }; describe('misc:is-renote', () => { diff --git a/packages/frontend-shared/js/compute-merged-cw.ts b/packages/frontend-shared/js/compute-merged-cw.ts index dfea57fdce..7d197accea 100644 --- a/packages/frontend-shared/js/compute-merged-cw.ts +++ b/packages/frontend-shared/js/compute-merged-cw.ts @@ -9,6 +9,9 @@ import { appendContentWarning } from '@@/js/append-content-warning.js'; export function computeMergedCw(note: Misskey.entities.Note): string | null { let cw = note.cw; + if (note.mandatoryCW) { + cw = appendContentWarning(cw, note.mandatoryCW); + } if (note.user.mandatoryCW) { cw = appendContentWarning(cw, note.user.mandatoryCW); } diff --git a/packages/frontend/src/components/SkMutedNote.vue b/packages/frontend/src/components/SkMutedNote.vue index e85316848c..9fb6e4465d 100644 --- a/packages/frontend/src/components/SkMutedNote.vue +++ b/packages/frontend/src/components/SkMutedNote.vue @@ -13,6 +13,11 @@ Displays a placeholder for a muted note.
+ + + + @@ -323,6 +332,8 @@ SPDX-License-Identifier: AGPL-3.0-only
+ +
raw
{{ JSON5.stringify(log, null, '\t') }}
@@ -338,6 +349,8 @@ import JSON5 from 'json5'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; import SkFetchNote from '@/components/SkFetchNote.vue'; +import MkUrlPreview from '@/components/MkUrlPreview.vue'; +import { url } from '@@/js/config.js'; const props = defineProps<{ log: Misskey.entities.ModerationLog; diff --git a/packages/frontend/src/utility/check-word-mute.ts b/packages/frontend/src/utility/check-word-mute.ts index 9a6ad6b9c9..b0e4dda777 100644 --- a/packages/frontend/src/utility/check-word-mute.ts +++ b/packages/frontend/src/utility/check-word-mute.ts @@ -18,6 +18,7 @@ export interface Mute { threadMuted?: boolean; noteMuted?: boolean; + noteMandatoryCW?: string | null; // TODO show this as a single block on user timelines userMandatoryCW?: string | null; } @@ -32,21 +33,22 @@ export function checkMute(note: Misskey.entities.Note, withHardMute?: boolean): const threadMuted = note.isMutingThread; const noteMuted = note.isMutingNote; + const noteMandatoryCW = note.mandatoryCW; const userMandatoryCW = note.user.mandatoryCW; // Hard mute if (withHardMute && isHardMuted(note)) { - return { hardMuted: true, sensitiveMuted, threadMuted, noteMuted, userMandatoryCW }; + return { hardMuted: true, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW }; } // Soft mute const softMutedWords = isSoftMuted(note); if (softMutedWords.length > 0) { - return { softMutedWords, sensitiveMuted, threadMuted, noteMuted, userMandatoryCW }; + return { softMutedWords, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW }; } // Other / no mute - return { sensitiveMuted, threadMuted, noteMuted, userMandatoryCW }; + return { sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW }; } function isHardMuted(note: Misskey.entities.Note): boolean { diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts index a7eb2f51a4..e73067baed 100644 --- a/packages/frontend/src/utility/get-note-menu.ts +++ b/packages/frontend/src/utility/get-note-menu.ts @@ -151,6 +151,28 @@ export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): Men }; } +export function getMandatoryCWMenu(note: Misskey.entities.Note): MenuItem { + return { + icon: 'ph-warning ph-bold ph-lg', + text: i18n.ts.mandatoryCWForNote, + action: async () => { + const result = await os.inputText({ + type: 'text', + title: i18n.ts.mandatoryCWForNote, + text: i18n.ts.mandatoryCWForNoteDescription, + default: note.mandatoryCW ?? '', + }); + + if (result.canceled) return; + + await os.apiWithDialog('admin/cw-note', { + noteId: note.id, + cw: result.result || null, + }); + }, + }; +} + export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem { return { icon: 'ti ti-link', @@ -442,6 +464,10 @@ export function getNoteMenu(props: { if (appearNote.userId !== $i.id) { menuItems.push({ type: 'divider' }); menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); + + if ($i.isModerator || $i.isAdmin) { + menuItems.push(getMandatoryCWMenu(appearNote)); + } } if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { diff --git a/packages/frontend/src/utility/get-note-summary.ts b/packages/frontend/src/utility/get-note-summary.ts index 4e093bcf4c..d91ff6853a 100644 --- a/packages/frontend/src/utility/get-note-summary.ts +++ b/packages/frontend/src/utility/get-note-summary.ts @@ -28,6 +28,9 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // Append mandatory CW, if applicable let cw = note.cw; + if (note.mandatoryCW) { + cw = appendContentWarning(cw, note.mandatoryCW); + } if (note.user.mandatoryCW) { cw = appendContentWarning(cw, note.user.mandatoryCW); } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index a553353d2a..86b59bdc8b 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -152,6 +152,9 @@ type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['resp // @public (undocumented) type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminCwNoteRequest = operations['admin___cw-note']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminCwUserRequest = operations['admin___cw-user']['requestBody']['content']['application/json']; @@ -1542,6 +1545,7 @@ declare namespace entities { AdminAvatarDecorationsUpdateRequest, AdminCaptchaCurrentResponse, AdminCaptchaSaveRequest, + AdminCwNoteRequest, AdminCwUserRequest, AdminDeclineUserRequest, AdminDeleteAccountRequest, @@ -3097,7 +3101,7 @@ type ModerationLog = { }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "decline", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "setMandatoryCW", "setRemoteInstanceNSFW", "unsetRemoteInstanceNSFW", "suspendRemoteInstance", "unsuspendRemoteInstance", "rejectRemoteInstanceReports", "acceptRemoteInstanceReports", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "decline", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "setMandatoryCW", "setMandatoryCWForNote", "setRemoteInstanceNSFW", "unsetRemoteInstanceNSFW", "suspendRemoteInstance", "unsuspendRemoteInstance", "rejectRemoteInstanceReports", "acceptRemoteInstanceReports", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom"]; // @public (undocumented) type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; @@ -3407,7 +3411,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:cw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:reject-quotes", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:chat", "read:chat"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:cw-user", "write:admin:cw-note", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:reject-quotes", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:chat", "read:chat"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index be7479b009..9246d4ae4f 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -272,6 +272,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:cw-note* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index f08323d783..428b9e7a46 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -38,6 +38,7 @@ import type { AdminAvatarDecorationsUpdateRequest, AdminCaptchaCurrentResponse, AdminCaptchaSaveRequest, + AdminCwNoteRequest, AdminCwUserRequest, AdminDeclineUserRequest, AdminDeleteAccountRequest, @@ -689,6 +690,7 @@ export type Endpoints = { 'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse }; 'admin/captcha/current': { req: EmptyRequest; res: AdminCaptchaCurrentResponse }; 'admin/captcha/save': { req: AdminCaptchaSaveRequest; res: EmptyResponse }; + 'admin/cw-note': { req: AdminCwNoteRequest; res: EmptyResponse }; 'admin/cw-user': { req: AdminCwUserRequest; res: EmptyResponse }; 'admin/decline-user': { req: AdminDeclineUserRequest; res: EmptyResponse }; 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index cf40793792..f0e16175e4 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -41,6 +41,7 @@ export type AdminAvatarDecorationsListResponse = operations['admin___avatar-deco export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json']; export type AdminCaptchaCurrentResponse = operations['admin___captcha___current']['responses']['200']['content']['application/json']; export type AdminCaptchaSaveRequest = operations['admin___captcha___save']['requestBody']['content']['application/json']; +export type AdminCwNoteRequest = operations['admin___cw-note']['requestBody']['content']['application/json']; export type AdminCwUserRequest = operations['admin___cw-user']['requestBody']['content']['application/json']; export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json']; export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 7f5810598b..ab0ac51004 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -233,6 +233,15 @@ export type paths = { */ post: operations['admin___captcha___save']; }; + '/admin/cw-note': { + /** + * admin/cw-note + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:cw-note* + */ + post: operations['admin___cw-note']; + }; '/admin/cw-user': { /** * admin/cw-user @@ -4671,6 +4680,7 @@ export type components = { deletedAt?: string | null; text: string | null; cw?: string | null; + mandatoryCW?: string | null; /** Format: id */ userId: string; user: components['schemas']['UserLite']; @@ -7389,6 +7399,59 @@ export type operations = { }; }; }; + /** + * admin/cw-note + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:cw-note* + */ + 'admin___cw-note': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + cw: string | null; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/cw-user * @description No description provided. diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index d0874aad3a..f183d5352a 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -81,6 +81,7 @@ export const permissions = [ 'write:admin:nsfw-user', 'write:admin:unnsfw-user', 'write:admin:cw-user', + 'write:admin:cw-note', 'write:admin:silence-user', 'write:admin:unsilence-user', 'write:admin:unset-user-avatar', @@ -148,6 +149,7 @@ export const moderationLogTypes = [ 'deleteUserAnnouncement', 'resetPassword', 'setMandatoryCW', + 'setMandatoryCWForNote', 'setRemoteInstanceNSFW', 'unsetRemoteInstanceNSFW', 'suspendRemoteInstance', @@ -343,6 +345,14 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + setMandatoryCWForNote: { + newCW: string | null; + oldCW: string | null; + noteId: string; + noteUserId: string; + noteUserUsername: string; + noteUserHost: string | null; + }; setRemoteInstanceNSFW: { id: string; host: string; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 29eb169ab8..697ff3f9e7 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -133,6 +133,9 @@ export type ModerationLog = { } | { type: 'setMandatoryCW'; info: ModerationLogPayloads['setMandatoryCW']; +} | { + type: 'setMandatoryCWForNote'; + info: ModerationLogPayloads['setMandatoryCWForNote']; } | { type: 'setRemoteInstanceNSFW'; info: ModerationLogPayloads['setRemoteInstanceNSFW']; diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 47b6f80bdb..891f2ca154 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -36,6 +36,7 @@ unmuteNote: "Unmute note" userSaysSomethingInMutedNote: "{name} said something in a muted post" userSaysSomethingInMutedThread: "{name} said something in a muted thread" userIsFlaggedAs: "{name} is flagged: \"{cw}\"" +noteIsFlaggedAs: "Note is flagged: \"{cw}\"" markAsNSFW: "Mark all media from user as NSFW" markInstanceAsNSFW: "Mark as NSFW" nsfwConfirm: "Are you sure that you want to mark all media from this account as NSFW?" @@ -344,6 +345,7 @@ _moderationLogTypes: approve: "Approved" decline: "Declined" setMandatoryCW: "Set content warning for user" + setMandatoryCWForNote: "Set content warning for note" setRemoteInstanceNSFW: "Set remote instance as NSFW" unsetRemoteInstanceNSFW: "Unset remote instance as NSFW" rejectRemoteInstanceReports: "Rejected reports from remote instance" @@ -504,6 +506,7 @@ _permissions: "write:admin:nsfw-user": "Mark users as NSFW" "write:admin:unnsfw-user": "Mark users as not NSFW" "write:admin:cw-user": "Apply mandatory CW on users" + "write:admin:cw-note": "Apply mandatory CW on notes" "write:admin:silence-user": "Silence users" "write:admin:unsilence-user": "Un-silence users" "write:admin:reject-quotes": "Allow/Prohibit quote posts from a user" @@ -542,7 +545,9 @@ _noteSearch: id: "ID" mandatoryCW: "Force content warning" -mandatoryCWDescription: "Applies a content warning to all posts created by this user. If the post already has a CW, then this is appended to the end." +mandatoryCWDescription: "Applies a content warning to all posts created by this user. The forced warnings will appear like a word mute to distinguish them from the author's own content warnings." +mandatoryCWForNote: "Force content warning" +mandatoryCWForNoteDescription: "Applies an additional content warning to this post. The new warning will appear like a word mute to distinguish it from the author's own content warning." fetchLinkedNote: "Fetch linked note" showTranslationButtonInNoteFooter: "Add \"Translate\" to note action menu" translationFailed: "Failed to translate note. Please try again later or contact an administrator for assistance."