implement mandatory CW for notes (resolves #910)

This commit is contained in:
Hazelnoot 2025-06-27 22:32:26 -04:00
parent 6f8d831e09
commit 92750240eb
29 changed files with 305 additions and 11 deletions

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

View file

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

View file

@ -46,7 +46,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
});

View file

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