parent
a4974e3c8a
commit
085a93b9fc
|
@ -0,0 +1,13 @@
|
||||||
|
export class AvatardecorationFed1704343998612 {
|
||||||
|
name = 'AvatardecorationFed1704343998612'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "avatar_decoration" ADD "host" character varying(256)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "avatar_decoration" ALTER COLUMN "category" SET DEFAULT ''`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "avatar_decoration" ALTER COLUMN "category" DROP DEFAULT`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "avatar_decoration" DROP COLUMN "host"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,8 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
import type { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||||
import { extractApHashtags } from './tag.js';
|
import { extractApHashtags } from './tag.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -76,6 +78,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
private apLoggerService: ApLoggerService;
|
private apLoggerService: ApLoggerService;
|
||||||
private accountMoveService: AccountMoveService;
|
private accountMoveService: AccountMoveService;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private httpRequestService: HttpRequestService;
|
||||||
|
private avatarDecorationService: AvatarDecorationService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@ -100,6 +104,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +129,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
this.apLoggerService = this.moduleRef.get('ApLoggerService');
|
||||||
this.accountMoveService = this.moduleRef.get('AccountMoveService');
|
this.accountMoveService = this.moduleRef.get('AccountMoveService');
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
|
this.httpRequestService = this.moduleRef.get('HttpRequestService');
|
||||||
|
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
||||||
}
|
}
|
||||||
|
|
||||||
private punyHost(url: string): string {
|
private punyHost(url: string): string {
|
||||||
|
@ -225,14 +232,14 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
|
private async resolveAvatarAndBanner(user: MiRemoteUser, host: string | null, icon: any, image: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
|
||||||
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
||||||
if (img == null) return null;
|
if (img == null) return null;
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
if (user == null) throw new Error('failed to create user: user is null');
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
const returnData: any = {
|
||||||
avatarId: avatar?.id ?? null,
|
avatarId: avatar?.id ?? null,
|
||||||
bannerId: banner?.id ?? null,
|
bannerId: banner?.id ?? null,
|
||||||
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
||||||
|
@ -240,6 +247,42 @@ export class ApPersonService implements OnModuleInit {
|
||||||
avatarBlurhash: avatar?.blurhash ?? null,
|
avatarBlurhash: avatar?.blurhash ?? null,
|
||||||
bannerBlurhash: banner?.blurhash ?? null,
|
bannerBlurhash: banner?.blurhash ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
const i = await this.federatedInstanceService.fetch(host);
|
||||||
|
console.log('avatarDecorationFetch: start');
|
||||||
|
if (i.softwareName === 'misskey') {
|
||||||
|
const remoteUserId = user.uri.split('/users/')[1];
|
||||||
|
const userMetaRequest = await this.httpRequestService.send(`https://${i.host}/api/users/show`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'userId': remoteUserId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res: any = await userMetaRequest.json();
|
||||||
|
if (res.avatarDecorations) {
|
||||||
|
const localDecos = await this.avatarDecorationService.getAll();
|
||||||
|
// ローカルのデコレーションとして登録する
|
||||||
|
for (const deco of res.avatarDecorations) {
|
||||||
|
if (localDecos.some((v) => v.id === deco.id)) continue;
|
||||||
|
await this.avatarDecorationService.create({
|
||||||
|
id: deco.id,
|
||||||
|
updatedAt: null,
|
||||||
|
url: deco.url,
|
||||||
|
name: `import_${host}_${deco.id}`,
|
||||||
|
description: `Imported from ${host}`,
|
||||||
|
host: host,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Object.assign(returnData, { avatarDecorations: res.avatarDecorations });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,7 +423,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
//#region アバターとヘッダー画像をフェッチ
|
//#region アバターとヘッダー画像をフェッチ
|
||||||
try {
|
try {
|
||||||
const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image);
|
const updates = await this.resolveAvatarAndBanner(user, host, person.icon, person.image);
|
||||||
await this.usersRepository.update(user.id, updates);
|
await this.usersRepository.update(user.id, updates);
|
||||||
user = { ...user, ...updates };
|
user = { ...user, ...updates };
|
||||||
|
|
||||||
|
@ -442,6 +485,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
|
||||||
const url = getOneApHrefNullable(person.url);
|
const url = getOneApHrefNullable(person.url);
|
||||||
|
let host = null;
|
||||||
|
if (url) {
|
||||||
|
host = new URL(url).host;
|
||||||
|
}
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (url && !checkHttps(url)) {
|
||||||
throw new Error('unexpected schema of person url: ' + url);
|
throw new Error('unexpected schema of person url: ' + url);
|
||||||
|
@ -462,7 +509,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
movedToUri: person.movedTo ?? null,
|
movedToUri: person.movedTo ?? null,
|
||||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||||
isExplorable: person.discoverable,
|
isExplorable: person.discoverable,
|
||||||
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
|
...(await this.resolveAvatarAndBanner(exist, host, person.icon, person.image).catch(() => ({}))),
|
||||||
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||||
|
|
||||||
const moving = ((): boolean => {
|
const moving = ((): boolean => {
|
||||||
|
|
|
@ -36,6 +36,11 @@ export class MiAvatarDecoration {
|
||||||
default: '',
|
default: '',
|
||||||
})
|
})
|
||||||
public category: string;
|
public category: string;
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public host: string;
|
||||||
|
|
||||||
// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
|
// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
|
|
|
@ -88,7 +88,9 @@ 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.map(avatarDecoration => ({
|
const filteredAvatarDecorations = avatarDecorations.filter(avatarDecoration => avatarDecoration.host === null);
|
||||||
|
console.log(filteredAvatarDecorations);
|
||||||
|
return filteredAvatarDecorations.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,
|
||||||
|
|
|
@ -70,7 +70,10 @@ 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 => ({
|
// Filter decorations where host is null
|
||||||
|
const filteredDecorations = decorations.filter(decoration => decoration.host === null);
|
||||||
|
|
||||||
|
return filteredDecorations.map(decoration => ({
|
||||||
id: decoration.id,
|
id: decoration.id,
|
||||||
name: decoration.name,
|
name: decoration.name,
|
||||||
description: decoration.description,
|
description: decoration.description,
|
||||||
|
|
|
@ -294,10 +294,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
|
||||||
if (typeof ps.isCat === 'boolean' && !ps.isGorilla) {
|
if (typeof ps.isCat === 'boolean' && !ps.isGorilla) {
|
||||||
updates.isCat = ps.isCat;
|
updates.isCat = ps.isCat;
|
||||||
};
|
}
|
||||||
if (typeof ps.isGorilla === 'boolean' && !ps.isCat) {
|
if (typeof ps.isGorilla === 'boolean' && !ps.isCat) {
|
||||||
updates.isGorilla = ps.isGorilla
|
updates.isGorilla = ps.isGorilla;
|
||||||
};
|
}
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||||
|
@ -342,11 +342,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
|
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
|
||||||
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.host === null && (d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))))
|
||||||
.map(d => d.id);
|
.map(d => d.id);
|
||||||
|
|
||||||
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
|
||||||
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
|
||||||
id: d.id,
|
id: d.id,
|
||||||
angle: d.angle ?? 0,
|
angle: d.angle ?? 0,
|
||||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ i18n.ts.add }}</MkButton>
|
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ i18n.ts.add }}</MkButton>
|
||||||
<MkButton v-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton>
|
<MkButton v-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton>
|
||||||
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
|
||||||
<section>
|
<section style="margin-bottom: 8px; border-top: solid 1.5px var(--divider);">
|
||||||
<div>
|
<div>
|
||||||
<MkSelect v-model="expiration" small>
|
<MkSelect v-model="expiration" small>
|
||||||
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
<template #label>{{ i18n.ts._poll.expiration }}</template>
|
||||||
|
@ -152,7 +152,7 @@ watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('
|
||||||
margin: 4px 8px;
|
margin: 4px 8px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: solid 2px var(--divider);
|
border: solid 1.5px var(--divider);
|
||||||
> .caution {
|
> .caution {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
|
|
@ -177,7 +177,7 @@ function show(ev: MouseEvent) {
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.label {
|
.label {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
padding: 0 0 8px 0;
|
padding: 8px 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
|
Loading…
Reference in New Issue