import RE2 from "re2"; import { updateUserProfileData } from "../../../../services/i/update.js"; import { Users, DriveFiles, UserProfiles, Pages } from "../../../../models/index.js"; import { notificationTypes } from "../../../../types.js"; import { langmap } from "../../../../misc/langmap.js"; import { verifyLink } from "../../../../services/fetch-rel-me.js"; import { ApiError } from "../../error.js"; import define from "../../define.js"; export const meta = { tags: [ "account" ], requireCredential: true, kind: "write:account", errors: { noSuchAvatar: { message: "No such avatar file.", code: "NO_SUCH_AVATAR", id: "539f3a45-f215-4f81-a9a8-31293640207f" }, noSuchBanner: { message: "No such banner file.", code: "NO_SUCH_BANNER", id: "0d8f5629-f210-41c2-9433-735831a58595" }, avatarNotAnImage: { message: "The file specified as an avatar is not an image.", code: "AVATAR_NOT_AN_IMAGE", id: "f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191" }, bannerNotAnImage: { message: "The file specified as a banner is not an image.", code: "BANNER_NOT_AN_IMAGE", id: "75aedb19-2afd-4e6d-87fc-67941256fa60" }, noSuchPage: { message: "No such page.", code: "NO_SUCH_PAGE", id: "8e01b590-7eb9-431b-a239-860e086c408e" }, invalidRegexp: { message: "Invalid Regular Expression.", code: "INVALID_REGEXP", id: "0d786918-10df-41cd-8f33-8dec7d9a89a5" }, invalidFieldName: { message: "Invalid field name.", code: "INVALID_FIELD_NAME", id: "8f81972e-8b53-4d30-b0d2-efb026dda673" }, invalidFieldValue: { message: "Invalid field value.", code: "INVALID_FIELD_VALUE", id: "aede7444-244b-11ee-be56-0242ac120002" } }, res: { type: "object", optional: false, nullable: false, ref: "MeDetailed" } }; export const paramDef = { type: "object", properties: { name: { ...Users.nameSchema, nullable: true }, description: { ...Users.descriptionSchema, nullable: true }, location: { ...Users.locationSchema, nullable: true }, birthday: { ...Users.birthdaySchema, nullable: true }, lang: { type: "string", enum: [ null, ...Object.keys(langmap) ], nullable: true }, avatarId: { type: "string", format: "misskey:id", nullable: true }, bannerId: { type: "string", format: "misskey:id", nullable: true }, fields: { type: "array", minItems: 0, maxItems: 16, items: { type: "object", properties: { name: { type: "string" }, value: { type: "string" } }, required: [ "name", "value" ] } }, isLocked: { type: "boolean" }, isExplorable: { type: "boolean" }, hideOnlineStatus: { type: "boolean" }, publicReactions: { type: "boolean" }, allowCalls: { type: "boolean" }, symbolFileId: { type: "string", format: "misskey:id", nullable: true }, carefulBot: { type: "boolean" }, autoAcceptFollowed: { type: "boolean" }, noCrawle: { type: "boolean" }, preventAiLearning: { type: "boolean" }, isBot: { type: "boolean" }, isCat: { type: "boolean" }, speakAsCat: { type: "boolean" }, minorBadges: { type: "array", uniqueItems: true, maxItems: 1, items: { type: "string", enum: [ "K", "T", "E" ] } }, injectFeaturedNote: { type: "boolean" }, receiveAnnouncementEmail: { type: "boolean" }, alwaysMarkNsfw: { type: "boolean" }, ffVisibility: { type: "string", enum: [ "public", "followers", "private" ] }, pinnedPageId: { type: "string", format: "misskey:id", nullable: true }, mutedWords: { type: "array" }, mutedInstances: { type: "array", items: { type: "string" } }, mutingNotificationTypes: { type: "array", items: { type: "string", enum: notificationTypes } }, emailNotificationTypes: { type: "array", items: { type: "string" } }, pronouns: { type: "object", nullable: true }, canBite: { type: "string", enum: [ "anyone", "followers", "nobody" ], nullable: true } } }; export default define(meta, paramDef, async (ps, _user, token)=>{ const user = await Users.findOneByOrFail({ id: _user.id }); const isSecure = token == null; const updates = {}; const profileUpdates = {}; const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; if (ps.mutedWords !== undefined) { // validate regular expression syntax ps.mutedWords.filter((x)=>!Array.isArray(x)).forEach((x)=>{ const regexp = x.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { new RE2(regexp[1], regexp[2]); } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } }); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes; if (typeof ps.isLocked === "boolean") updates.isLocked = ps.isLocked; if (typeof ps.isExplorable === "boolean") updates.isExplorable = ps.isExplorable; if (typeof ps.hideOnlineStatus === "boolean") updates.hideOnlineStatus = ps.hideOnlineStatus; if (typeof ps.publicReactions === "boolean") profileUpdates.publicReactions = ps.publicReactions; if (typeof ps.allowCalls === "boolean") profileUpdates.allowCalls = ps.allowCalls; if (ps.symbolFileId !== undefined) profileUpdates.symbolFileId = ps.symbolFileId; if (typeof ps.isBot === "boolean") updates.isBot = ps.isBot; if (ps.minorBadges !== undefined) updates.minorBadges = ps.minorBadges.slice(0, 1); if (typeof ps.carefulBot === "boolean") profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === "boolean") profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === "boolean") profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat; if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat; if (typeof ps.injectFeaturedNote === "boolean") profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === "boolean") profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === "boolean") profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (typeof ps.pronouns === "object") { if (ps.pronouns === null) ps.pronouns = {}; for (const key of Object.keys(ps.pronouns)){ if (key.length !== 2 || typeof ps.pronouns[key] !== "string" || ps.pronouns[key].length === 0) { delete ps.pronouns[key]; } } profileUpdates.pronouns = ps.pronouns; } const avatar = ps.avatarId ? await DriveFiles.findOneBy({ id: ps.avatarId }) : null; const banner = ps.bannerId ? await DriveFiles.findOneBy({ id: ps.bannerId }) : null; if (ps.avatarId) { if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); if (!avatar.type.startsWith("image/")) throw new ApiError(meta.errors.avatarNotAnImage); updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(avatar, true); updates.avatarBlurhash = avatar.blurhash; } if (ps.bannerId) { if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); if (!banner.type.startsWith("image/")) throw new ApiError(meta.errors.bannerNotAnImage); updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(banner, false); updates.bannerBlurhash = banner.blurhash; } if (ps.pinnedPageId) { const page = await Pages.findOneBy({ id: ps.pinnedPageId }); if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage); profileUpdates.pinnedPageId = page.id; } else if (ps.pinnedPageId === null) { profileUpdates.pinnedPageId = null; } if (ps.fields) { for (const field of ps.fields){ if (!field || field.name === "" || field.value === "") { continue; } if (typeof field.name !== "string" || field.name === "") { throw new ApiError(meta.errors.invalidFieldName); } if (typeof field.value !== "string" || field.value === "") { throw new ApiError(meta.errors.invalidFieldValue); } if (field.value.startsWith("http")) { field.verified = await verifyLink(field.value, user.username); } } profileUpdates.fields = ps.fields.filter((x)=>Object.keys(x).length !== 0).map((x)=>{ return { name: x.name, value: x.value, verified: x.verified }; }); } if (ps.canBite) { updates.canBite = ps.canBite; } return updateUserProfileData(user, profile, updates, profileUpdates, isSecure); });