Feat: アバターデコレーションの受信を許可制に

Signed-off-by: mattyatea <mattyacocacora0@gmail.com>
This commit is contained in:
mattyatea 2023-10-27 10:32:22 +09:00
parent 4bee1a45a1
commit 082e48056c
No known key found for this signature in database
GPG Key ID: 068E54E2C33BEF9A
14 changed files with 135 additions and 38 deletions

2
locales/index.d.ts vendored
View File

@ -220,6 +220,8 @@ export interface Locale {
"blockedInstancesDescription": string; "blockedInstancesDescription": string;
"silencedInstances": string; "silencedInstances": string;
"silencedInstancesDescription": string; "silencedInstancesDescription": string;
"avatarDecorationsAcceptInstance": string;
"avatarDecorationsAcceptInstancesDescription": string;
"muteAndBlock": string; "muteAndBlock": string;
"mutedUsers": string; "mutedUsers": string;
"blockedUsers": string; "blockedUsers": string;

View File

@ -217,6 +217,8 @@ blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたサーバー" silencedInstances: "サイレンスしたサーバー"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
avatarDecorationsAcceptInstance: "アイコンデコレーションを表示できるサーバー"
avatarDecorationsAcceptInstancesDescription: "アイコンデコレーションを受け入れたいサーバーのホストを改行で区切って設定します。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"

View File

@ -0,0 +1,11 @@
export class Avatardecorationaccepthost1698363835246 {
name = 'Avatardecorationaccepthost1698363835246'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "avatarDecorationAcceptHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "avatarDecorationAcceptHosts"`);
}
}

View File

@ -41,6 +41,12 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
} }
@bindThis
public avatarDecorationAcceptHost(AcceptHost: string[] | undefined, host: string | null): boolean {
if (!AcceptHost || host == null) return false;
return AcceptHost.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis @bindThis
public extractDbHost(uri: string): string { public extractDbHost(uri: string): string {
const url = new URL(uri); const url = new URL(uri);

View File

@ -294,21 +294,24 @@ export class ApPersonService implements OnModuleInit {
//#endregion //#endregion
//#region アバターデコレーション取得 //#region アバターデコレーション取得
const _avatarDecorations = await this.apNoteService.extractAvatarDecorations(person.tag ?? [], host)
.catch(err => {
this.logger.error('error occurred while fetching user avatar decorations', { stack: err });
return [];
});
const avatarDecorations: {id: string, angle: number, flipH: boolean}[] = []; const avatarDecorations: {id: string, angle: number, flipH: boolean}[] = [];
if (this.utilityService.avatarDecorationAcceptHost((await this.metaService.fetch()).avatarDecorationAcceptHosts, host)) {
const _avatarDecorations = await this.apNoteService.extractAvatarDecorations(person.tag ?? [], host)
.catch(err => {
this.logger.error('error occurred while fetching user avatar decorations', { stack: err });
return [];
});
_avatarDecorations.forEach((value, index) => { _avatarDecorations.forEach((value, index) => {
avatarDecorations.push({ avatarDecorations.push({
id: value.id, id: value.id,
angle: person.AvatarDecorations ? person.AvatarDecorations[index].angle : 0, angle: person.AvatarDecorations ? person.AvatarDecorations[index].angle : 0,
flipH: person.AvatarDecorations ? person.AvatarDecorations[index].flipH : false, flipH: person.AvatarDecorations ? person.AvatarDecorations[index].flipH : false,
});
}); });
}); }
//#endregion //#endregion
try { try {
@ -439,22 +442,22 @@ export class ApPersonService implements OnModuleInit {
const person = this.validateActor(object, uri); const person = this.validateActor(object, uri);
this.logger.info(`Updating the Person: ${person.id}`); this.logger.info(`Updating the Person: ${person.id}`);
const _avatarDecorations = await this.apNoteService.extractAvatarDecorations(person.tag ?? [], exist.host)
.catch(err => {
this.logger.error('error occurred while fetching user avatar decorations', { stack: err });
return [];
});
const avatarDecorations: {id: string, angle: number, flipH: boolean}[] = []; const avatarDecorations: {id: string, angle: number, flipH: boolean}[] = [];
if (this.utilityService.avatarDecorationAcceptHost((await this.metaService.fetch()).avatarDecorationAcceptHosts, exist.host)) {
const _avatarDecorations = await this.apNoteService.extractAvatarDecorations(person.tag ?? [], exist.host)
.catch(err => {
this.logger.error('error occurred while fetching user avatar decorations', { stack: err });
return [];
});
_avatarDecorations.forEach((value, index) => { _avatarDecorations.forEach((value, index) => {
avatarDecorations.push({ avatarDecorations.push({
id: value.id, id: value.id,
angle: person.AvatarDecorations ? person.AvatarDecorations[index].angle : 0, angle: person.AvatarDecorations ? person.AvatarDecorations[index].angle : 0,
flipH: person.AvatarDecorations ? person.AvatarDecorations[index].flipH : false, flipH: person.AvatarDecorations ? person.AvatarDecorations[index].flipH : false,
});
}); });
}); }
// カスタム絵文字取得 // カスタム絵文字取得
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {

View File

@ -81,6 +81,11 @@ export class MiMeta {
}) })
public silencedHosts: string[]; public silencedHosts: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public avatarDecorationAcceptHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
nullable: true, nullable: true,

View File

@ -11,6 +11,9 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -33,6 +36,9 @@ export class CleanProcessorService {
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private idService: IdService, private idService: IdService,
private metaService: MetaService,
private utilityService: UtilityService,
private avatarDecorationService: AvatarDecorationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('clean'); this.logger = this.queueLoggerService.logger.createSubLogger('clean');
} }
@ -54,6 +60,14 @@ export class CleanProcessorService {
}); });
} }
const { avatarDecorationAcceptHosts } = await this.metaService.fetch();
const allAvatarDecorations = await this.avatarDecorationService.getAll();
for (const avatarDecoration of allAvatarDecorations) {
if (avatarDecoration.host !== null && !this.utilityService.avatarDecorationAcceptHost(avatarDecorationAcceptHosts, avatarDecoration.host)) {
await this.avatarDecorationService.delete(avatarDecoration.id);
}
}
const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign')
.where('assign.expiresAt IS NOT NULL') .where('assign.expiresAt IS NOT NULL')
.andWhere('assign.expiresAt < :now', { now: new Date() }) .andWhere('assign.expiresAt < :now', { now: new Date() })

View File

@ -86,8 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const avatarDecorations = await this.avatarDecorationService.getAll(true); const avatarDecorations = await this.avatarDecorationService.getAll(true);
return avatarDecorations.filter(x => x.host === null).map(avatarDecoration => ({
return avatarDecorations.map(avatarDecoration => ({
id: avatarDecoration.id, id: avatarDecoration.id,
createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(), createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(),
updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null, updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null,

View File

@ -115,6 +115,16 @@ export const meta = {
nullable: false, nullable: false,
}, },
}, },
avatarDecorationAcceptHosts: {
type: 'array',
optional: true,
nullable: false,
items: {
type: 'string',
optional: false,
nullable: false,
},
},
pinnedUsers: { pinnedUsers: {
type: 'array', type: 'array',
optional: false, nullable: false, optional: false, nullable: false,
@ -382,6 +392,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts, silencedHosts: instance.silencedHosts,
avatarDecorationAcceptHosts: instance.avatarDecorationAcceptHosts,
sensitiveWords: instance.sensitiveWords, sensitiveWords: instance.sensitiveWords,
preservedUsernames: instance.preservedUsernames, preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey, hcaptchaSecretKey: instance.hcaptchaSecretKey,

View File

@ -35,6 +35,20 @@ export const paramDef = {
type: 'string', type: 'string',
}, },
}, },
silencedHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
avatarDecorationAcceptHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
sensitiveWords: { sensitiveWords: {
type: 'array', nullable: true, items: { type: 'array', nullable: true, items: {
type: 'string', type: 'string',
@ -126,13 +140,6 @@ export const paramDef = {
perUserHomeTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' },
notesPerOneAd: { type: 'integer' }, notesPerOneAd: { type: 'integer' },
silencedHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
}, },
required: [], required: [],
} as const; } as const;
@ -173,6 +180,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return h !== '' && h !== lv && !set.blockedHosts?.includes(h); return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
}); });
} }
if (Array.isArray(ps.avatarDecorationAcceptHosts)) {
let lastValue = '';
set.avatarDecorationAcceptHosts = ps.avatarDecorationAcceptHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== '' && h !== lv && !set.avatarDecorationAcceptHosts?.includes(h);
});
}
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor; set.themeColor = ps.themeColor;
} }

View File

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const decorations = await this.avatarDecorationService.getAll(true); const decorations = await this.avatarDecorationService.getAll(true);
const allRoles = await this.roleService.getRoles(); const allRoles = await this.roleService.getRoles();
return decorations.map(decoration => ({ return decorations.filter(x => x.host === null).map(decoration => ({
id: decoration.id, id: decoration.id,
name: decoration.name, name: decoration.name,
description: decoration.description, description: decoration.description,

View File

@ -313,6 +313,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const allRoles = await this.roleService.getRoles(); const allRoles = await this.roleService.getRoles();
const decorationIds = decorations const decorationIds = decorations
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
.filter(d => d.host === null)
.map(d => d.id); .map(d => d.id);
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({

View File

@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkStickyContainer> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900"> <MkSpacer :contentMax="900">
<div class="_gaps"> <div v-if="tab === 'avatarDecorations'" class="_gaps">
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null"> <MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
<template #label>{{ avatarDecoration.name }}</template> <template #label>{{ avatarDecoration.name }}</template>
<template #caption>{{ avatarDecoration.description }}</template> <template #caption>{{ avatarDecoration.description }}</template>
@ -29,6 +29,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
</div> </div>
<div v-else-if="tab === 'avatarDecorationsAcceptHosts'">
<MkTextarea v-model="acceptHosts">
<span>{{ i18n.ts.avatarDecorationsAcceptInstance }}</span>
<template #caption>{{ i18n.ts.avatarDecorationsAcceptInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="acceptSave"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -48,7 +55,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
let avatarDecorations: any[] = $ref([]); let avatarDecorations: any[] = $ref([]);
let tab = $ref('avatarDecorations');
let acceptHosts: string = $ref('');
function add() { function add() {
avatarDecorations.unshift({ avatarDecorations.unshift({
_id: Math.random().toString(36), _id: Math.random().toString(36),
@ -79,10 +87,19 @@ async function save(avatarDecoration) {
} }
} }
async function acceptSave() {
await os.apiWithDialog('admin/update-meta', {
avatarDecorationAcceptHosts: acceptHosts.split('\n') || [],
});
}
function load() { function load() {
os.api('admin/avatar-decorations/list').then(_avatarDecorations => { os.api('admin/avatar-decorations/list').then(_avatarDecorations => {
avatarDecorations = _avatarDecorations; avatarDecorations = _avatarDecorations;
}); });
os.api('admin/meta').then(_meta => {
acceptHosts = _meta.avatarDecorationAcceptHosts.join('\n');
});
} }
load(); load();
@ -94,7 +111,15 @@ const headerActions = $computed(() => [{
handler: add, handler: add,
}]); }]);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => [{
key: 'avatarDecorations',
title: i18n.ts.avatarDecorations,
icon: 'ti ti-sparkles',
}, {
key: 'avatarDecorationsAcceptHosts',
title: i18n.ts.avatarDecorationsAcceptInstance,
icon: 'ti ti-thumb-up-filled',
}]);
definePageMetadata({ definePageMetadata({
title: i18n.ts.avatarDecorations, title: i18n.ts.avatarDecorations,

View File

@ -393,6 +393,7 @@ export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
export type AdminInstanceMetadata = DetailedInstanceMetadata & { export type AdminInstanceMetadata = DetailedInstanceMetadata & {
// TODO: There are more fields. // TODO: There are more fields.
blockedHosts: string[]; blockedHosts: string[];
avatarDecorationAcceptHosts: string[];
silencedHosts: string[]; silencedHosts: string[];
app192IconUrl: string | null; app192IconUrl: string | null;
app512IconUrl: string | null; app512IconUrl: string | null;