diff --git a/CHANGELOG.md b/CHANGELOG.md index 76abe42e10..99a278736c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) +- Enhance: 連合する必要のないプロフィール項目しか更新されなかった場合には連合先にUpdateアクティビティを発行しないように - fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように - Fix: Nested proxy requestsを検出した際にブロックするように [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 5617a29bab..b62bf88bdc 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -9,7 +9,8 @@ import { In } from 'typeorm'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; +import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser, MiLocalUserForApPersonRender } from '@/models/User.js'; +import type { MiUserProfileForApPersonRender } from '@/models/UserProfile.js'; import type { IMentionedRemoteUsers, MiNote } from '@/models/Note.js'; import type { MiBlocking } from '@/models/Blocking.js'; import type { MiRelay } from '@/models/Relay.js'; @@ -251,7 +252,7 @@ export class ApRendererService { } @bindThis - public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { + public renderKey(user: { id: MiUser['id'] }, key: MiUserKeypair, postfix?: string): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', @@ -449,14 +450,25 @@ export class ApRendererService { } @bindThis - public async renderPerson(user: MiLocalUser) { + public async renderPerson(user: MiLocalUserForApPersonRender) { const id = this.userEntityService.genLocalUserUri(user.id); const isSystem = user.username.includes('.'); + /** + * 【profile について】 + * + * i/updateで虚無を連合するのを防止するための処理に伴い、 + * 使用できるプロパティを狭めることで、連合に使用するプロパティを増やした際に + * miUserProfileKeysUsedForApPersonRenderを変更するのを + * 忘れないようにするためにasを使っている。 + * + * See https://github.com/misskey-dev/misskey/pull/14301 + */ + const [avatar, banner, profile] = await Promise.all([ user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined, user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined, - this.userProfilesRepository.findOneByOrFail({ userId: user.id }), + (this.userProfilesRepository.findOneByOrFail({ userId: user.id }) as Promise), ]); const attachment = profile.fields.map(field => ({ diff --git a/packages/backend/src/misc/prelude/object.ts b/packages/backend/src/misc/prelude/object.ts new file mode 100644 index 0000000000..e74d461c69 --- /dev/null +++ b/packages/backend/src/misc/prelude/object.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function getObjKeys(obj: T): (keyof T)[] { + return Object.keys(obj); +} diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 96de30c4c2..f8034eeb2a 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -307,6 +307,24 @@ export type MiPartialRemoteUser = Partial & { uri: string; } +export const miLocalUserKeysUsedForApPersonRender = [ + 'id', + 'username', + 'avatarId', + 'bannerId', + 'emojis', + 'tags', + 'isBot', + 'isCat', + 'name', + 'isLocked', + 'isExplorable', + 'movedToUri', + 'alsoKnownAs', +] as const satisfies (keyof MiLocalUser)[]; + +export type MiLocalUserForApPersonRender = Pick; + export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 5544555296..5ca9829442 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -295,3 +295,12 @@ export class MiUserProfile { } } } + +export const miUserProfileKeysUsedForApPersonRender = [ + 'fields', + 'description', + 'birthday', + 'location', +] as const satisfies (keyof MiUserProfile)[]; + +export type MiUserProfileForApPersonRender = Pick; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index d3eeb75b27..e2538e0d97 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { isDeepStrictEqual } from 'node:util'; import RE2 from 're2'; import * as mfm from 'mfm-js'; import { Inject, Injectable } from '@nestjs/common'; @@ -11,6 +12,7 @@ import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; +import { getObjKeys } from '@/misc/prelude/object.js'; import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; @@ -34,6 +36,8 @@ import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; +import { miLocalUserKeysUsedForApPersonRender } from '@/models/User.js'; +import { miUserProfileKeysUsedForApPersonRender } from '@/models/UserProfile.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -507,15 +511,37 @@ export default class extends Endpoint { // eslint- this.hashtagService.updateUsertags(user, tags); //#endregion - if (Object.keys(updates).length > 0) { - await this.usersRepository.update(user.id, updates); + //#region 変更されていないプロパティを削除 + const _updates = getObjKeys(updates).reduce>((acc, key) => { + if (updates[key] !== undefined && !isDeepStrictEqual(updates[key], user[key])) { + (acc[key] as MiUser[typeof key]) = updates[key]; + } + return acc; + }, {}); + + const _profileUpdates = getObjKeys(profileUpdates).reduce>((acc, key) => { + if (profileUpdates[key] !== undefined && !isDeepStrictEqual(profileUpdates[key], profile[key])) { + (acc[key] as MiUserProfile[typeof key]) = profileUpdates[key]; + } + return acc; + }, {}); + //#endregion + + if (Object.keys(_updates).length > 0) { + await this.usersRepository.update(user.id, _updates); this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); } - await this.userProfilesRepository.update(user.id, { - ...profileUpdates, - verifiedLinks: [], - }); + if ( + Object.keys(_profileUpdates).length > 0 || + profile.fields.filter(x => x.value.startsWith('https://')).length > 0 || + profile.verifiedLinks.length > 0 + ) { + await this.userProfilesRepository.update(user.id, { + ..._profileUpdates, + verifiedLinks: [], + }); + } const iObj = await this.userEntityService.pack(user.id, user, { schema: 'MeDetailed', @@ -534,8 +560,13 @@ export default class extends Endpoint { // eslint- this.userFollowingService.acceptAllFollowRequests(user); } - // フォロワーにUpdateを配信 - this.accountUpdateService.publishToFollowers(user.id); + // 連合する必要があるプロパティが変更されている場合はフォロワーにUpdateを配信 + if ( + miLocalUserKeysUsedForApPersonRender.some(k => getObjKeys(_updates).includes(k)) || + miUserProfileKeysUsedForApPersonRender.some(k => getObjKeys(_profileUpdates).includes(k)) + ) { + this.accountUpdateService.publishToFollowers(user.id); + } const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://')); for (const url of urls) {