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 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<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 => { | ||||
| 			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<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; | ||||
| 
 | ||||
| 		const moving = ((): boolean => { | ||||
|  |  | |||
|  | @ -36,6 +36,11 @@ export class MiAvatarDecoration { | |||
| 		default: '', | ||||
| 	}) | ||||
| 	public category: string; | ||||
| 	@Column('varchar', { | ||||
| 		length: 256, | ||||
| 		nullable: true, | ||||
| 	}) | ||||
| 	public host: string; | ||||
| 
 | ||||
| 	// TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする
 | ||||
| 	@Column('varchar', { | ||||
|  |  | |||
|  | @ -88,7 +88,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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, | ||||
|  |  | |||
|  | @ -70,7 +70,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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, | ||||
|  |  | |||
|  | @ -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.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<typeof meta, typeof paramDef> { // 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, | ||||
|  |  | |||
|  | @ -163,7 +163,7 @@ onMounted(() => { | |||
| 			focus(); | ||||
| 		} | ||||
| 	}); | ||||
| 	 | ||||
| 
 | ||||
| 	if (props.mfmAutocomplete) { | ||||
| 		autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); | ||||
| 	} | ||||
|  |  | |||
|  | @ -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-else class="add" disabled>{{ i18n.ts._poll.noMore }}</MkButton> | ||||
| 	<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch> | ||||
| 	<section> | ||||
| 	<section style="margin-bottom: 8px;  border-top: solid 1.5px var(--divider);"> | ||||
| 		<div> | ||||
| 			<MkSelect v-model="expiration" small> | ||||
| 				<template #label>{{ i18n.ts._poll.expiration }}</template> | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -177,7 +177,7 @@ function show(ev: MouseEvent) { | |||
| <style lang="scss" module> | ||||
| .label { | ||||
| 	font-size: 0.85em; | ||||
| 	padding: 0 0 8px 0; | ||||
| 	padding: 8px 0; | ||||
| 	user-select: none; | ||||
| 
 | ||||
| 	&:empty { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue