mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-04-28 09:36:56 +00:00
merge: Add "reject quotes" settings (!901)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/901 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
14a81b4f85
49 changed files with 693 additions and 100 deletions
38
locales/index.d.ts
vendored
38
locales/index.d.ts
vendored
|
@ -8566,6 +8566,10 @@ export interface Locale extends ILocale {
|
||||||
* Un-silence users
|
* Un-silence users
|
||||||
*/
|
*/
|
||||||
"write:admin:unsilence-user": string;
|
"write:admin:unsilence-user": string;
|
||||||
|
/**
|
||||||
|
* Allow/Prohibit quote posts from a user
|
||||||
|
*/
|
||||||
|
"write:admin:reject-quotes": string;
|
||||||
/**
|
/**
|
||||||
* View your list of scheduled notes
|
* View your list of scheduled notes
|
||||||
*/
|
*/
|
||||||
|
@ -10242,6 +10246,14 @@ export interface Locale extends ILocale {
|
||||||
* Accepted reports from remote instance
|
* Accepted reports from remote instance
|
||||||
*/
|
*/
|
||||||
"acceptRemoteInstanceReports": string;
|
"acceptRemoteInstanceReports": string;
|
||||||
|
/**
|
||||||
|
* Blocked/Stripped quote posts from user
|
||||||
|
*/
|
||||||
|
"rejectQuotesUser": string;
|
||||||
|
/**
|
||||||
|
* Allowed quote posts from user
|
||||||
|
*/
|
||||||
|
"allowQuotesUser": string;
|
||||||
};
|
};
|
||||||
"_fileViewer": {
|
"_fileViewer": {
|
||||||
/**
|
/**
|
||||||
|
@ -11240,6 +11252,26 @@ export interface Locale extends ILocale {
|
||||||
* Reject reports from this instance
|
* Reject reports from this instance
|
||||||
*/
|
*/
|
||||||
"rejectReports": string;
|
"rejectReports": string;
|
||||||
|
/**
|
||||||
|
* Strip quote posts from this instance
|
||||||
|
*/
|
||||||
|
"rejectQuotesInstance": string;
|
||||||
|
/**
|
||||||
|
* Strip quote posts from this user
|
||||||
|
*/
|
||||||
|
"rejectQuotesRemoteUser": string;
|
||||||
|
/**
|
||||||
|
* Block quote posts from this user
|
||||||
|
*/
|
||||||
|
"rejectQuotesLocalUser": string;
|
||||||
|
/**
|
||||||
|
* Are you sure you wish to strip quote posts?
|
||||||
|
*/
|
||||||
|
"rejectQuotesConfirm": string;
|
||||||
|
/**
|
||||||
|
* Are you sure you wish to allow quote posts?
|
||||||
|
*/
|
||||||
|
"allowQuotesConfirm": string;
|
||||||
/**
|
/**
|
||||||
* This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s).
|
* This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s).
|
||||||
*/
|
*/
|
||||||
|
@ -12109,6 +12141,12 @@ export interface Locale extends ILocale {
|
||||||
* 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.
|
* 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;
|
"mandatoryCWDescription": string;
|
||||||
|
"_processErrors": {
|
||||||
|
/**
|
||||||
|
* Unable to process quote. This post may be missing context.
|
||||||
|
*/
|
||||||
|
"quoteUnavailable": string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -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;
|
uri?: string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
app?: MiApp | null;
|
app?: MiApp | null;
|
||||||
|
processErrors?: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: 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
|
// Check blocking
|
||||||
if (this.isRenote(data) && !this.isQuote(data)) {
|
if (this.isRenote(data) && !this.isQuote(data)) {
|
||||||
if (data.renote.userHost === null) {
|
if (data.renote.userHost === null) {
|
||||||
|
@ -482,6 +486,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
renoteUserId: data.renote ? data.renote.userId : null,
|
renoteUserId: data.renote ? data.renote.userId : null,
|
||||||
renoteUserHost: data.renote ? data.renote.userHost : null,
|
renoteUserHost: data.renote ? data.renote.userHost : null,
|
||||||
userHost: user.host,
|
userHost: user.host,
|
||||||
|
processErrors: data.processErrors,
|
||||||
});
|
});
|
||||||
|
|
||||||
// should really not happen, but better safe than sorry
|
// 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> {
|
public async onApplicationShutdown(signal?: string | undefined): Promise<void> {
|
||||||
await this.dispose();
|
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;
|
app?: MiApp | null;
|
||||||
updatedAt?: Date | null;
|
updatedAt?: Date | null;
|
||||||
editcount?: boolean | null;
|
editcount?: boolean | null;
|
||||||
|
processErrors?: string[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -337,6 +338,9 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check quote permissions
|
||||||
|
await this.noteCreateService.checkQuotePermissions(data, user);
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (this.isRenote(data) && !this.isQuote(data)) {
|
if (this.isRenote(data) && !this.isQuote(data)) {
|
||||||
if (data.renote.userHost === null) {
|
if (data.renote.userHost === null) {
|
||||||
|
@ -529,6 +533,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
if (data.uri != null) note.uri = data.uri;
|
if (data.uri != null) note.uri = data.uri;
|
||||||
if (data.url != null) note.url = data.url;
|
if (data.url != null) note.url = data.url;
|
||||||
|
if (data.processErrors !== undefined) note.processErrors = data.processErrors;
|
||||||
|
|
||||||
if (mentionedUsers.length > 0) {
|
if (mentionedUsers.length > 0) {
|
||||||
note.mentions = mentionedUsers.map(u => u.id);
|
note.mentions = mentionedUsers.map(u => u.id);
|
||||||
|
|
|
@ -100,6 +100,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||||
noindex: false,
|
noindex: false,
|
||||||
enableRss: true,
|
enableRss: true,
|
||||||
mandatoryCW: null,
|
mandatoryCW: null,
|
||||||
|
rejectQuotes: false,
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -143,6 +144,7 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
||||||
renoteUserId: null,
|
renoteUserId: null,
|
||||||
renoteUserHost: null,
|
renoteUserHost: null,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
|
processErrors: [],
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
|
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
|
@ -296,44 +297,8 @@ export class ApNoteService {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
let quote: MiNote | undefined | null = null;
|
const quote = await this.getQuote(note, entryUri, resolver);
|
||||||
|
const processErrors = quote === null ? ['quoteUnavailable'] : 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// vote
|
// vote
|
||||||
if (reply && reply.hasPoll) {
|
if (reply && reply.hasPoll) {
|
||||||
|
@ -369,7 +334,8 @@ export class ApNoteService {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
renote: quote,
|
renote: quote ?? null,
|
||||||
|
processErrors,
|
||||||
name: note.name,
|
name: note.name,
|
||||||
cw,
|
cw,
|
||||||
text,
|
text,
|
||||||
|
@ -538,44 +504,8 @@ export class ApNoteService {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
let quote: MiNote | undefined | null = null;
|
const quote = await this.getQuote(note, entryUri, resolver);
|
||||||
|
const processErrors = quote === null ? ['quoteUnavailable'] : 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// vote
|
// vote
|
||||||
if (reply && reply.hasPoll) {
|
if (reply && reply.hasPoll) {
|
||||||
|
@ -611,7 +541,8 @@ export class ApNoteService {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
renote: quote,
|
renote: quote ?? null,
|
||||||
|
processErrors,
|
||||||
name: note.name,
|
name: note.name,
|
||||||
cw,
|
cw,
|
||||||
text,
|
text,
|
||||||
|
@ -734,6 +665,66 @@ 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) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}":`, e);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isRetryableError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
function getBestIcon(note: IObject): IObject | null {
|
||||||
|
|
|
@ -60,6 +60,7 @@ export class InstanceEntityService {
|
||||||
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
|
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
|
||||||
isNSFW: instance.isNSFW,
|
isNSFW: instance.isNSFW,
|
||||||
rejectReports: instance.rejectReports,
|
rejectReports: instance.rejectReports,
|
||||||
|
rejectQuotes: instance.rejectQuotes,
|
||||||
moderationNote: iAmModerator ? instance.moderationNote : null,
|
moderationNote: iAmModerator ? instance.moderationNote : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -490,6 +490,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
clippedCount: note.clippedCount,
|
clippedCount: note.clippedCount,
|
||||||
|
processErrors: note.processErrors,
|
||||||
|
|
||||||
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
||||||
detail: false,
|
detail: false,
|
||||||
|
|
|
@ -593,6 +593,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
noindex: user.noindex,
|
noindex: user.noindex,
|
||||||
enableRss: user.enableRss,
|
enableRss: user.enableRss,
|
||||||
mandatoryCW: user.mandatoryCW,
|
mandatoryCW: user.mandatoryCW,
|
||||||
|
rejectQuotes: user.rejectQuotes,
|
||||||
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||||
speakAsCat: user.speakAsCat ?? false,
|
speakAsCat: user.speakAsCat ?? false,
|
||||||
approved: user.approved,
|
approved: user.approved,
|
||||||
|
|
22
packages/backend/src/misc/is-retryable-error.ts
Normal file
22
packages/backend/src/misc/is-retryable-error.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AbortError } from 'node-fetch';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns false if the provided value represents a "permanent" error that cannot be retried.
|
||||||
|
* Returns true if the error is retryable, unknown (as all errors are retryable by default), or not an error object.
|
||||||
|
*/
|
||||||
|
export function isRetryableError(e: unknown): boolean {
|
||||||
|
if (e instanceof StatusError) return e.isRetryable;
|
||||||
|
if (e instanceof IdentifiableError) return e.isRetryable;
|
||||||
|
if (e instanceof UnrecoverableError) return false;
|
||||||
|
if (e instanceof AbortError) return true;
|
||||||
|
if (e instanceof Error) return e.name === 'AbortError';
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -164,6 +164,15 @@ export class MiInstance {
|
||||||
})
|
})
|
||||||
public rejectReports: boolean;
|
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', {
|
@Column('varchar', {
|
||||||
length: 16384, default: '',
|
length: 16384, default: '',
|
||||||
})
|
})
|
||||||
|
|
|
@ -203,6 +203,17 @@ export class MiNote {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public channel: MiChannel | null;
|
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
|
//#region Denormalized fields
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
|
|
|
@ -348,6 +348,15 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public mandatoryCW: string | null;
|
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>) {
|
constructor(data: Partial<MiUser>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -126,6 +126,11 @@ export const packedFederationInstanceSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
rejectQuotes: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
moderationNote: {
|
moderationNote: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
|
|
@ -17,6 +17,11 @@ export const packedNoteSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
deletedAt: {
|
deletedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
@ -256,6 +261,14 @@ export const packedNoteSchema = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
processErrors: {
|
||||||
|
type: 'array',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
myReaction: {
|
myReaction: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
@ -138,6 +138,10 @@ export const packedUserLiteSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
|
rejectQuotes: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
isBot: {
|
isBot: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
|
|
|
@ -74,6 +74,7 @@ export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-
|
||||||
export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
|
export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
|
||||||
export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
|
export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
|
||||||
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
|
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
|
||||||
|
export * as 'admin/reject-quotes' from './endpoints/admin/reject-quotes.js';
|
||||||
export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
|
export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
|
||||||
export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
|
export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
|
||||||
export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
|
export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const paramDef = {
|
||||||
isNSFW: { type: 'boolean' },
|
isNSFW: { type: 'boolean' },
|
||||||
rejectReports: { type: 'boolean' },
|
rejectReports: { type: 'boolean' },
|
||||||
moderationNote: { type: 'string' },
|
moderationNote: { type: 'string' },
|
||||||
|
rejectQuotes: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['host'],
|
required: ['host'],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -59,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
suspensionState,
|
suspensionState,
|
||||||
isNSFW: ps.isNSFW,
|
isNSFW: ps.isNSFW,
|
||||||
rejectReports: ps.rejectReports,
|
rejectReports: ps.rejectReports,
|
||||||
|
rejectQuotes: ps.rejectQuotes,
|
||||||
moderationNote: ps.moderationNote,
|
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) {
|
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
|
||||||
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
|
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
|
||||||
id: instance.id,
|
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',
|
code: 'CONTAINS_TOO_MANY_MENTIONS',
|
||||||
id: '4de0363a-3046-481b-9b0f-feff3e211025',
|
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;
|
} as const;
|
||||||
|
|
||||||
|
@ -415,6 +421,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||||
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||||
|
} else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
|
||||||
|
throw new ApiError(meta.errors.quoteDisabledForUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -176,6 +176,12 @@ export const meta = {
|
||||||
id: '33510210-8452-094c-6227-4a6c05d99f02',
|
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: {
|
containsProhibitedWords: {
|
||||||
message: 'Cannot post because it contains prohibited words.',
|
message: 'Cannot post because it contains prohibited words.',
|
||||||
code: '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);
|
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||||
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
|
||||||
throw new ApiError(meta.errors.containsTooManyMentions);
|
throw new ApiError(meta.errors.containsTooManyMentions);
|
||||||
|
} else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
|
||||||
|
throw new ApiError(meta.errors.quoteDisabledForUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -132,6 +132,10 @@ export const moderationLogTypes = [
|
||||||
'deletePage',
|
'deletePage',
|
||||||
'deleteFlash',
|
'deleteFlash',
|
||||||
'deleteGalleryPost',
|
'deleteGalleryPost',
|
||||||
|
'acceptQuotesUser',
|
||||||
|
'rejectQuotesUser',
|
||||||
|
'acceptQuotesInstance',
|
||||||
|
'rejectQuotesInstance',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -417,6 +421,24 @@ export type ModerationLogPayloads = {
|
||||||
postUserUsername: string;
|
postUserUsername: string;
|
||||||
post: any;
|
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> = {
|
export type Serialized<T> = {
|
||||||
|
|
|
@ -61,6 +61,7 @@ describe('NoteCreateService', () => {
|
||||||
replyUserHost: null,
|
replyUserHost: null,
|
||||||
renoteUserId: null,
|
renoteUserId: null,
|
||||||
renoteUserHost: null,
|
renoteUserHost: null,
|
||||||
|
processErrors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const poll: IPoll = {
|
const poll: IPoll = {
|
||||||
|
|
|
@ -44,6 +44,7 @@ const base: MiNote = {
|
||||||
replyUserHost: null,
|
replyUserHost: null,
|
||||||
renoteUserId: null,
|
renoteUserId: null,
|
||||||
renoteUserHost: null,
|
renoteUserHost: null,
|
||||||
|
processErrors: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('misc:is-renote', () => {
|
describe('misc:is-renote', () => {
|
||||||
|
|
73
packages/backend/test/unit/misc/is-retryable-error.ts
Normal file
73
packages/backend/test/unit/misc/is-retryable-error.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
|
import { AbortError } from 'node-fetch';
|
||||||
|
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||||
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
|
||||||
|
describe(isRetryableError, () => {
|
||||||
|
it('should return true for retryable StatusError', () => {
|
||||||
|
const error = new StatusError('test error', 500);
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for permanent StatusError', () => {
|
||||||
|
const error = new StatusError('test error', 400);
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for retryable IdentifiableError', () => {
|
||||||
|
const error = new IdentifiableError('id', 'message', true);
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for permanent StatusError', () => {
|
||||||
|
const error = new IdentifiableError('id', 'message', false);
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for UnrecoverableError', () => {
|
||||||
|
const error = new UnrecoverableError();
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for typed AbortError', () => {
|
||||||
|
const error = new AbortError();
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for named AbortError', () => {
|
||||||
|
const error = new Error();
|
||||||
|
error.name = 'AbortError';
|
||||||
|
|
||||||
|
const result = isRetryableError(error);
|
||||||
|
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const nonErrorInputs = [
|
||||||
|
[null, 'null'],
|
||||||
|
[undefined, 'undefined'],
|
||||||
|
[0, 'number'],
|
||||||
|
['string', 'string'],
|
||||||
|
[true, 'boolean'],
|
||||||
|
[[], 'array'],
|
||||||
|
[{}, 'object'],
|
||||||
|
];
|
||||||
|
for (const [input, label] of nonErrorInputs) {
|
||||||
|
it(`should return true for ${label} input`, () => {
|
||||||
|
const result = isRetryableError(input);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||||
|
@ -142,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote && !props.mock"
|
v-if="canRenote && !props.mock && !$i?.rejectQuotes"
|
||||||
ref="quoteButton"
|
ref="quoteButton"
|
||||||
:class="$style.footerButton"
|
:class="$style.footerButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
|
|
|
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -153,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote"
|
v-if="canRenote && !$i?.rejectQuotes"
|
||||||
ref="quoteButton"
|
ref="quoteButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="$style.noteFooterButton"
|
:class="$style.noteFooterButton"
|
||||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote"
|
v-if="canRenote && !$i?.rejectQuotes"
|
||||||
ref="quoteButton"
|
ref="quoteButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="$style.noteFooterButton"
|
:class="$style.noteFooterButton"
|
||||||
|
|
|
@ -640,7 +640,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
|
|
||||||
const paste = ev.clipboardData.getData('text');
|
const paste = ev.clipboardData.getData('text');
|
||||||
|
|
||||||
if (!renoteTargetNote.value && !quoteId.value && paste.startsWith(url + '/notes/')) {
|
if (!renoteTargetNote.value && !quoteId.value && paste.startsWith(url + '/notes/') && !$i.rejectQuotes) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
os.confirm({
|
os.confirm({
|
||||||
|
|
|
@ -87,20 +87,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { defineAsyncComponent, onDeactivated, onUnmounted, ref, watch } from 'vue';
|
import { defineAsyncComponent, onDeactivated, onUnmounted, ref, watch } from 'vue';
|
||||||
import { url as local } from '@@/js/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
import { versatileLang } from '@@/js/intl-const.js';
|
import { versatileLang } from '@@/js/intl-const.js';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import type { summaly } from '@misskey-dev/summaly';
|
import type { summaly } from '@misskey-dev/summaly';
|
||||||
|
import type MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
|
import type SkNoteSimple from '@/components/SkNoteSimple.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
const XNoteSimple = defineAsyncComponent(() =>
|
const XNoteSimple = defineAsyncComponent<typeof MkNoteSimple | typeof SkNoteSimple>(() =>
|
||||||
(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteSimple.vue') :
|
defaultStore.state.noteDesign === 'misskey'
|
||||||
(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteSimple.vue') :
|
? import('@/components/MkNoteSimple.vue')
|
||||||
null
|
: import('@/components/SkNoteSimple.vue'),
|
||||||
);
|
);
|
||||||
|
|
||||||
type SummalyResult = Awaited<ReturnType<typeof summaly>>;
|
type SummalyResult = Awaited<ReturnType<typeof summaly>>;
|
||||||
|
@ -111,12 +113,13 @@ const props = withDefaults(defineProps<{
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
showAsQuote?: boolean;
|
showAsQuote?: boolean;
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
skipNoteIds?: string[];
|
skipNoteIds?: (string | undefined)[];
|
||||||
}>(), {
|
}>(), {
|
||||||
detail: false,
|
detail: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
showAsQuote: false,
|
showAsQuote: false,
|
||||||
showActions: true,
|
showActions: true,
|
||||||
|
skipNoteIds: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const MOBILE_THRESHOLD = 500;
|
const MOBILE_THRESHOLD = 500;
|
||||||
|
|
43
packages/frontend/src/components/SkErrorList.vue
Normal file
43
packages/frontend/src/components/SkErrorList.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Match appearance of MkRemoteCaution.vue -->
|
||||||
|
<div v-for="error of displayErrors" :key="error" :class="$style.root">
|
||||||
|
<i :class="$style.icon" class="ti ti-alert-triangle"></i>{{ i18n.ts._processErrors[error] ?? error }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
errors?: string[] | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const displayErrors = computed<Iterable<string>>(() => {
|
||||||
|
if (!props.errors?.length) return [];
|
||||||
|
|
||||||
|
// Set constructor preserve order, so we can sort first to avoid a copy operation.
|
||||||
|
return new Set(props.errors.toSorted());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.root {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 16px;
|
||||||
|
background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent);
|
||||||
|
color: var(--MI_THEME-infoWarnFg);
|
||||||
|
border-radius: var(--MI-radius);
|
||||||
|
overflow: clip;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -104,7 +104,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||||
|
@ -143,7 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote && !props.mock"
|
v-if="canRenote && !props.mock && !$i?.rejectQuotes"
|
||||||
ref="quoteButton"
|
ref="quoteButton"
|
||||||
:class="$style.footerButton"
|
:class="$style.footerButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
|
|
|
@ -122,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="!appearNote.user.rejectQuotes" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,7 +158,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote"
|
v-if="canRenote && !$i?.rejectQuotes"
|
||||||
ref="quoteButton"
|
ref="quoteButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="$style.noteFooterButton"
|
:class="$style.noteFooterButton"
|
||||||
|
|
|
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canRenote"
|
v-if="canRenote && !$i?.rejectQuotes"
|
||||||
ref="quoteButton"
|
ref="quoteButton"
|
||||||
class="_button"
|
class="_button"
|
||||||
:class="$style.noteFooterButton"
|
:class="$style.noteFooterButton"
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps" :class="$style.textRoot">
|
<div class="_gaps" :class="$style.textRoot">
|
||||||
<Mfm :text="block.text ?? ''" :isBlock="true" :isNote="false"/>
|
<Mfm :text="block.text ?? ''" :isBlock="true" :isNote="false"/>
|
||||||
<div v-if="isEnabledUrlPreview" class="_gaps_s">
|
<div v-if="isEnabledUrlPreview" class="_gaps_s">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :showAsQuote="!page.user.rejectQuotes"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -81,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
|
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
|
||||||
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="rejectQuotes" @update:modelValue="toggleRejectQuotes">{{ user.host == null ? i18n.ts.rejectQuotesLocalUser : i18n.ts.rejectQuotesRemoteUser }}</MkSwitch>
|
||||||
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
|
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
|
||||||
|
|
||||||
<MkInput v-model="mandatoryCW" type="text" manualSave @update:modelValue="onMandatoryCWChanged">
|
<MkInput v-model="mandatoryCW" type="text" manualSave @update:modelValue="onMandatoryCWChanged">
|
||||||
|
@ -247,6 +248,7 @@ const moderator = ref(false);
|
||||||
const silenced = ref(false);
|
const silenced = ref(false);
|
||||||
const approved = ref(false);
|
const approved = ref(false);
|
||||||
const suspended = ref(false);
|
const suspended = ref(false);
|
||||||
|
const rejectQuotes = ref(false);
|
||||||
const markedAsNSFW = ref(false);
|
const markedAsNSFW = ref(false);
|
||||||
const moderationNote = ref('');
|
const moderationNote = ref('');
|
||||||
const mandatoryCW = ref<string | null>(null);
|
const mandatoryCW = ref<string | null>(null);
|
||||||
|
@ -287,6 +289,7 @@ function createFetcher() {
|
||||||
approved.value = info.value.approved;
|
approved.value = info.value.approved;
|
||||||
markedAsNSFW.value = info.value.alwaysMarkNsfw;
|
markedAsNSFW.value = info.value.alwaysMarkNsfw;
|
||||||
suspended.value = info.value.isSuspended;
|
suspended.value = info.value.isSuspended;
|
||||||
|
rejectQuotes.value = user.value.rejectQuotes ?? false;
|
||||||
moderationNote.value = info.value.moderationNote;
|
moderationNote.value = info.value.moderationNote;
|
||||||
mandatoryCW.value = user.value.mandatoryCW;
|
mandatoryCW.value = user.value.mandatoryCW;
|
||||||
});
|
});
|
||||||
|
@ -368,6 +371,22 @@ async function toggleSuspend(v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleRejectQuotes(v: boolean): Promise<void> {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: v ? i18n.ts.rejectQuotesConfirm : i18n.ts.allowQuotesConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) {
|
||||||
|
rejectQuotes.value = !v;
|
||||||
|
} else {
|
||||||
|
await misskeyApi('admin/reject-quotes', {
|
||||||
|
userId: props.userId,
|
||||||
|
rejectQuotes: v,
|
||||||
|
});
|
||||||
|
await refreshUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function unsetUserAvatar() {
|
async function unsetUserAvatar() {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -28,6 +28,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
'unsetRemoteInstanceNSFW',
|
'unsetRemoteInstanceNSFW',
|
||||||
'rejectRemoteInstanceReports',
|
'rejectRemoteInstanceReports',
|
||||||
'acceptRemoteInstanceReports',
|
'acceptRemoteInstanceReports',
|
||||||
|
'rejectQuotesUser',
|
||||||
|
'acceptQuotesUser',
|
||||||
].includes(log.type),
|
].includes(log.type),
|
||||||
[$style.logRed]: [
|
[$style.logRed]: [
|
||||||
'suspend',
|
'suspend',
|
||||||
|
@ -53,6 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
<span v-else-if="log.type === 'rejectQuotesUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
<span v-else-if="log.type === 'acceptQuotesUser'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
<span v-else-if="log.type === 'rejectQuotesInstance'">: {{ log.info.host }}</span>
|
||||||
|
<span v-else-if="log.type === 'acceptQuotesInstance'">: {{ log.info.host }}</span>
|
||||||
<span v-else-if="log.type === 'decline'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'decline'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
<span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||||
|
@ -134,6 +140,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-else-if="log.type === 'unsuspend'">
|
<template v-else-if="log.type === 'unsuspend'">
|
||||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="log.type === 'rejectQuotesUser'">
|
||||||
|
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="log.type === 'acceptQuotesUser'">
|
||||||
|
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||||
|
</template>
|
||||||
<template v-else-if="log.type === 'updateRole'">
|
<template v-else-if="log.type === 'updateRole'">
|
||||||
<div :class="$style.diff">
|
<div :class="$style.diff">
|
||||||
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
|
||||||
|
|
|
@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
|
<MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
|
||||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch>
|
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="rejectQuotes" :disabled="!instance" @update:modelValue="toggleRejectQuotes">{{ i18n.ts.rejectQuotesInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch>
|
<MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch>
|
||||||
<MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
|
<MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
|
||||||
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
|
||||||
|
@ -211,6 +212,7 @@ const isSuspended = ref(false);
|
||||||
const isBlocked = ref(false);
|
const isBlocked = ref(false);
|
||||||
const isSilenced = ref(false);
|
const isSilenced = ref(false);
|
||||||
const isNSFW = ref(false);
|
const isNSFW = ref(false);
|
||||||
|
const rejectQuotes = ref(false);
|
||||||
const rejectReports = ref(false);
|
const rejectReports = ref(false);
|
||||||
const isMediaSilenced = ref(false);
|
const isMediaSilenced = ref(false);
|
||||||
const faviconUrl = ref<string | null>(null);
|
const faviconUrl = ref<string | null>(null);
|
||||||
|
@ -282,6 +284,7 @@ async function fetch(): Promise<void> {
|
||||||
isSilenced.value = instance.value?.isSilenced ?? false;
|
isSilenced.value = instance.value?.isSilenced ?? false;
|
||||||
isNSFW.value = instance.value?.isNSFW ?? false;
|
isNSFW.value = instance.value?.isNSFW ?? false;
|
||||||
rejectReports.value = instance.value?.rejectReports ?? false;
|
rejectReports.value = instance.value?.rejectReports ?? false;
|
||||||
|
rejectQuotes.value = instance.value?.rejectQuotes ?? false;
|
||||||
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
|
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
|
||||||
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
||||||
moderationNote.value = instance.value?.moderationNote ?? '';
|
moderationNote.value = instance.value?.moderationNote ?? '';
|
||||||
|
@ -347,6 +350,15 @@ async function toggleRejectReports(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleRejectQuotes(): Promise<void> {
|
||||||
|
if (!iAmModerator) return;
|
||||||
|
if (!instance.value) throw new Error('No instance?');
|
||||||
|
await misskeyApi('admin/federation/update-instance', {
|
||||||
|
host: instance.value.host,
|
||||||
|
rejectQuotes: rejectQuotes.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function refreshMetadata(): void {
|
function refreshMetadata(): void {
|
||||||
if (!iAmModerator) return;
|
if (!iAmModerator) return;
|
||||||
if (!instance.value) throw new Error('No instance?');
|
if (!instance.value) throw new Error('No instance?');
|
||||||
|
|
|
@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div class="_margin _gaps_s">
|
<div class="_margin _gaps_s">
|
||||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||||
|
<SkErrorList :errors="note.processErrors"/>
|
||||||
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
|
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="clips && clips.length > 0" class="_margin">
|
<div v-if="clips && clips.length > 0" class="_margin">
|
||||||
|
@ -60,6 +61,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||||
|
import SkErrorList from '@/components/SkErrorList.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
import { serverContext, assertServerContext } from '@/server-context.js';
|
import { serverContext, assertServerContext } from '@/server-context.js';
|
||||||
|
@ -69,9 +71,9 @@ import { $i } from '@/account.js';
|
||||||
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
|
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
|
||||||
|
|
||||||
const MkNoteDetailed = defineAsyncComponent(() =>
|
const MkNoteDetailed = defineAsyncComponent(() =>
|
||||||
(defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') :
|
(defaultStore.state.noteDesign === 'misskey')
|
||||||
(defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteDetailed.vue') :
|
? import('@/components/MkNoteDetailed.vue')
|
||||||
null
|
: import('@/components/SkNoteDetailed.vue'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
@ -281,6 +281,9 @@ type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBo
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
|
type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminRejectQuotesRequest = operations['admin___reject-quotes']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
|
type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -1355,6 +1358,7 @@ declare namespace entities {
|
||||||
AdminQueueInboxDelayedResponse,
|
AdminQueueInboxDelayedResponse,
|
||||||
AdminQueuePromoteRequest,
|
AdminQueuePromoteRequest,
|
||||||
AdminQueueStatsResponse,
|
AdminQueueStatsResponse,
|
||||||
|
AdminRejectQuotesRequest,
|
||||||
AdminRelaysAddRequest,
|
AdminRelaysAddRequest,
|
||||||
AdminRelaysAddResponse,
|
AdminRelaysAddResponse,
|
||||||
AdminRelaysListResponse,
|
AdminRelaysListResponse,
|
||||||
|
@ -2556,6 +2560,18 @@ type ModerationLog = {
|
||||||
} | {
|
} | {
|
||||||
type: 'unsuspend';
|
type: 'unsuspend';
|
||||||
info: ModerationLogPayloads['unsuspend'];
|
info: ModerationLogPayloads['unsuspend'];
|
||||||
|
} | {
|
||||||
|
type: 'acceptQuotesUser';
|
||||||
|
info: ModerationLogPayloads['acceptQuotesUser'];
|
||||||
|
} | {
|
||||||
|
type: 'rejectQuotesUser';
|
||||||
|
info: ModerationLogPayloads['rejectQuotesUser'];
|
||||||
|
} | {
|
||||||
|
type: 'acceptQuotesInstance';
|
||||||
|
info: ModerationLogPayloads['acceptQuotesInstance'];
|
||||||
|
} | {
|
||||||
|
type: 'rejectQuotesInstance';
|
||||||
|
info: ModerationLogPayloads['rejectQuotesInstance'];
|
||||||
} | {
|
} | {
|
||||||
type: 'updateUserNote';
|
type: 'updateUserNote';
|
||||||
info: ModerationLogPayloads['updateUserNote'];
|
info: ModerationLogPayloads['updateUserNote'];
|
||||||
|
@ -3025,7 +3041,7 @@ type PartialRolePolicyOverride = Partial<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "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", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "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", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:reject-quotes", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||||
|
|
|
@ -713,6 +713,17 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:admin:reject-quotes*
|
||||||
|
*/
|
||||||
|
request<E extends 'admin/reject-quotes', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -83,6 +83,7 @@ import type {
|
||||||
AdminQueueInboxDelayedResponse,
|
AdminQueueInboxDelayedResponse,
|
||||||
AdminQueuePromoteRequest,
|
AdminQueuePromoteRequest,
|
||||||
AdminQueueStatsResponse,
|
AdminQueueStatsResponse,
|
||||||
|
AdminRejectQuotesRequest,
|
||||||
AdminRelaysAddRequest,
|
AdminRelaysAddRequest,
|
||||||
AdminRelaysAddResponse,
|
AdminRelaysAddResponse,
|
||||||
AdminRelaysListResponse,
|
AdminRelaysListResponse,
|
||||||
|
@ -674,6 +675,7 @@ export type Endpoints = {
|
||||||
'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse };
|
'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse };
|
||||||
'admin/queue/promote': { req: AdminQueuePromoteRequest; res: EmptyResponse };
|
'admin/queue/promote': { req: AdminQueuePromoteRequest; res: EmptyResponse };
|
||||||
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
|
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
|
||||||
|
'admin/reject-quotes': { req: AdminRejectQuotesRequest; res: EmptyResponse };
|
||||||
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
|
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
|
||||||
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };
|
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };
|
||||||
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
|
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
|
||||||
|
|
|
@ -86,6 +86,7 @@ export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliv
|
||||||
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
|
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
|
||||||
export type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json'];
|
export type AdminQueuePromoteRequest = operations['admin___queue___promote']['requestBody']['content']['application/json'];
|
||||||
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
|
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
|
||||||
|
export type AdminRejectQuotesRequest = operations['admin___reject-quotes']['requestBody']['content']['application/json'];
|
||||||
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
|
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
|
||||||
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];
|
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];
|
||||||
export type AdminRelaysListResponse = operations['admin___relays___list']['responses']['200']['content']['application/json'];
|
export type AdminRelaysListResponse = operations['admin___relays___list']['responses']['200']['content']['application/json'];
|
||||||
|
|
|
@ -594,6 +594,15 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['admin___queue___stats'];
|
post: operations['admin___queue___stats'];
|
||||||
};
|
};
|
||||||
|
'/admin/reject-quotes': {
|
||||||
|
/**
|
||||||
|
* admin/reject-quotes
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:admin:reject-quotes*
|
||||||
|
*/
|
||||||
|
post: operations['admin___reject-quotes'];
|
||||||
|
};
|
||||||
'/admin/relays/add': {
|
'/admin/relays/add': {
|
||||||
/**
|
/**
|
||||||
* admin/relays/add
|
* admin/relays/add
|
||||||
|
@ -3978,6 +3987,7 @@ export type components = {
|
||||||
noindex: boolean;
|
noindex: boolean;
|
||||||
enableRss: boolean;
|
enableRss: boolean;
|
||||||
mandatoryCW: string | null;
|
mandatoryCW: string | null;
|
||||||
|
rejectQuotes?: boolean;
|
||||||
isBot?: boolean;
|
isBot?: boolean;
|
||||||
isCat?: boolean;
|
isCat?: boolean;
|
||||||
speakAsCat?: boolean;
|
speakAsCat?: boolean;
|
||||||
|
@ -4340,6 +4350,8 @@ export type components = {
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
|
updatedAt?: string;
|
||||||
|
/** Format: date-time */
|
||||||
deletedAt?: string | null;
|
deletedAt?: string | null;
|
||||||
text: string | null;
|
text: string | null;
|
||||||
cw?: string | null;
|
cw?: string | null;
|
||||||
|
@ -4408,6 +4420,7 @@ export type components = {
|
||||||
url?: string;
|
url?: string;
|
||||||
reactionAndUserPairCache?: string[];
|
reactionAndUserPairCache?: string[];
|
||||||
clippedCount?: number;
|
clippedCount?: number;
|
||||||
|
processErrors?: string[] | null;
|
||||||
myReaction?: string | null;
|
myReaction?: string | null;
|
||||||
};
|
};
|
||||||
NoteReaction: {
|
NoteReaction: {
|
||||||
|
@ -4965,6 +4978,7 @@ export type components = {
|
||||||
latestRequestReceivedAt: string | null;
|
latestRequestReceivedAt: string | null;
|
||||||
isNSFW: boolean;
|
isNSFW: boolean;
|
||||||
rejectReports: boolean;
|
rejectReports: boolean;
|
||||||
|
rejectQuotes: boolean;
|
||||||
moderationNote?: string | null;
|
moderationNote?: string | null;
|
||||||
};
|
};
|
||||||
GalleryPost: {
|
GalleryPost: {
|
||||||
|
@ -8286,6 +8300,7 @@ export type operations = {
|
||||||
isNSFW?: boolean;
|
isNSFW?: boolean;
|
||||||
rejectReports?: boolean;
|
rejectReports?: boolean;
|
||||||
moderationNote?: string;
|
moderationNote?: string;
|
||||||
|
rejectQuotes?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -9222,6 +9237,59 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* admin/reject-quotes
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:admin:reject-quotes*
|
||||||
|
*/
|
||||||
|
'admin___reject-quotes': {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
userId: string;
|
||||||
|
rejectQuotes: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* admin/relays/add
|
* admin/relays/add
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
|
@ -89,6 +89,7 @@ export const permissions = [
|
||||||
'write:admin:unset-user-avatar',
|
'write:admin:unset-user-avatar',
|
||||||
'write:admin:unset-user-banner',
|
'write:admin:unset-user-banner',
|
||||||
'write:admin:unsuspend-user',
|
'write:admin:unsuspend-user',
|
||||||
|
'write:admin:reject-quotes',
|
||||||
'write:admin:meta',
|
'write:admin:meta',
|
||||||
'write:admin:user-note',
|
'write:admin:user-note',
|
||||||
'write:admin:roles',
|
'write:admin:roles',
|
||||||
|
@ -491,4 +492,22 @@ export type ModerationLogPayloads = {
|
||||||
postUserUsername: string;
|
postUserUsername: string;
|
||||||
post: GalleryPost;
|
post: GalleryPost;
|
||||||
};
|
};
|
||||||
|
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,18 @@ export type ModerationLog = {
|
||||||
} | {
|
} | {
|
||||||
type: 'unsuspend';
|
type: 'unsuspend';
|
||||||
info: ModerationLogPayloads['unsuspend'];
|
info: ModerationLogPayloads['unsuspend'];
|
||||||
|
} | {
|
||||||
|
type: 'acceptQuotesUser';
|
||||||
|
info: ModerationLogPayloads['acceptQuotesUser'];
|
||||||
|
} | {
|
||||||
|
type: 'rejectQuotesUser';
|
||||||
|
info: ModerationLogPayloads['rejectQuotesUser'];
|
||||||
|
} | {
|
||||||
|
type: 'acceptQuotesInstance';
|
||||||
|
info: ModerationLogPayloads['acceptQuotesInstance'];
|
||||||
|
} | {
|
||||||
|
type: 'rejectQuotesInstance';
|
||||||
|
info: ModerationLogPayloads['rejectQuotesInstance'];
|
||||||
} | {
|
} | {
|
||||||
type: 'updateUserNote';
|
type: 'updateUserNote';
|
||||||
info: ModerationLogPayloads['updateUserNote'];
|
info: ModerationLogPayloads['updateUserNote'];
|
||||||
|
|
|
@ -41,6 +41,11 @@ continueOnRemote: "Continue on remote instance"
|
||||||
chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub"
|
chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub"
|
||||||
mediaSilenceThisInstance: "Silence media from this instance"
|
mediaSilenceThisInstance: "Silence media from this instance"
|
||||||
rejectReports: "Reject reports from this instance"
|
rejectReports: "Reject reports from this instance"
|
||||||
|
rejectQuotesInstance: "Strip quote posts from this instance"
|
||||||
|
rejectQuotesRemoteUser: "Strip quote posts from this user"
|
||||||
|
rejectQuotesLocalUser: "Block quote posts from this user"
|
||||||
|
rejectQuotesConfirm: "Are you sure you wish to strip quote posts?"
|
||||||
|
allowQuotesConfirm: "Are you sure you wish to allow quote posts?"
|
||||||
silencedInstancesDescription: "List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
|
silencedInstancesDescription: "List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
|
||||||
mediaSilencedInstances: "Media-silenced instances"
|
mediaSilencedInstances: "Media-silenced instances"
|
||||||
mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances."
|
mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances."
|
||||||
|
@ -313,6 +318,8 @@ _moderationLogTypes:
|
||||||
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
|
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
|
||||||
rejectRemoteInstanceReports: "Rejected reports from remote instance"
|
rejectRemoteInstanceReports: "Rejected reports from remote instance"
|
||||||
acceptRemoteInstanceReports: "Accepted reports from remote instance"
|
acceptRemoteInstanceReports: "Accepted reports from remote instance"
|
||||||
|
rejectQuotesUser: "Blocked/Stripped quote posts from user"
|
||||||
|
allowQuotesUser: "Allowed quote posts from user"
|
||||||
_mfm:
|
_mfm:
|
||||||
uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
|
uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks"
|
||||||
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
|
intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax."
|
||||||
|
@ -445,6 +452,7 @@ _permissions:
|
||||||
"write:admin:cw-user": "Apply mandatory CW on users"
|
"write:admin:cw-user": "Apply mandatory CW on users"
|
||||||
"write:admin:silence-user": "Silence users"
|
"write:admin:silence-user": "Silence users"
|
||||||
"write:admin:unsilence-user": "Un-silence users"
|
"write:admin:unsilence-user": "Un-silence users"
|
||||||
|
"write:admin:reject-quotes": "Allow/Prohibit quote posts from a user"
|
||||||
"read:notes-schedule": "View your list of scheduled notes"
|
"read:notes-schedule": "View your list of scheduled notes"
|
||||||
"write:notes-schedule": "Compose or delete scheduled notes"
|
"write:notes-schedule": "Compose or delete scheduled notes"
|
||||||
|
|
||||||
|
@ -481,3 +489,6 @@ id: "ID"
|
||||||
|
|
||||||
mandatoryCW: "Force content warning"
|
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."
|
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."
|
||||||
|
|
||||||
|
_processErrors:
|
||||||
|
quoteUnavailable: "Unable to process quote. This post may be missing context."
|
||||||
|
|
Loading…
Add table
Reference in a new issue