add mandatory CW for instances

This commit is contained in:
Hazelnoot 2025-06-27 23:20:59 -04:00
parent 595c004a74
commit 5e0115335a
26 changed files with 282 additions and 8 deletions

22
locales/index.d.ts vendored
View file

@ -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
*/

View file

@ -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"`);
}
}

View file

@ -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);

View file

@ -68,6 +68,7 @@ export class InstanceEntityService {
rejectQuotes: instance.rejectQuotes,
moderationNote: iAmModerator ? instance.moderationNote : null,
isBubbled: this.utilityService.isBubbledHost(instance.host),
mandatoryCW: instance.mandatoryCW,
};
}

View file

@ -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,

View file

@ -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) {

View file

@ -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;
}

View file

@ -139,5 +139,9 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
mandatoryCW: {
type: 'string',
optional: false, nullable: true,
},
},
} as const;

View file

@ -228,6 +228,10 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: false,
},
mandatoryCW: {
type: 'string',
nullable: true, optional: false,
},
},
},
followersCount: {

View file

@ -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';

View file

@ -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<typeof meta, typeof paramDef> { // 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,
});
});
}
}

View file

@ -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

View file

@ -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;

View file

@ -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;
}

View file

@ -26,6 +26,14 @@ Displays a placeholder for a muted note.
{{ mute.userMandatoryCW }}
</template>
</I18n>
<I18n v-if="mute.instanceMandatoryCW" :src="i18n.ts.instanceIsFlaggedAs" tag="small">
<template #name>
{{ note.user.instance?.name ?? note.user.host }}
</template>
<template #cw>
{{ mute.instanceMandatoryCW }}
</template>
</I18n>
<!-- Muted notes/threads -->
<I18n v-if="mute.noteMuted" :src="i18n.ts.userSaysSomethingInMutedNote" tag="small">
@ -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);

View file

@ -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
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'setMandatoryCW'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'setMandatoryCWForNote'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
<span v-else-if="log.type === 'setMandatoryCWForInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span>
<span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span>
<span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span>
@ -217,6 +219,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="0" :hideHeader="true" :oldString="log.info.oldCW ?? ''" :newString="log.info.newCW ?? ''" maxHeight="150px"/>
</div>
</template>
<template v-else-if="log.type === 'setMandatoryCWForInstance'">
<div :class="$style.diff">
<CodeDiff :context="0" :hideHeader="true" :oldString="log.info.oldCW ?? ''" :newString="log.info.newCW ?? ''" maxHeight="150px"/>
</div>
</template>
<template v-else-if="log.type === 'unsuspend'">
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
</template>

View file

@ -123,6 +123,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
<MkInput v-model="mandatoryCW" type="text" manualSave @update:modelValue="onMandatoryCWChanged">
<template #label>{{ i18n.ts.mandatoryCW }}</template>
<template #caption>{{ i18n.ts.mandatoryCWDescription }}</template>
</MkInput>
<div :class="$style.buttonStrip">
<MkButton inline :disabled="!instance" @click="refreshMetadata"><i class="ph-cloud-arrow-down ph-bold ph-lg"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
<MkButton inline :disabled="!instance" danger @click="deleteAllFiles"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
@ -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<string | null>(null);
const moderationNote = ref('');
const mandatoryCW = ref<string | null>(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<void> {
const [m, i] = await Promise.all([
(withHint && props.metaHint)
@ -389,6 +410,7 @@ async function fetch(withHint = false): Promise<void> {
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<void> {

View file

@ -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 {

View file

@ -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'];

View file

@ -272,6 +272,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:cw-instance*
*/
request<E extends 'admin/cw-instance', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View file

@ -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 };

View file

@ -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'];

View file

@ -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.

View file

@ -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;

View file

@ -136,6 +136,9 @@ export type ModerationLog = {
} | {
type: 'setMandatoryCWForNote';
info: ModerationLogPayloads['setMandatoryCWForNote'];
} | {
type: 'setMandatoryCWForInstance';
info: ModerationLogPayloads['setMandatoryCWForInstance'];
} | {
type: 'setRemoteInstanceNSFW';
info: ModerationLogPayloads['setRemoteInstanceNSFW'];

View file

@ -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."