From 44fb31ab13a92fbfc18c1c1cc5a0f712ce54e95b Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Wed, 18 Oct 2023 00:48:34 +0200 Subject: [PATCH] [mastodon-client] Use new backend service for user (profile) updates This fixes profile updates not immediately federating when edited through the Mastodon client API. --- .../src/server/api/endpoints/i/update.ts | 64 +---------------- .../src/server/api/mastodon/helpers/user.ts | 17 +++-- packages/backend/src/services/i/update.ts | 70 ++++++++++++++++++- 3 files changed, 81 insertions(+), 70 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a2a912698..3af61f6f1 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -2,7 +2,7 @@ import RE2 from "re2"; import * as mfm from "mfm-js"; import { publishMainStream, publishUserEvent } from "@/services/stream.js"; import acceptAllFollowRequests from "@/services/following/requests/accept-all.js"; -import { publishToFollowers } from "@/services/i/update.js"; +import { publishToFollowers, updateUserProfileData } from "@/services/i/update.js"; import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; import { extractHashtags } from "@/misc/extract-hashtags.js"; import { updateUsertags } from "@/services/update-hashtag.js"; @@ -274,65 +274,5 @@ export default define(meta, paramDef, async (ps, _user, token) => { }); } - //#region emojis/tags - - let emojis = [] as string[]; - let tags = [] as string[]; - - const newName = updates.name === undefined ? user.name : updates.name; - const newDescription = - profileUpdates.description === undefined - ? profile.description - : profileUpdates.description; - - if (newName != null) { - const tokens = mfm.parseSimple(newName); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - } - - if (newDescription != null) { - const tokens = mfm.parse(newDescription); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - tags = extractHashtags(tokens!) - .map((tag) => normalizeForSearch(tag)) - .splice(0, 32); - } - - updates.emojis = emojis; - updates.tags = tags; - - // ハッシュタグ更新 - updateUsertags(user, tags); - //#endregion - - if (Object.keys(updates).length > 0) await Users.update(user.id, updates); - if (Object.keys(profileUpdates).length > 0) { - await UserProfiles.update(user.id, profileUpdates); - await UserProfiles.updateMentions(user.id); - } - - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: isSecure, - }); - - // Publish meUpdated event - publishMainStream(user.id, "meUpdated", iObj); - publishUserEvent( - user.id, - "updateUserProfile", - await UserProfiles.findOneBy({ userId: user.id }), - ); - - // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && ps.isLocked === false) { - acceptAllFollowRequests(user); - } - - // フォロワーにUpdateを配信 - UserProfiles.updateMentions(user.id).finally(() => { - publishToFollowers(user.id); - }); - - return iObj; + return updateUserProfileData(user, profile, updates, profileUpdates, isSecure); }); diff --git a/packages/backend/src/server/api/mastodon/helpers/user.ts b/packages/backend/src/server/api/mastodon/helpers/user.ts index 0a679c3e4..5c46442e8 100644 --- a/packages/backend/src/server/api/mastodon/helpers/user.ts +++ b/packages/backend/src/server/api/mastodon/helpers/user.ts @@ -44,6 +44,7 @@ import { MastoContext } from "@/server/api/mastodon/index.js"; import { resolveUser } from "@/remote/resolve-user.js"; import { updatePerson } from "@/remote/activitypub/models/person.js"; import { promiseEarlyReturn } from "@/prelude/promise.js"; +import { updateUserProfileData } from "@/services/i/update.js"; export type AccountCache = { locks: AsyncLock; @@ -181,12 +182,18 @@ export class UserHelpers { if (formData.fields_attributes) { profileUpdates.fields = await Promise.all(formData.fields_attributes.map(async field => { - const verified = field.value.startsWith("http") ? await verifyLink(field.value, user.username) : undefined; + if (!(field.name.trim() === "" && field.value.trim() === "")) { + if (field.name.trim() === "") throw new MastoApiError(400, "Field name can not be empty"); + if (field.value.trim() === "") throw new MastoApiError(400, "Field value can not be empty"); + } + const verified = field.value.startsWith("http") + ? (await promiseEarlyReturn(verifyLink(field.value, user.username), 1500)) ?? false + : undefined; return { ...field, verified }; - })); + })).then(p => p.filter(field => field.name.trim().length > 0 && field.value.length > 0)); } if (formData.display_name) updates.name = formData.display_name; @@ -195,11 +202,7 @@ export class UserHelpers { if (formData.bot) updates.isBot = formData.bot; if (formData.discoverable) updates.isExplorable = formData.discoverable; - if (Object.keys(updates).length > 0) await Users.update(user.id, updates); - if (Object.keys(profileUpdates).length > 0) { - await UserProfiles.update({ userId: user.id }, profileUpdates); - await promiseEarlyReturn(UserProfiles.updateMentions(user.id), 1500); - } + await updateUserProfileData(user, null, updates, profileUpdates, false); return this.verifyCredentials(ctx); } diff --git a/packages/backend/src/services/i/update.ts b/packages/backend/src/services/i/update.ts index cc950ac85..2f8260da6 100644 --- a/packages/backend/src/services/i/update.ts +++ b/packages/backend/src/services/i/update.ts @@ -1,10 +1,78 @@ import renderUpdate from "@/remote/activitypub/renderer/update.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { Users } from "@/models/index.js"; +import { UserProfiles, Users } from "@/models/index.js"; import type { User } from "@/models/entities/user.js"; import { renderPerson } from "@/remote/activitypub/renderer/person.js"; import { deliverToFollowers } from "@/remote/activitypub/deliver-manager.js"; import { deliverToRelays } from "../relay.js"; +import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; +import { extractHashtags } from "@/misc/extract-hashtags.js"; +import { normalizeForSearch } from "@/misc/normalize-for-search.js"; +import { updateUsertags } from "@/services/update-hashtag.js"; +import { publishMainStream, publishUserEvent } from "@/services/stream.js"; +import acceptAllFollowRequests from "@/services/following/requests/accept-all.js"; +import { UserProfile } from "@/models/entities/user-profile.js"; +import mfm from "mfm-js"; +import { promiseEarlyReturn } from "@/prelude/promise.js"; + +export async function updateUserProfileData(user: User, profile: UserProfile | null, updates: Partial, profileUpdates: Partial, isSecure: boolean) { + if (!profile) profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + + let emojis = [] as string[]; + let tags = [] as string[]; + + const newName = updates.name === undefined ? user.name : updates.name; + const newDescription = + profileUpdates.description === undefined + ? profile.description + : profileUpdates.description; + + if (newName != null) { + const tokens = mfm.parseSimple(newName); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + } + + if (newDescription != null) { + const tokens = mfm.parse(newDescription); + emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); + tags = extractHashtags(tokens!) + .map((tag) => normalizeForSearch(tag)) + .splice(0, 32); + } + + updates.emojis = emojis; + updates.tags = tags; + + updateUsertags(user, tags); + + if (Object.keys(updates).length > 0) await Users.update(user.id, updates); + if (Object.keys(profileUpdates).length > 0) { + await UserProfiles.update(user.id, profileUpdates); + await promiseEarlyReturn(UserProfiles.updateMentions(user.id), 1500); + } + + const iObj = await Users.pack(user.id, user, { + detail: true, + includeSecrets: isSecure, + }); + + publishMainStream(user.id, "meUpdated", iObj); + publishUserEvent( + user.id, + "updateUserProfile", + await UserProfiles.findOneByOrFail({ userId: user.id }), + ); + + if (user.isLocked && updates.isLocked === false) { + acceptAllFollowRequests(user); + } + + UserProfiles.updateMentions(user.id).finally(() => { + publishToFollowers(user.id); + }); + + return iObj; +} export async function publishToFollowers(userId: User["id"]) { const user = await Users.findOneBy({ id: userId });