implement mandatory CW for notes (resolves #910)
This commit is contained in:
parent
6f8d831e09
commit
92750240eb
29 changed files with 305 additions and 11 deletions
|
|
@ -127,6 +127,7 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
|||
renoteUserInstance: null,
|
||||
updatedAt: null,
|
||||
processErrors: [],
|
||||
mandatoryCW: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
63
packages/backend/src/server/api/endpoints/admin/cw-note.ts
Normal file
63
packages/backend/src/server/api/endpoints/admin/cw-note.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue