diff --git a/packages/backend/migration/1704343998612-avatardecoration_fed.js b/packages/backend/migration/1704343998612-avatardecoration_fed.js new file mode 100644 index 0000000000..8e182f92f4 --- /dev/null +++ b/packages/backend/migration/1704343998612-avatardecoration_fed.js @@ -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"`); + } +} diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index bf38d5fd60..c436f9f529 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -38,6 +38,8 @@ import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.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 { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -76,6 +78,8 @@ export class ApPersonService implements OnModuleInit { private apLoggerService: ApLoggerService; private accountMoveService: AccountMoveService; private logger: Logger; + private httpRequestService: HttpRequestService; + private avatarDecorationService: AvatarDecorationService; constructor( private moduleRef: ModuleRef, @@ -100,6 +104,7 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + ) { } @@ -124,6 +129,8 @@ export class ApPersonService implements OnModuleInit { this.apLoggerService = this.moduleRef.get('ApLoggerService'); this.accountMoveService = this.moduleRef.get('AccountMoveService'); this.logger = this.apLoggerService.logger; + this.httpRequestService = this.moduleRef.get('HttpRequestService'); + this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService'); } private punyHost(url: string): string { @@ -225,14 +232,14 @@ export class ApPersonService implements OnModuleInit { return null; } - private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise> { + private async resolveAvatarAndBanner(user: MiRemoteUser, host: string | null, icon: any, image: any): Promise> { const [avatar, banner] = await Promise.all([icon, image].map(img => { if (img == null) return null; if (user == null) throw new Error('failed to create user: user is null'); return this.apImageService.resolveImage(user, img).catch(() => null); })); - return { + const returnData: any = { avatarId: avatar?.id ?? null, bannerId: banner?.id ?? null, avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null, @@ -240,6 +247,42 @@ export class ApPersonService implements OnModuleInit { avatarBlurhash: avatar?.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 アバターとヘッダー画像をフェッチ 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); 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 url = getOneApHrefNullable(person.url); + let host = null; + if (url) { + host = new URL(url).host; + } if (url && !checkHttps(url)) { throw new Error('unexpected schema of person url: ' + url); @@ -462,7 +509,7 @@ export class ApPersonService implements OnModuleInit { movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, isExplorable: person.discoverable, - ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), + ...(await this.resolveAvatarAndBanner(exist, host, person.icon, person.image).catch(() => ({}))), } as Partial & Pick; const moving = ((): boolean => { diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index 010cbfe82d..c98d13df30 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -36,6 +36,11 @@ export class MiAvatarDecoration { default: '', }) public category: string; + @Column('varchar', { + length: 256, + nullable: true, + }) + public host: string; // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする @Column('varchar', { diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index 39297de536..805c1b974d 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -88,7 +88,9 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { 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, createdAt: this.idService.parse(avatarDecoration.id).date.toISOString(), updatedAt: avatarDecoration.updatedAt?.toISOString() ?? null, diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 028d647393..4fe15d8cf0 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -70,7 +70,10 @@ export default class extends Endpoint { // eslint- const decorations = await this.avatarDecorationService.getAll(true); 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, name: decoration.name, description: decoration.description, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b258349148..9299f7fa7c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -294,10 +294,10 @@ export default class extends Endpoint { // eslint- if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.isCat === 'boolean' && !ps.isGorilla) { updates.isCat = 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.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { @@ -342,11 +342,10 @@ export default class extends Endpoint { // eslint- const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); 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); if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); - updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, angle: d.angle ?? 0, diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index ae797eb7d2..345c0c888a 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -163,7 +163,7 @@ onMounted(() => { focus(); } }); - + if (props.mfmAutocomplete) { autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); } diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index cb2b9c280d..9a45455775 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.add }} {{ i18n.ts._poll.noMore }} {{ i18n.ts._poll.canMultipleVote }} -
+
@@ -152,7 +152,7 @@ watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit(' margin: 4px 8px; padding: 4px 8px; border-radius: 8px; - border: solid 2px var(--divider); + border: solid 1.5px var(--divider); > .caution { margin: 0 0 8px 0; font-size: 0.8em; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 33b8a9a86d..dfe1a0a69a 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -177,7 +177,7 @@ function show(ev: MouseEvent) {