View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/876 Closes #905 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
534c35cca2
46 changed files with 843 additions and 108 deletions
|
|
@ -228,7 +228,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async create(user: {
|
||||
public async create(user: MiUser & {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
|
|
@ -435,7 +435,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async import(user: {
|
||||
public async import(user: MiUser & {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
|
|
@ -486,10 +486,10 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
// should really not happen, but better safe than sorry
|
||||
if (data.reply?.id === insert.id) {
|
||||
throw new Error("A note can't reply to itself");
|
||||
throw new Error('A note can\'t reply to itself');
|
||||
}
|
||||
if (data.renote?.id === insert.id) {
|
||||
throw new Error("A note can't renote itself");
|
||||
throw new Error('A note can\'t renote itself');
|
||||
}
|
||||
|
||||
if (data.uri != null) insert.uri = data.uri;
|
||||
|
|
@ -552,7 +552,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async postNoteCreated(note: MiNote, user: {
|
||||
private async postNoteCreated(note: MiNote, user: MiUser & {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
|
|
@ -753,7 +753,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
//#region AP deliver
|
||||
if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
|
||||
(async () => {
|
||||
const noteActivity = await this.renderNoteOrRenoteActivity(data, note);
|
||||
const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
|
||||
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
|
||||
|
||||
// メンションされたリモートユーザーに配送
|
||||
|
|
@ -899,12 +899,12 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) {
|
||||
if (data.localOnly) return null;
|
||||
|
||||
const content = this.isRenote(data) && !this.isQuote(data)
|
||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
||||
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
|
||||
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, user, false), note);
|
||||
|
||||
return this.apRendererService.addContext(content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async edit(user: {
|
||||
public async edit(user: MiUser & {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
|
|
@ -309,7 +309,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
|
||||
if (this.isRenote(data)) {
|
||||
if (data.renote.id === oldnote.id) {
|
||||
throw new Error("A note can't renote itself");
|
||||
throw new Error('A note can\'t renote itself');
|
||||
}
|
||||
|
||||
switch (data.renote.visibility) {
|
||||
|
|
@ -584,7 +584,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async postNoteEdited(note: MiNote, oldNote: MiNote, user: {
|
||||
private async postNoteEdited(note: MiNote, oldNote: MiNote, user: MiUser & {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
|
|
@ -703,7 +703,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
//#region AP deliver
|
||||
if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
|
||||
(async () => {
|
||||
const noteActivity = await this.renderNoteOrRenoteActivity(data, note);
|
||||
const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
|
||||
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
|
||||
|
||||
// メンションされたリモートユーザーに配送
|
||||
|
|
@ -834,14 +834,12 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
|
||||
private async renderNoteOrRenoteActivity(data: Option, note: MiNote, user: MiUser) {
|
||||
if (data.localOnly) return null;
|
||||
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
||||
if (user == null) throw new Error('user not found');
|
||||
|
||||
const content = this.isRenote(data) && !this.isQuote(data)
|
||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
||||
: this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, false), user);
|
||||
: this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, user, false), user);
|
||||
|
||||
return this.apRendererService.addContext(content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export class PollService {
|
|||
if (user == null) throw new Error('note not found');
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, user, false), user));
|
||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
this.relayService.deliverToRelays(user, content);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
|||
signupReason: null,
|
||||
noindex: false,
|
||||
enableRss: true,
|
||||
mandatoryCW: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
|
@ -216,6 +217,7 @@ function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'
|
|||
isSystem: false,
|
||||
isSilenced: user.isSilenced,
|
||||
enableRss: true,
|
||||
mandatoryCW: null,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { appendContentWarning } from '@/misc/append-content-warning.js';
|
||||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import { CONTEXT } from './misc/contexts.js';
|
||||
|
|
@ -339,7 +340,7 @@ export class ApRendererService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async renderNote(note: MiNote, dive = true): Promise<IPost> {
|
||||
public async renderNote(note: MiNote, author: MiUser, dive = true): Promise<IPost> {
|
||||
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
|
|
@ -353,14 +354,14 @@ export class ApRendererService {
|
|||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||
|
||||
if (inReplyToNote != null) {
|
||||
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
|
||||
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
||||
|
||||
if (inReplyToUserExist) {
|
||||
if (inReplyToUser) {
|
||||
if (inReplyToNote.uri) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
} else {
|
||||
if (dive) {
|
||||
inReplyTo = await this.renderNote(inReplyToNote, false);
|
||||
inReplyTo = await this.renderNote(inReplyToNote, inReplyToUser, false);
|
||||
} else {
|
||||
inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`;
|
||||
}
|
||||
|
|
@ -423,7 +424,12 @@ export class ApRendererService {
|
|||
apAppend += `\n\nRE: ${quote}`;
|
||||
}
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
|
||||
// Apply mandatory CW, if applicable
|
||||
if (author.mandatoryCW) {
|
||||
summary = appendContentWarning(summary, author.mandatoryCW);
|
||||
}
|
||||
|
||||
const { content } = this.apMfmService.getNoteHtml(note, apAppend);
|
||||
|
||||
|
|
@ -636,7 +642,7 @@ export class ApRendererService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async renderUpNote(note: MiNote, dive = true): Promise<IPost> {
|
||||
public async renderUpNote(note: MiNote, author: MiUser, dive = true): Promise<IPost> {
|
||||
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
|
|
@ -650,14 +656,14 @@ export class ApRendererService {
|
|||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||
|
||||
if (inReplyToNote != null) {
|
||||
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
|
||||
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
||||
|
||||
if (inReplyToUserExist) {
|
||||
if (inReplyToUser) {
|
||||
if (inReplyToNote.uri) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
} else {
|
||||
if (dive) {
|
||||
inReplyTo = await this.renderUpNote(inReplyToNote, false);
|
||||
inReplyTo = await this.renderUpNote(inReplyToNote, inReplyToUser, false);
|
||||
} else {
|
||||
inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`;
|
||||
}
|
||||
|
|
@ -720,7 +726,12 @@ export class ApRendererService {
|
|||
apAppend += `\n\nRE: ${quote}`;
|
||||
}
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
|
||||
// Apply mandatory CW, if applicable
|
||||
if (author.mandatoryCW) {
|
||||
summary = appendContentWarning(summary, author.mandatoryCW);
|
||||
}
|
||||
|
||||
const { content } = this.apMfmService.getNoteHtml(note, apAppend);
|
||||
|
||||
|
|
|
|||
|
|
@ -209,11 +209,12 @@ export class Resolver {
|
|||
case 'notes':
|
||||
return this.notesRepository.findOneByOrFail({ id: parsed.id })
|
||||
.then(async note => {
|
||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||
if (parsed.rest === 'activity') {
|
||||
// this refers to the create activity and not the note itself
|
||||
return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note), note));
|
||||
return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author), note));
|
||||
} else {
|
||||
return this.apRendererService.renderNote(note);
|
||||
return this.apRendererService.renderNote(note, author);
|
||||
}
|
||||
});
|
||||
case 'users':
|
||||
|
|
|
|||
|
|
@ -592,6 +592,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
isCat: user.isCat,
|
||||
noindex: user.noindex,
|
||||
enableRss: user.enableRss,
|
||||
mandatoryCW: user.mandatoryCW,
|
||||
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||
speakAsCat: user.speakAsCat ?? false,
|
||||
approved: user.approved,
|
||||
|
|
|
|||
62
packages/backend/src/misc/append-content-warning.ts
Normal file
62
packages/backend/src/misc/append-content-warning.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/*
|
||||
* Important Note: this file must be kept in sync with packages/frontend-shared/js/append-content-warning.ts
|
||||
*/
|
||||
|
||||
/**
|
||||
* Appends an additional content warning onto an existing one.
|
||||
* The additional value will not be added if it already exists within the original input.
|
||||
* @param original Existing content warning
|
||||
* @param additional Content warning to append
|
||||
* @param reverse If true, then the additional CW will be prepended instead of appended.
|
||||
*/
|
||||
export function appendContentWarning(original: string | null | undefined, additional: string, reverse = false): string {
|
||||
// Easy case - if original is empty, then additional replaces it.
|
||||
if (!original) {
|
||||
return additional;
|
||||
}
|
||||
|
||||
// Easy case - if the additional CW is empty, then don't append it.
|
||||
if (!additional) {
|
||||
return original;
|
||||
}
|
||||
|
||||
// If the additional CW already exists in the input, then we *don't* append another copy!
|
||||
if (includesWholeWord(original, additional)) {
|
||||
return original;
|
||||
}
|
||||
|
||||
return reverse
|
||||
? `${additional}, ${original}`
|
||||
: `${original}, ${additional}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates a regular expression like /\b(pattern)\b/, but with a raw non-regex pattern.
|
||||
* We're checking to see whether the default CW appears inside the existing CW, but *only* if there's word boundaries on either side.
|
||||
* @param input Input string to search
|
||||
* @param target Target word / phrase to search for
|
||||
*/
|
||||
function includesWholeWord(input: string, target: string): boolean {
|
||||
const parts = input.split(target);
|
||||
|
||||
// The additional string could appear multiple times within the original input.
|
||||
// We need to check each occurrence, since any of them could potentially match.
|
||||
for (let i = 0; i + 1 < parts.length; i++) {
|
||||
const before = parts[i];
|
||||
const after = parts[i + 1];
|
||||
|
||||
// If either the preceding or following tokens are a "word", then this "match" is actually just part of a longer word.
|
||||
// Likewise, if *neither* token is a word, then this is a real match and the CW already exists in the input.
|
||||
if (!/\w$/.test(before) && !/^\w/.test(after)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't match, then there is no existing CW.
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { appendContentWarning } from './append-content-warning.js';
|
||||
import type { Packed } from './json-schema.js';
|
||||
|
||||
/**
|
||||
|
|
@ -20,9 +21,15 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
|||
|
||||
let summary = '';
|
||||
|
||||
// Append mandatory CW, if applicable
|
||||
let cw = note.cw;
|
||||
if (note.user.mandatoryCW) {
|
||||
cw = appendContentWarning(cw, note.user.mandatoryCW);
|
||||
}
|
||||
|
||||
// 本文
|
||||
if (note.cw != null) {
|
||||
summary += `CW: ${note.cw}`;
|
||||
if (cw != null) {
|
||||
summary += `CW: ${cw}`;
|
||||
} else if (note.text) {
|
||||
summary += note.text;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,6 +339,15 @@ export class MiUser {
|
|||
})
|
||||
public enableRss: boolean;
|
||||
|
||||
/**
|
||||
* Specifies a Content Warning that should be forcibly applied to all notes by this user.
|
||||
* If null (default), then no Content Warning is applied.
|
||||
*/
|
||||
@Column('text', {
|
||||
nullable: true,
|
||||
})
|
||||
public mandatoryCW: string | null;
|
||||
|
||||
constructor(data: Partial<MiUser>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ export const packedUserLiteSchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
mandatoryCW: {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
isBot: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
|
|
|
|||
|
|
@ -103,15 +103,16 @@ export class ActivityPubServerService {
|
|||
/**
|
||||
* Pack Create<Note> or Announce Activity
|
||||
* @param note Note
|
||||
* @param author Author of the note
|
||||
*/
|
||||
@bindThis
|
||||
private async packActivity(note: MiNote): Promise<any> {
|
||||
private async packActivity(note: MiNote, author: MiUser): Promise<any> {
|
||||
if (isRenote(note) && !isQuote(note)) {
|
||||
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
||||
}
|
||||
|
||||
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
|
||||
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -506,7 +507,7 @@ export class ActivityPubServerService {
|
|||
this.notesRepository.findOneByOrFail({ id: pining.noteId }))))
|
||||
.filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility));
|
||||
|
||||
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note)));
|
||||
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note, user)));
|
||||
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
`${this.config.url}/users/${userId}/collections/featured`,
|
||||
|
|
@ -579,7 +580,7 @@ export class ActivityPubServerService {
|
|||
|
||||
if (sinceId) notes.reverse();
|
||||
|
||||
const activities = await Promise.all(notes.map(note => this.packActivity(note)));
|
||||
const activities = await Promise.all(notes.map(note => this.packActivity(note, user)));
|
||||
const rendered = this.apRendererService.renderOrderedCollectionPage(
|
||||
`${partOf}?${url.query({
|
||||
page: 'true',
|
||||
|
|
@ -723,7 +724,9 @@ export class ActivityPubServerService {
|
|||
|
||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false));
|
||||
|
||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||
return this.apRendererService.addContext(await this.apRendererService.renderNote(note, author, false));
|
||||
});
|
||||
|
||||
// note activity
|
||||
|
|
@ -746,7 +749,9 @@ export class ActivityPubServerService {
|
|||
|
||||
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(await this.packActivity(note)));
|
||||
|
||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||
return (this.apRendererService.addContext(await this.packActivity(note, author)));
|
||||
});
|
||||
|
||||
// outbox
|
||||
|
|
|
|||
|
|
@ -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-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';
|
||||
export * as 'admin/delete-all-files-of-a-user' from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||
|
|
|
|||
67
packages/backend/src/server/api/endpoints/admin/cw-user.ts
Normal file
67
packages/backend/src/server/api/endpoints/admin/cw-user.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { UsersRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:cw-user',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
cw: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['userId', 'cw'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private readonly usersRepository: UsersRepository,
|
||||
|
||||
private readonly globalEventService: GlobalEventService,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
|
||||
// Skip if there's nothing to do
|
||||
if (user.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, 'setMandatoryCW', {
|
||||
newCW: ps.cw,
|
||||
oldCW: user.mandatoryCW,
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
await this.usersRepository.update(ps.userId, {
|
||||
// Collapse empty strings to null
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
mandatoryCW: ps.cw || null,
|
||||
});
|
||||
|
||||
// Synchronize caches and other processes
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
|
@ -28,10 +29,12 @@ export const paramDef = {
|
|||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
private readonly userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private readonly cacheService: CacheService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
|
|
@ -43,6 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
await this.userProfilesRepository.update(user.id, {
|
||||
alwaysMarkNsfw: true,
|
||||
});
|
||||
|
||||
await this.cacheService.userProfileCache.refresh(ps.userId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export const moderationLogTypes = [
|
|||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
'resetPassword',
|
||||
'setMandatoryCW',
|
||||
'setRemoteInstanceNSFW',
|
||||
'unsetRemoteInstanceNSFW',
|
||||
'suspendRemoteInstance',
|
||||
|
|
@ -261,6 +262,13 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
setMandatoryCW: {
|
||||
newCW: string | null;
|
||||
oldCW: string | null;
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
setRemoteInstanceNSFW: {
|
||||
id: string;
|
||||
host: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue