summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints/i/update.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server/api/endpoints/i/update.ts')
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts261
1 files changed, 144 insertions, 117 deletions
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 3c2f1cea0d..4b904d4696 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -1,19 +1,23 @@
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 { Inject, Injectable } from '@nestjs/common';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
-import { updateUsertags } from '@/services/update-hashtag.js';
-import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index.js';
-import { User } from '@/models/entities/user.js';
-import { UserProfile } from '@/models/entities/user-profile.js';
+import { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js';
+import type { User } from '@/models/entities/User.js';
+import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js';
+import type { UserProfile } from '@/models/entities/UserProfile.js';
import { notificationTypes } from '@/types.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { UserFollowingService } from '@/core/UserFollowingService.js';
+import { AccountUpdateService } from '@/core/AccountUpdateService.js';
+import { HashtagService } from '@/core/HashtagService.js';
+import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
-import define from '../../define.js';
export const meta = {
tags: ['account'],
@@ -70,10 +74,10 @@ export const meta = {
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 },
+ name: { ...nameSchema, nullable: true },
+ description: { ...descriptionSchema, nullable: true },
+ location: { ...locationSchema, nullable: true },
+ birthday: { ...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 },
@@ -122,134 +126,157 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
-export default define(meta, paramDef, async (ps, _user, token) => {
- const user = await Users.findOneByOrFail({ id: _user.id });
- const isSecure = token == null;
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
- const updates = {} as Partial<User>;
- const profileUpdates = {} as Partial<UserProfile>;
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
- const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
- 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);
+ @Inject(DI.pagesRepository)
+ private pagesRepository: PagesRepository,
- try {
- new RE2(regexp[1], regexp[2]);
- } catch (err) {
- throw new ApiError(meta.errors.invalidRegexp);
- }
- });
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
+ private userFollowingService: UserFollowingService,
+ private accountUpdateService: AccountUpdateService,
+ private hashtagService: HashtagService,
+ ) {
+ super(meta, paramDef, async (ps, _user, token) => {
+ const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
+ const isSecure = token == null;
- 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 as typeof notificationTypes[number][];
- 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.isBot === 'boolean') updates.isBot = ps.isBot;
- if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
- 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.isCat === 'boolean') updates.isCat = ps.isCat;
- 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 (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
- if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
+ const updates = {} as Partial<User>;
+ const profileUpdates = {} as Partial<UserProfile>;
- if (ps.avatarId) {
- const avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
+ const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
- if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
- }
+ 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);
- if (ps.bannerId) {
- const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
+ try {
+ new RE2(regexp[1], regexp[2]);
+ } catch (err) {
+ throw new ApiError(meta.errors.invalidRegexp);
+ }
+ });
- if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
- if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
- }
+ 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 as typeof notificationTypes[number][];
+ 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.isBot === 'boolean') updates.isBot = ps.isBot;
+ if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
+ 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.isCat === 'boolean') updates.isCat = ps.isCat;
+ 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 (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
+ if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
- if (ps.pinnedPageId) {
- const page = await Pages.findOneBy({ id: ps.pinnedPageId });
+ if (ps.avatarId) {
+ const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
- if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
+ if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
+ if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
+ }
- profileUpdates.pinnedPageId = page.id;
- } else if (ps.pinnedPageId === null) {
- profileUpdates.pinnedPageId = null;
- }
+ if (ps.bannerId) {
+ const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
- if (ps.fields) {
- profileUpdates.fields = ps.fields
- .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
- .map(x => {
- return { name: x.name, value: x.value };
- });
- }
+ if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
+ if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
+ }
- //#region emojis/tags
+ if (ps.pinnedPageId) {
+ const page = await this.pagesRepository.findOneBy({ id: ps.pinnedPageId });
- let emojis = [] as string[];
- let tags = [] as string[];
+ if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
- const newName = updates.name === undefined ? user.name : updates.name;
- const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
+ profileUpdates.pinnedPageId = page.id;
+ } else if (ps.pinnedPageId === null) {
+ profileUpdates.pinnedPageId = null;
+ }
- if (newName != null) {
- const tokens = mfm.parseSimple(newName);
- emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
- }
+ if (ps.fields) {
+ profileUpdates.fields = ps.fields
+ .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
+ .map(x => {
+ return { name: x.name, value: x.value };
+ });
+ }
- if (newDescription != null) {
- const tokens = mfm.parse(newDescription);
- emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
- tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
- }
+ //#region emojis/tags
- updates.emojis = emojis;
- updates.tags = tags;
+ let emojis = [] as string[];
+ let tags = [] as string[];
- // ハッシュタグ更新
- updateUsertags(user, tags);
- //#endregion
+ const newName = updates.name === undefined ? user.name : updates.name;
+ const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
- if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
- if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates);
+ if (newName != null) {
+ const tokens = mfm.parseSimple(newName);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
+ }
- const iObj = await Users.pack<true, true>(user.id, user, {
- detail: true,
- includeSecrets: isSecure,
- });
+ if (newDescription != null) {
+ const tokens = mfm.parse(newDescription);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
+ tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
+ }
- // Publish meUpdated event
- publishMainStream(user.id, 'meUpdated', iObj);
- publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOneBy({ userId: user.id }));
+ updates.emojis = emojis;
+ updates.tags = tags;
- // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
- if (user.isLocked && ps.isLocked === false) {
- acceptAllFollowRequests(user);
- }
+ // ハッシュタグ更新
+ this.hashtagService.updateUsertags(user, tags);
+ //#endregion
+
+ if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates);
+ if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
- // フォロワーにUpdateを配信
- publishToFollowers(user.id);
+ const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
+ detail: true,
+ includeSecrets: isSecure,
+ });
- return iObj;
-});
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(user.id, 'meUpdated', iObj);
+ this.globalEventService.publishUserEvent(user.id, 'updateUserProfile', await this.userProfilesRepository.findOneBy({ userId: user.id }));
+
+ // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
+ if (user.isLocked && ps.isLocked === false) {
+ this.userFollowingService.acceptAllFollowRequests(user);
+ }
+
+ // フォロワーにUpdateを配信
+ this.accountUpdateService.publishToFollowers(user.id);
+
+ return iObj;
+ });
+ }
+}