mirror of
https://git.boykissers.com/pawkey/pawkey-sk.git
synced 2025-12-20 12:14:18 +00:00
add "reject quotes" toggle at user and instance level
+ improve, cleanup, and de-duplicate quote resolution + add warning message when quote cannot be loaded + add "process error" framework to display warnings when a note cannot be correctly loaded from another instance
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
export class AddNoteProcessErrors1739671352784 {
|
||||
name = 'AddNoteProcessErrors1739671352784'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "processErrors" text array`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "processErrors"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export class AddUserRejectQuotes1739671777344 {
|
||||
name = 'AddUserRejectQuotes1739671777344'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "rejectQuotes" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "rejectQuotes"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export class AddInstanceRejectQuotes1739671847942 {
|
||||
name = 'AddInstanceRejectQuotes1739671847942'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" ADD "rejectQuotes" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "rejectQuotes"`);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,7 @@ type Option = {
|
||||
uri?: string | null;
|
||||
url?: string | null;
|
||||
app?: MiApp | null;
|
||||
processErrors?: string[] | null;
|
||||
};
|
||||
|
||||
export type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] });
|
||||
@@ -309,6 +310,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
// Check quote permissions
|
||||
await this.checkQuotePermissions(data, user);
|
||||
|
||||
// Check blocking
|
||||
if (this.isRenote(data) && !this.isQuote(data)) {
|
||||
if (data.renote.userHost === null) {
|
||||
@@ -482,6 +486,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
renoteUserId: data.renote ? data.renote.userId : null,
|
||||
renoteUserHost: data.renote ? data.renote.userHost : null,
|
||||
userHost: user.host,
|
||||
processErrors: data.processErrors,
|
||||
});
|
||||
|
||||
// should really not happen, but better safe than sorry
|
||||
@@ -1147,4 +1152,29 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkQuotePermissions(data: Option, user: MiUser): Promise<void> {
|
||||
// Not a quote
|
||||
if (!this.isRenote(data) || !this.isQuote(data)) return;
|
||||
|
||||
// User cannot quote
|
||||
if (user.rejectQuotes) {
|
||||
if (user.host == null) {
|
||||
throw new IdentifiableError('1c0ea108-d1e3-4e8e-aa3f-4d2487626153', 'QUOTE_DISABLED_FOR_USER');
|
||||
} else {
|
||||
(data as Option).renote = null;
|
||||
(data.processErrors ??= []).push('quoteUnavailable');
|
||||
}
|
||||
}
|
||||
|
||||
// Instance cannot quote
|
||||
if (user.host) {
|
||||
const instance = await this.federatedInstanceService.fetch(user.host);
|
||||
if (instance?.rejectQuotes) {
|
||||
(data as Option).renote = null;
|
||||
(data.processErrors ??= []).push('quoteUnavailable');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ type Option = {
|
||||
app?: MiApp | null;
|
||||
updatedAt?: Date | null;
|
||||
editcount?: boolean | null;
|
||||
processErrors?: string[] | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -337,6 +338,9 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
// Check quote permissions
|
||||
await this.noteCreateService.checkQuotePermissions(data, user);
|
||||
|
||||
// Check blocking
|
||||
if (this.isRenote(data) && !this.isQuote(data)) {
|
||||
if (data.renote.userHost === null) {
|
||||
@@ -529,6 +533,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||
|
||||
if (data.uri != null) note.uri = data.uri;
|
||||
if (data.url != null) note.url = data.url;
|
||||
if (data.processErrors !== undefined) note.processErrors = data.processErrors;
|
||||
|
||||
if (mentionedUsers.length > 0) {
|
||||
note.mentions = mentionedUsers.map(u => u.id);
|
||||
|
||||
@@ -296,44 +296,8 @@ export class ApNoteService {
|
||||
: null;
|
||||
|
||||
// 引用
|
||||
let quote: MiNote | undefined | null = null;
|
||||
|
||||
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
||||
const tryResolveNote = async (uri: unknown): Promise<
|
||||
| { status: 'ok'; res: MiNote }
|
||||
| { status: 'permerror' | 'temperror' }
|
||||
> => {
|
||||
if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
|
||||
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
|
||||
return { status: 'permerror' };
|
||||
}
|
||||
try {
|
||||
const res = await this.resolveNote(uri, { resolver });
|
||||
if (res == null) {
|
||||
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
|
||||
return { status: 'permerror' };
|
||||
}
|
||||
return { status: 'ok', res };
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
|
||||
|
||||
return {
|
||||
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null));
|
||||
const results = await Promise.all(uris.map(tryResolveNote));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||
if (!quote) {
|
||||
if (results.some(x => x.status === 'temperror')) {
|
||||
throw new Error(`temporary error resolving quote for ${entryUri}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const quote = await this.getQuote(note, entryUri, resolver);
|
||||
const processErrors = quote === null ? ['quoteUnavailable'] : null;
|
||||
|
||||
// vote
|
||||
if (reply && reply.hasPoll) {
|
||||
@@ -369,7 +333,8 @@ export class ApNoteService {
|
||||
createdAt: note.published ? new Date(note.published) : null,
|
||||
files,
|
||||
reply,
|
||||
renote: quote,
|
||||
renote: quote ?? null,
|
||||
processErrors,
|
||||
name: note.name,
|
||||
cw,
|
||||
text,
|
||||
@@ -538,44 +503,8 @@ export class ApNoteService {
|
||||
: null;
|
||||
|
||||
// 引用
|
||||
let quote: MiNote | undefined | null = null;
|
||||
|
||||
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
||||
const tryResolveNote = async (uri: unknown): Promise<
|
||||
| { status: 'ok'; res: MiNote }
|
||||
| { status: 'permerror' | 'temperror' }
|
||||
> => {
|
||||
if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
|
||||
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
|
||||
return { status: 'permerror' };
|
||||
}
|
||||
try {
|
||||
const res = await this.resolveNote(uri, { resolver });
|
||||
if (res == null) {
|
||||
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
|
||||
return { status: 'permerror' };
|
||||
}
|
||||
return { status: 'ok', res };
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
|
||||
|
||||
return {
|
||||
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null));
|
||||
const results = await Promise.all(uris.map(tryResolveNote));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||
if (!quote) {
|
||||
if (results.some(x => x.status === 'temperror')) {
|
||||
throw new Error(`temporary error resolving quote for ${entryUri}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const quote = await this.getQuote(note, entryUri, resolver);
|
||||
const processErrors = quote === null ? ['quoteUnavailable'] : null;
|
||||
|
||||
// vote
|
||||
if (reply && reply.hasPoll) {
|
||||
@@ -611,7 +540,8 @@ export class ApNoteService {
|
||||
createdAt: note.published ? new Date(note.published) : null,
|
||||
files,
|
||||
reply,
|
||||
renote: quote,
|
||||
renote: quote ?? null,
|
||||
processErrors,
|
||||
name: note.name,
|
||||
cw,
|
||||
text,
|
||||
@@ -734,6 +664,63 @@ export class ApNoteService {
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the note's quoted post.
|
||||
* On success - returns the note.
|
||||
* On skip (no quote) - returns undefined.
|
||||
* On permanent error - returns null.
|
||||
* On temporary error - throws an exception.
|
||||
*/
|
||||
private async getQuote(note: IPost, entryUri: string, resolver: Resolver): Promise<MiNote | null | undefined> {
|
||||
const quoteUris = new Set<string>();
|
||||
if (note._misskey_quote) quoteUris.add(note._misskey_quote);
|
||||
if (note.quoteUrl) quoteUris.add(note.quoteUrl);
|
||||
if (note.quoteUri) quoteUris.add(note.quoteUri);
|
||||
|
||||
// No quote, return undefined
|
||||
if (quoteUris.size < 1) return undefined;
|
||||
|
||||
/**
|
||||
* Attempts to resolve a quote by URI.
|
||||
* Returns the note if successful, true if there's a retryable error, and false if there's a permanent error.
|
||||
*/
|
||||
const resolveQuote = async (uri: unknown): Promise<MiNote | boolean> => {
|
||||
if (typeof(uri) !== 'string' || !/^https?:/.test(uri)) {
|
||||
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": URI is invalid`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const quote = await this.resolveNote(uri, { resolver });
|
||||
|
||||
if (quote == null) {
|
||||
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": request error`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return quote;
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${error}`);
|
||||
|
||||
return (e instanceof StatusError && e.isRetryable);
|
||||
}
|
||||
};
|
||||
|
||||
const results = await Promise.all(Array.from(quoteUris).map(u => resolveQuote(u)));
|
||||
|
||||
// Success - return the quote
|
||||
const quote = results.find(r => typeof(r) === 'object');
|
||||
if (quote) return quote;
|
||||
|
||||
// Temporary / retryable error - throw error
|
||||
const tempError = results.find(r => r === true);
|
||||
if (tempError) throw new Error(`temporary error resolving quote for "${entryUri}"`);
|
||||
|
||||
// Permanent error - return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getBestIcon(note: IObject): IObject | null {
|
||||
|
||||
@@ -60,6 +60,7 @@ export class InstanceEntityService {
|
||||
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
|
||||
isNSFW: instance.isNSFW,
|
||||
rejectReports: instance.rejectReports,
|
||||
rejectQuotes: instance.rejectQuotes,
|
||||
moderationNote: iAmModerator ? instance.moderationNote : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -490,6 +490,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
|
||||
...(opts.detail ? {
|
||||
clippedCount: note.clippedCount,
|
||||
processErrors: note.processErrors,
|
||||
|
||||
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
||||
detail: false,
|
||||
|
||||
@@ -674,6 +674,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||
: false,
|
||||
rejectQuotes: user.rejectQuotes,
|
||||
} : {}),
|
||||
|
||||
...(isDetailed && isMe ? {
|
||||
|
||||
@@ -164,6 +164,15 @@ export class MiInstance {
|
||||
})
|
||||
public rejectReports: boolean;
|
||||
|
||||
/**
|
||||
* If true, quote posts from this instance will be downgraded to normal posts.
|
||||
* The quote will be stripped and a process error will be generated.
|
||||
*/
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public rejectQuotes: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 16384, default: '',
|
||||
})
|
||||
|
||||
@@ -203,6 +203,17 @@ export class MiNote {
|
||||
@JoinColumn()
|
||||
public channel: MiChannel | null;
|
||||
|
||||
/**
|
||||
* List of non-fatal errors encountered while processing (creating or updating) this note.
|
||||
* Entries can be a translation key (which will be queried from the "_processErrors" section) or a raw string.
|
||||
* Errors will be displayed to the user when viewing the note.
|
||||
*/
|
||||
@Column('text', {
|
||||
array: true,
|
||||
nullable: true,
|
||||
})
|
||||
public processErrors: string[] | null;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
||||
@@ -348,6 +348,15 @@ export class MiUser {
|
||||
})
|
||||
public mandatoryCW: string | null;
|
||||
|
||||
/**
|
||||
* If true, quote posts from this user will be downgraded to normal posts.
|
||||
* The quote will be stripped and a process error will be generated.
|
||||
*/
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public rejectQuotes: boolean;
|
||||
|
||||
constructor(data: Partial<MiUser>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
||||
@@ -126,6 +126,11 @@ export const packedFederationInstanceSchema = {
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
rejectQuotes: {
|
||||
type: 'boolean',
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
moderationNote: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
|
||||
@@ -256,6 +256,14 @@ export const packedNoteSchema = {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
processErrors: {
|
||||
type: 'array',
|
||||
optional: true, nullable: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
|
||||
myReaction: {
|
||||
type: 'string',
|
||||
|
||||
@@ -445,6 +445,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
rejectQuotes: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
//#region relations
|
||||
isFollowing: {
|
||||
type: 'boolean',
|
||||
|
||||
@@ -27,6 +27,7 @@ export const paramDef = {
|
||||
isNSFW: { type: 'boolean' },
|
||||
rejectReports: { type: 'boolean' },
|
||||
moderationNote: { type: 'string' },
|
||||
rejectQuotes: { type: 'boolean' },
|
||||
},
|
||||
required: ['host'],
|
||||
} as const;
|
||||
@@ -59,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
suspensionState,
|
||||
isNSFW: ps.isNSFW,
|
||||
rejectReports: ps.rejectReports,
|
||||
rejectQuotes: ps.rejectQuotes,
|
||||
moderationNote: ps.moderationNote,
|
||||
});
|
||||
|
||||
@@ -92,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.rejectQuotes != null && instance.rejectQuotes !== ps.rejectQuotes) {
|
||||
const message = ps.rejectReports ? 'rejectQuotesInstance' : 'acceptQuotesInstance';
|
||||
this.moderationLogService.log(me, message, {
|
||||
id: instance.id,
|
||||
host: instance.host,
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
|
||||
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
|
||||
id: instance.id,
|
||||
|
||||
@@ -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 { UsersRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'write:admin:reject-quotes',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
rejectQuotes: { type: 'boolean', nullable: false },
|
||||
},
|
||||
required: ['userId', 'rejectQuotes'],
|
||||
} 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.rejectQuotes === ps.rejectQuotes) return;
|
||||
|
||||
// Log event first.
|
||||
// This ensures that we don't "lose" the log if an error occurs
|
||||
await this.moderationLogService.log(me, ps.rejectQuotes ? 'rejectQuotesUser' : 'acceptQuotesUser', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
await this.usersRepository.update(ps.userId, {
|
||||
rejectQuotes: ps.rejectQuotes,
|
||||
});
|
||||
|
||||
// Synchronize caches and other processes
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,12 @@ export const meta = {
|
||||
code: 'CONTAINS_TOO_MANY_MENTIONS',
|
||||
id: '4de0363a-3046-481b-9b0f-feff3e211025',
|
||||
},
|
||||
|
||||
quoteDisabledForUser: {
|
||||
message: 'You do not have permission to create quote posts.',
|
||||
code: 'QUOTE_DISABLED_FOR_USER',
|
||||
id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -415,6 +421,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||
} else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
|
||||
throw new ApiError(meta.errors.quoteDisabledForUser);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -176,6 +176,12 @@ export const meta = {
|
||||
id: '33510210-8452-094c-6227-4a6c05d99f02',
|
||||
},
|
||||
|
||||
quoteDisabledForUser: {
|
||||
message: 'You do not have permission to create quote posts.',
|
||||
code: 'QUOTE_DISABLED_FOR_USER',
|
||||
id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
|
||||
},
|
||||
|
||||
containsProhibitedWords: {
|
||||
message: 'Cannot post because it contains prohibited words.',
|
||||
code: 'CONTAINS_PROHIBITED_WORDS',
|
||||
@@ -469,6 +475,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||
} else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
|
||||
throw new ApiError(meta.errors.quoteDisabledForUser);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -132,6 +132,10 @@ export const moderationLogTypes = [
|
||||
'deletePage',
|
||||
'deleteFlash',
|
||||
'deleteGalleryPost',
|
||||
'acceptQuotesUser',
|
||||
'rejectQuotesUser',
|
||||
'acceptQuotesInstance',
|
||||
'rejectQuotesInstance',
|
||||
] as const;
|
||||
|
||||
export type ModerationLogPayloads = {
|
||||
@@ -417,6 +421,24 @@ export type ModerationLogPayloads = {
|
||||
postUserUsername: string;
|
||||
post: any;
|
||||
};
|
||||
acceptQuotesUser: {
|
||||
userId: string,
|
||||
userUsername: string,
|
||||
userHost: string | null,
|
||||
};
|
||||
rejectQuotesUser: {
|
||||
userId: string,
|
||||
userUsername: string,
|
||||
userHost: string | null,
|
||||
};
|
||||
acceptQuotesInstance: {
|
||||
id: string;
|
||||
host: string;
|
||||
};
|
||||
rejectQuotesInstance: {
|
||||
id: string;
|
||||
host: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Serialized<T> = {
|
||||
|
||||
Reference in New Issue
Block a user