From 5e0115335a04543a25adeccf5a7d13ffd4c95ef6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 27 Jun 2025 23:20:59 -0400 Subject: [PATCH] add mandatory CW for instances --- locales/index.d.ts | 22 ++++++- .../1751078046239-add-instance-mandatoryCW.js | 14 +++++ .../src/core/activitypub/ApRendererService.ts | 9 ++- .../core/entities/InstanceEntityService.ts | 1 + .../src/core/entities/UserEntityService.ts | 1 + packages/backend/src/misc/get-note-summary.ts | 3 + packages/backend/src/models/Instance.ts | 9 +++ .../models/json-schema/federation-instance.ts | 4 ++ .../backend/src/models/json-schema/user.ts | 4 ++ .../backend/src/server/api/endpoint-list.ts | 1 + .../server/api/endpoints/admin/cw-instance.ts | 54 ++++++++++++++++ .../server/api/mastodon/MastodonConverters.ts | 6 ++ packages/backend/src/types.ts | 6 ++ .../frontend-shared/js/compute-merged-cw.ts | 3 + .../frontend/src/components/SkMutedNote.vue | 10 ++- .../src/pages/admin/modlog.ModLog.vue | 7 +++ packages/frontend/src/pages/instance-info.vue | 22 +++++++ .../frontend/src/utility/check-word-mute.ts | 8 ++- packages/misskey-js/etc/misskey-js.api.md | 14 ++++- .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 ++++ packages/misskey-js/src/autogen/endpoint.ts | 2 + packages/misskey-js/src/autogen/entities.ts | 1 + packages/misskey-js/src/autogen/types.ts | 63 +++++++++++++++++++ packages/misskey-js/src/consts.ts | 7 +++ packages/misskey-js/src/entities.ts | 3 + sharkey-locales/en-US.yml | 5 ++ 26 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 packages/backend/migration/1751078046239-add-instance-mandatoryCW.js create mode 100644 packages/backend/src/server/api/endpoints/admin/cw-instance.ts diff --git a/locales/index.d.ts b/locales/index.d.ts index 8287a5f3b7..06b9e26006 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9216,6 +9216,10 @@ export interface Locale extends ILocale { * Apply mandatory CW on notes */ "write:admin:cw-note": string; + /** + * Apply mandatory CW on instances + */ + "write:admin:cw-instance": string; /** * Silence users */ @@ -10956,6 +10960,10 @@ export interface Locale extends ILocale { * Set content warning for note */ "setMandatoryCWForNote": string; + /** + * Set content warning for instance + */ + "setMandatoryCWForInstance": string; /** * Set remote instance as NSFW */ @@ -12100,6 +12108,10 @@ export interface Locale extends ILocale { * Note is flagged: "{cw}" */ "noteIsFlaggedAs": ParameterizedString<"cw">; + /** + * {name} is flagged: "{cw}" + */ + "instanceIsFlaggedAs": ParameterizedString<"name" | "cw">; /** * Mark all media from user as NSFW */ @@ -13054,13 +13066,21 @@ export interface Locale extends ILocale { */ "mandatoryCWDescription": string; /** - * Add content warning + * Force 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; + /** + * Force content warning + */ + "mandatoryCWForInstance": string; + /** + * Applies a content warning to all posts originating from this instance. The forced warnings will appear like a word mute to distinguish them from the notes' own content warnings. + */ + "mandatoryCWForInstanceDescription": string; /** * Fetch linked note */ diff --git a/packages/backend/migration/1751078046239-add-instance-mandatoryCW.js b/packages/backend/migration/1751078046239-add-instance-mandatoryCW.js new file mode 100644 index 0000000000..8a71b6f5ef --- /dev/null +++ b/packages/backend/migration/1751078046239-add-instance-mandatoryCW.js @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddInstanceMandatoryCW1751078046239 { + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" ADD "mandatoryCW" text`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "mandatoryCW"`); + } +} diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 82c0fba46a..25ad0852cb 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -34,6 +34,7 @@ import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { CacheService } from '@/core/CacheService.js'; import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; @@ -75,9 +76,10 @@ export class ApRendererService { private apMfmService: ApMfmService, private mfmService: MfmService, private idService: IdService, - private readonly queryService: QueryService, private utilityService: UtilityService, + private readonly queryService: QueryService, private readonly cacheService: CacheService, + private readonly federatedInstanceService: FederatedInstanceService, ) { } @@ -398,6 +400,8 @@ export class ApRendererService { return ids.map(id => items.find(item => item.id === id)).filter(x => x != null); }; + const instance = author.instance ?? (author.host ? await this.federatedInstanceService.fetch(author.host) : null); + let inReplyTo; let inReplyToNote: MiNote | null; @@ -503,6 +507,9 @@ export class ApRendererService { if (author.mandatoryCW) { summary = appendContentWarning(summary, author.mandatoryCW); } + if (instance?.mandatoryCW) { + summary = appendContentWarning(summary, instance.mandatoryCW); + } const { content } = this.apMfmService.getNoteHtml(note, apAppend); diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index f1149fe113..09dd54812c 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -68,6 +68,7 @@ export class InstanceEntityService { rejectQuotes: instance.rejectQuotes, moderationNote: iAmModerator ? instance.moderationNote : null, isBubbled: this.utilityService.isBubbledHost(instance.host), + mandatoryCW: instance.mandatoryCW, }; } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 227814454d..af25300944 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -578,6 +578,7 @@ export class UserEntityService implements OnModuleInit { faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, isSilenced: instance.isSilenced, + mandatoryCW: instance.mandatoryCW, } : undefined) : undefined, followersCount: followersCount ?? 0, followingCount: followingCount ?? 0, diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 6080f469b5..3a725e4f43 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -29,6 +29,9 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { if (note.user.mandatoryCW) { cw = appendContentWarning(cw, note.user.mandatoryCW); } + if (note.user.instance?.mandatoryCW) { + cw = appendContentWarning(cw, note.user.instance.mandatoryCW); + } // 本文 if (cw != null) { diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index 0cde4b75fc..b445247176 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -228,4 +228,13 @@ export class MiInstance { length: 16384, default: '', }) public moderationNote: string; + + /** + * Specifies a Content Warning that should be forcibly applied to all notes from this instance + * If null (default), then no Content Warning is applied. + */ + @Column('text', { + nullable: true, + }) + public mandatoryCW: string | null; } diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 0e9ce9334e..51452b4598 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -139,5 +139,9 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + mandatoryCW: { + type: 'string', + optional: false, nullable: true, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 4a2d5780d1..0c9f9f540a 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -228,6 +228,10 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: false, }, + mandatoryCW: { + type: 'string', + nullable: true, optional: false, + }, }, }, followersCount: { diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 392b45eab6..446f554bcc 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-instance' from './endpoints/admin/cw-instance.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'; diff --git a/packages/backend/src/server/api/endpoints/admin/cw-instance.ts b/packages/backend/src/server/api/endpoints/admin/cw-instance.ts new file mode 100644 index 0000000000..5fd9fd10b3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/cw-instance.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:cw-instance', +} as const; + +export const paramDef = { + type: 'object', + properties: { + host: { type: 'string' }, + cw: { type: 'string', nullable: true }, + }, + required: ['host', 'cw'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private readonly moderationLogService: ModerationLogService, + private readonly federatedInstanceService: FederatedInstanceService, + ) { + super(meta, paramDef, async (ps, me) => { + const instance = await this.federatedInstanceService.fetchOrRegister(ps.host); + + // Skip if there's nothing to do + if (instance.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, 'setMandatoryCWForInstance', { + newCW: ps.cw, + oldCW: instance.mandatoryCW, + host: ps.host, + }); + + await this.federatedInstanceService.update(instance.id, { + // Collapse empty strings to null + 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 2664a65b82..42a1836e5c 100644 --- a/packages/backend/src/server/api/mastodon/MastodonConverters.ts +++ b/packages/backend/src/server/api/mastodon/MastodonConverters.ts @@ -23,6 +23,7 @@ import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.j import { GetterService } from '@/server/api/GetterService.js'; import { appendContentWarning } from '@/misc/append-content-warning.js'; import { isRenote } from '@/misc/is-renote.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; // Missing from Megalodon apparently // https://docs.joinmastodon.org/entities/StatusEdit/ @@ -68,6 +69,7 @@ export class MastodonConverters { private readonly idService: IdService, private readonly driveFileEntityService: DriveFileEntityService, private readonly mastodonDataService: MastodonDataService, + private readonly federatedInstanceService: FederatedInstanceService, ) {} private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention { @@ -210,6 +212,7 @@ export class MastodonConverters { } const noteUser = await this.getUser(note.userId); + const noteInstance = noteUser.instance ?? (noteUser.host ? await this.federatedInstanceService.fetch(noteUser.host) : null); const account = await this.convertAccount(noteUser); const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } }); const history: StatusEdit[] = []; @@ -231,6 +234,9 @@ export class MastodonConverters { if (noteUser.mandatoryCW) { cw = appendContentWarning(cw, noteUser.mandatoryCW); } + if (noteInstance?.mandatoryCW) { + cw = appendContentWarning(cw, noteInstance.mandatoryCW); + } const isQuote = renote && (edit.cw || edit.newText || edit.fileIds.length > 0 || note.replyId); const quoteUri = isQuote diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 0e1d36da55..cbc490156f 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -107,6 +107,7 @@ export const moderationLogTypes = [ 'resetPassword', 'setMandatoryCW', 'setMandatoryCWForNote', + 'setMandatoryCWForInstance', 'setRemoteInstanceNSFW', 'unsetRemoteInstanceNSFW', 'suspendRemoteInstance', @@ -303,6 +304,11 @@ export type ModerationLogPayloads = { noteUserUsername: string; noteUserHost: string | null; }; + setMandatoryCWForInstance: { + newCW: string | null; + oldCW: string | null; + host: string; + }; setRemoteInstanceNSFW: { id: string; host: string; diff --git a/packages/frontend-shared/js/compute-merged-cw.ts b/packages/frontend-shared/js/compute-merged-cw.ts index 7d197accea..88dcce2c04 100644 --- a/packages/frontend-shared/js/compute-merged-cw.ts +++ b/packages/frontend-shared/js/compute-merged-cw.ts @@ -15,6 +15,9 @@ export function computeMergedCw(note: Misskey.entities.Note): string | null { if (note.user.mandatoryCW) { cw = appendContentWarning(cw, note.user.mandatoryCW); } + if (note.user.instance?.mandatoryCW) { + cw = appendContentWarning(cw, note.user.instance.mandatoryCW); + } return cw ?? null; } diff --git a/packages/frontend/src/components/SkMutedNote.vue b/packages/frontend/src/components/SkMutedNote.vue index ffb197323d..1e7c2fbe17 100644 --- a/packages/frontend/src/components/SkMutedNote.vue +++ b/packages/frontend/src/components/SkMutedNote.vue @@ -26,6 +26,14 @@ Displays a placeholder for a muted note. {{ mute.userMandatoryCW }} + + + + @@ -89,7 +97,7 @@ const expandNote = ref(false); const mute = computed(() => checkMute(props.note, props.withHardMute)); const mutedWords = computed(() => mute.value.softMutedWords?.join(', ')); -const isMuted = computed(() => mute.value.hardMuted || mutedWords.value || mute.value.noteMandatoryCW || mute.value.userMandatoryCW || mute.value.noteMuted || mute.value.threadMuted || mute.value.sensitiveMuted); +const isMuted = computed(() => mute.value.hardMuted || mutedWords.value || mute.value.noteMandatoryCW || mute.value.userMandatoryCW || mute.value.instanceMandatoryCW || mute.value.noteMuted || mute.value.threadMuted || mute.value.sensitiveMuted); const isExpanded = computed(() => expandNote.value || !isMuted.value); const rootClass = computed(() => isExpanded.value ? props.expandedClass : undefined); diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 59de4cf7e8..1e3706c90e 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only 'resetPassword', 'setMandatoryCW', 'setMandatoryCWForNote', + 'setMandatoryCWForInstance', 'suspendRemoteInstance', 'setRemoteInstanceNSFW', 'unsetRemoteInstanceNSFW', @@ -81,6 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} : @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }} + : {{ log.info.host }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} {{ log.info.roleName }} : @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} {{ log.info.roleName }} : {{ log.info.role.name }} @@ -217,6 +219,11 @@ SPDX-License-Identifier: AGPL-3.0-only + diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 3532d16c47..813b1a471f 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -123,6 +123,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.mediaSilencedByBase }} {{ i18n.ts.mediaSilenceThisInstance }} + + + + +
{{ i18n.ts.updateRemoteUser }} {{ i18n.ts.deleteAllFiles }} @@ -234,6 +239,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard'; import MkFolder from '@/components/MkFolder.vue'; import MkNumber from '@/components/MkNumber.vue'; import SkBadgeStrip from '@/components/SkBadgeStrip.vue'; +import MkInput from '@/components/MkInput.vue'; const props = withDefaults(defineProps<{ host: string; @@ -259,6 +265,7 @@ const rejectReports = ref(false); const isMediaSilenced = ref(false); const faviconUrl = ref(null); const moderationNote = ref(''); +const mandatoryCW = ref(null); const baseDomains = computed(() => { const domains: string[] = []; @@ -306,6 +313,13 @@ const badges = computed(() => { style: 'warning', }); } + if (instance.value.mandatoryCW) { + arr.push({ + key: 'cw', + label: i18n.ts.cw, + style: 'warning', + }); + } if (instance.value.isNSFW) { arr.push({ key: 'nsfw', @@ -365,6 +379,13 @@ async function saveModerationNote() { } } +async function onMandatoryCWChanged(value: string | number) { + await os.promiseDialog(async () => { + await misskeyApi('admin/cw-instance', { host: props.host, cw: String(value) || null }); + await fetch(); + }); +} + async function fetch(withHint = false): Promise { const [m, i] = await Promise.all([ (withHint && props.metaHint) @@ -389,6 +410,7 @@ async function fetch(withHint = false): Promise { isMediaSilenced.value = instance.value?.isMediaSilenced ?? false; faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); moderationNote.value = instance.value?.moderationNote ?? ''; + mandatoryCW.value = instance.value?.mandatoryCW ?? ''; } async function toggleBlock(): Promise { diff --git a/packages/frontend/src/utility/check-word-mute.ts b/packages/frontend/src/utility/check-word-mute.ts index b0e4dda777..b829ecebe0 100644 --- a/packages/frontend/src/utility/check-word-mute.ts +++ b/packages/frontend/src/utility/check-word-mute.ts @@ -21,6 +21,7 @@ export interface Mute { noteMandatoryCW?: string | null; // TODO show this as a single block on user timelines userMandatoryCW?: string | null; + instanceMandatoryCW?: string | null; } export function checkMute(note: Misskey.entities.Note, withHardMute?: boolean): Mute { @@ -35,20 +36,21 @@ export function checkMute(note: Misskey.entities.Note, withHardMute?: boolean): const noteMuted = note.isMutingNote; const noteMandatoryCW = note.mandatoryCW; const userMandatoryCW = note.user.mandatoryCW; + const instanceMandatoryCW = note.user.instance?.mandatoryCW; // Hard mute if (withHardMute && isHardMuted(note)) { - return { hardMuted: true, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW }; + return { hardMuted: true, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW }; } // Soft mute const softMutedWords = isSoftMuted(note); if (softMutedWords.length > 0) { - return { softMutedWords, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW }; + return { softMutedWords, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW }; } // Other / no mute - return { sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW }; + return { sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW }; } function isHardMuted(note: Misskey.entities.Note): boolean { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 86b59bdc8b..3575786e88 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 AdminCwInstanceRequest = operations['admin___cw-instance']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminCwNoteRequest = operations['admin___cw-note']['requestBody']['content']['application/json']; @@ -1545,6 +1548,7 @@ declare namespace entities { AdminAvatarDecorationsUpdateRequest, AdminCaptchaCurrentResponse, AdminCaptchaSaveRequest, + AdminCwInstanceRequest, AdminCwNoteRequest, AdminCwUserRequest, AdminDeclineUserRequest, @@ -2954,6 +2958,12 @@ type ModerationLog = { } | { type: 'setMandatoryCW'; info: ModerationLogPayloads['setMandatoryCW']; +} | { + type: 'setMandatoryCWForNote'; + info: ModerationLogPayloads['setMandatoryCWForNote']; +} | { + type: 'setMandatoryCWForInstance'; + info: ModerationLogPayloads['setMandatoryCWForInstance']; } | { type: 'setRemoteInstanceNSFW'; info: ModerationLogPayloads['setRemoteInstanceNSFW']; @@ -3101,7 +3111,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", "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"]; +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", "setMandatoryCWForInstance", "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']; @@ -3411,7 +3421,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: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"]; +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:cw-instance", "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 9246d4ae4f..0e061c8e06 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-instance* + */ + 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 428b9e7a46..bf7d179c50 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, + AdminCwInstanceRequest, AdminCwNoteRequest, AdminCwUserRequest, AdminDeclineUserRequest, @@ -690,6 +691,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-instance': { req: AdminCwInstanceRequest; res: EmptyResponse }; 'admin/cw-note': { req: AdminCwNoteRequest; res: EmptyResponse }; 'admin/cw-user': { req: AdminCwUserRequest; res: EmptyResponse }; 'admin/decline-user': { req: AdminDeclineUserRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index f0e16175e4..4d2a669cf3 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 AdminCwInstanceRequest = operations['admin___cw-instance']['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']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index ab0ac51004..f41bca6fb3 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-instance': { + /** + * admin/cw-instance + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:cw-instance* + */ + post: operations['admin___cw-instance']; + }; '/admin/cw-note': { /** * admin/cw-note @@ -4309,6 +4318,7 @@ export type components = { faviconUrl: string | null; themeColor: string | null; isSilenced: boolean; + mandatoryCW: string | null; }; followersCount: number; followingCount: number; @@ -5341,6 +5351,7 @@ export type components = { rejectQuotes: boolean; moderationNote?: string | null; isBubbled: boolean; + mandatoryCW: string | null; }; GalleryPost: { /** @@ -7399,6 +7410,58 @@ export type operations = { }; }; }; + /** + * admin/cw-instance + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:cw-instance* + */ + 'admin___cw-instance': { + requestBody: { + content: { + 'application/json': { + host: 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-note * @description No description provided. diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index f183d5352a..25e02a9f1c 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -82,6 +82,7 @@ export const permissions = [ 'write:admin:unnsfw-user', 'write:admin:cw-user', 'write:admin:cw-note', + 'write:admin:cw-instance', 'write:admin:silence-user', 'write:admin:unsilence-user', 'write:admin:unset-user-avatar', @@ -150,6 +151,7 @@ export const moderationLogTypes = [ 'resetPassword', 'setMandatoryCW', 'setMandatoryCWForNote', + 'setMandatoryCWForInstance', 'setRemoteInstanceNSFW', 'unsetRemoteInstanceNSFW', 'suspendRemoteInstance', @@ -353,6 +355,11 @@ export type ModerationLogPayloads = { noteUserUsername: string; noteUserHost: string | null; }; + setMandatoryCWForInstance: { + newCW: string | null; + oldCW: string | null; + host: string; + }; setRemoteInstanceNSFW: { id: string; host: string; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 697ff3f9e7..b42d52115c 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -136,6 +136,9 @@ export type ModerationLog = { } | { type: 'setMandatoryCWForNote'; info: ModerationLogPayloads['setMandatoryCWForNote']; +} | { + type: 'setMandatoryCWForInstance'; + info: ModerationLogPayloads['setMandatoryCWForInstance']; } | { type: 'setRemoteInstanceNSFW'; info: ModerationLogPayloads['setRemoteInstanceNSFW']; diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 891f2ca154..96a3d58fe0 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -37,6 +37,7 @@ 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}\"" +instanceIsFlaggedAs: "{name} 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?" @@ -346,6 +347,7 @@ _moderationLogTypes: decline: "Declined" setMandatoryCW: "Set content warning for user" setMandatoryCWForNote: "Set content warning for note" + setMandatoryCWForInstance: "Set content warning for instance" setRemoteInstanceNSFW: "Set remote instance as NSFW" unsetRemoteInstanceNSFW: "Unset remote instance as NSFW" rejectRemoteInstanceReports: "Rejected reports from remote instance" @@ -507,6 +509,7 @@ _permissions: "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:cw-instance": "Apply mandatory CW on instances" "write:admin:silence-user": "Silence users" "write:admin:unsilence-user": "Un-silence users" "write:admin:reject-quotes": "Allow/Prohibit quote posts from a user" @@ -548,6 +551,8 @@ mandatoryCW: "Force content warning" 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." +mandatoryCWForInstance: "Force content warning" +mandatoryCWForInstanceDescription: "Applies a content warning to all posts originating from this instance. The forced warnings will appear like a word mute to distinguish them from the notes' own content warnings." 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."