mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-04-28 17:46:56 +00:00
add user-level "force content warning" moderation feature
This commit is contained in:
parent
2bf8648ebc
commit
ea89348b62
11 changed files with 534 additions and 407 deletions
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -12089,6 +12089,14 @@ export interface Locale extends ILocale {
|
|||
* ID
|
||||
*/
|
||||
"id": string;
|
||||
/**
|
||||
* Force content warning
|
||||
*/
|
||||
"mandatoryCW": string;
|
||||
/**
|
||||
* Applies a content warning to all posts created by this user. If the post already has a CW, then this is appended to the end.
|
||||
*/
|
||||
"mandatoryCWDescription": string;
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export class AddUserMandatoryCW1738043621143 {
|
||||
name = 'AddUserCW1738043621143'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "mandatoryCW" text`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "mandatoryCW"`);
|
||||
}
|
||||
}
|
|
@ -234,6 +234,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
noindex: MiUser['noindex'];
|
||||
mandatoryCW: MiUser['mandatoryCW'];
|
||||
}, data: Option, silent = false): Promise<MiNote> {
|
||||
// チャンネル外にリプライしたら対象のスコープに合わせる
|
||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
||||
|
@ -368,6 +369,15 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
data.cw = null;
|
||||
}
|
||||
|
||||
// Apply mandatory CW, if applicable
|
||||
if (user.mandatoryCW) {
|
||||
if (data.cw) {
|
||||
data.cw += `, ${user.mandatoryCW}`;
|
||||
} else {
|
||||
data.cw = user.mandatoryCW;
|
||||
}
|
||||
}
|
||||
|
||||
let tags = data.apHashtags;
|
||||
let emojis = data.apEmojis;
|
||||
let mentionedUsers = data.apMentions;
|
||||
|
@ -441,6 +451,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
noindex: MiUser['noindex'];
|
||||
mandatoryCW: MiUser['mandatoryCW'];
|
||||
}, data: Option): Promise<MiNote> {
|
||||
return this.create(user, data, true);
|
||||
}
|
||||
|
|
|
@ -230,6 +230,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
noindex: MiUser['noindex'];
|
||||
mandatoryCW: MiUser['mandatoryCW'];
|
||||
}, editid: MiNote['id'], data: Option, silent = false): Promise<MiNote> {
|
||||
if (!editid) {
|
||||
throw new Error('fail');
|
||||
|
@ -396,6 +397,15 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
data.cw = null;
|
||||
}
|
||||
|
||||
// Apply mandatory CW, if applicable
|
||||
if (user.mandatoryCW) {
|
||||
if (data.cw) {
|
||||
data.cw += `, ${user.mandatoryCW}`;
|
||||
} else {
|
||||
data.cw = user.mandatoryCW;
|
||||
}
|
||||
}
|
||||
|
||||
let tags = data.apHashtags;
|
||||
let emojis = data.apEmojis;
|
||||
let mentionedUsers = data.apMentions;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
53
packages/backend/src/server/api/endpoints/admin/cw-user.ts
Normal file
53
packages/backend/src/server/api/endpoints/admin/cw-user.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
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,
|
||||
) {
|
||||
super(meta, paramDef, async ps => {
|
||||
const result = await this.usersRepository.update(ps.userId, {
|
||||
// Collapse empty strings to null
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
mandatoryCW: ps.cw || null,
|
||||
});
|
||||
|
||||
if (result.affected && result.affected < 1) {
|
||||
throw new Error('No such user');
|
||||
}
|
||||
|
||||
// Synchronize caches and other processes
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
|
||||
});
|
||||
}
|
||||
}
|
|
@ -144,6 +144,10 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
mandatoryCW: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
signins: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
|
@ -260,6 +264,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
isHibernated: user.isHibernated,
|
||||
lastActiveDate: user.lastActiveDate ? user.lastActiveDate.toISOString() : null,
|
||||
moderationNote: profile.moderationNote ?? '',
|
||||
mandatoryCW: user.mandatoryCW,
|
||||
signins,
|
||||
policies: await this.roleService.getUserPolicies(user.id),
|
||||
roles: await this.roleEntityService.packMany(roles, me),
|
||||
|
|
|
@ -83,6 +83,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
|
||||
|
||||
<MkInput v-model="mandatoryCW" type="text" manualSave>
|
||||
<template #label>{{ i18n.ts.mandatoryCW }}</template>
|
||||
<template #caption>{{ i18n.ts.mandatoryCWDescription }}</template>
|
||||
</MkInput>
|
||||
|
||||
<div>
|
||||
<MkButton v-if="user.host == null && !isSystem" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
</div>
|
||||
|
@ -222,6 +227,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { iAmAdmin, $i, iAmModerator } from '@/account.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
userId: string;
|
||||
|
@ -243,6 +249,7 @@ const approved = ref(false);
|
|||
const suspended = ref(false);
|
||||
const markedAsNSFW = ref(false);
|
||||
const moderationNote = ref('');
|
||||
const mandatoryCW = ref<string | null>(null);
|
||||
const isSystem = computed(() => info.value?.isSystem ?? false);
|
||||
const filesPagination = {
|
||||
endpoint: 'admin/drive/files' as const,
|
||||
|
@ -281,6 +288,15 @@ function createFetcher() {
|
|||
markedAsNSFW.value = info.value.alwaysMarkNsfw;
|
||||
suspended.value = info.value.isSuspended;
|
||||
moderationNote.value = info.value.moderationNote;
|
||||
mandatoryCW.value = info.value.mandatoryCW;
|
||||
|
||||
// These watch statements work because they're lazy-initialized.
|
||||
// The watched values are already set, so they don't trigger any "change" just from refreshing the user.
|
||||
|
||||
watch(mandatoryCW, async () => {
|
||||
await os.apiWithDialog('admin/cw-user', { userId: props.userId, cw: mandatoryCW.value });
|
||||
refreshUser();
|
||||
});
|
||||
|
||||
watch(moderationNote, async () => {
|
||||
await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -83,6 +83,7 @@ export const permissions = [
|
|||
'write:admin:decline-user',
|
||||
'write:admin:nsfw-user',
|
||||
'write:admin:unnsfw-user',
|
||||
'write:admin:cw-user',
|
||||
'write:admin:silence-user',
|
||||
'write:admin:unsilence-user',
|
||||
'write:admin:unset-user-avatar',
|
||||
|
|
|
@ -471,3 +471,6 @@ _noteSearch:
|
|||
flash: "Flash"
|
||||
|
||||
id: "ID"
|
||||
|
||||
mandatoryCW: "Force content warning"
|
||||
mandatoryCWDescription: "Applies a content warning to all posts created by this user. If the post already has a CW, then this is appended to the end."
|
||||
|
|
Loading…
Add table
Reference in a new issue