From 89e2c302dd0f41abb3f49700d63fb1f8cdf7e84a Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 16 Mar 2023 14:24:11 +0900 Subject: [PATCH] refactor(backend): integrate CreateNotificationService to NotificationService --- .../backend/src/core/AchievementService.ts | 6 +- packages/backend/src/core/CoreModule.ts | 8 +- .../src/core/CreateNotificationService.ts | 125 ------------------ .../backend/src/core/NoteCreateService.ts | 16 +-- .../backend/src/core/NotificationService.ts | 114 +++++++++++++++- packages/backend/src/core/ReactionService.ts | 52 ++++---- .../backend/src/core/UserFollowingService.ts | 102 +++++++------- .../EndedPollNotificationProcessorService.ts | 8 +- .../server/api/endpoints/notes/polls/vote.ts | 2 - .../api/endpoints/notifications/create.ts | 6 +- 10 files changed, 205 insertions(+), 234 deletions(-) delete mode 100644 packages/backend/src/core/CreateNotificationService.ts diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts index 2ebee0f7e0..1ca38d8bb0 100644 --- a/packages/backend/src/core/AchievementService.ts +++ b/packages/backend/src/core/AchievementService.ts @@ -3,7 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js' import type { User } from '@/models/entities/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const ACHIEVEMENT_TYPES = [ 'notes1', @@ -90,7 +90,7 @@ export class AchievementService { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, ) { } @@ -114,7 +114,7 @@ export class AchievementService { }], }); - this.createNotificationService.createNotification(userId, 'achievementEarned', { + this.notificationService.createNotification(userId, 'achievementEarned', { achievement: type, }); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 1fd2d15004..d67e80fc1d 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -5,7 +5,6 @@ import { AntennaService } from './AntennaService.js'; import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; import { CaptchaService } from './CaptchaService.js'; -import { CreateNotificationService } from './CreateNotificationService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CustomEmojiService } from './CustomEmojiService.js'; import { DeleteAccountService } from './DeleteAccountService.js'; @@ -126,7 +125,6 @@ const $AntennaService: Provider = { provide: 'AntennaService', useExisting: Ante const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService }; const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; -const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService }; const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; @@ -250,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppLockService, AchievementService, CaptchaService, - CreateNotificationService, CreateSystemUserService, CustomEmojiService, DeleteAccountService, @@ -368,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppLockService, $AchievementService, $CaptchaService, - $CreateNotificationService, $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, @@ -487,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AppLockService, AchievementService, CaptchaService, - CreateNotificationService, CreateSystemUserService, CustomEmojiService, DeleteAccountService, @@ -604,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AppLockService, $AchievementService, $CaptchaService, - $CreateNotificationService, $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, @@ -714,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#endregion ], }) -export class CoreModule {} +export class CoreModule { } diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts deleted file mode 100644 index eba7171fb6..0000000000 --- a/packages/backend/src/core/CreateNotificationService.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { setTimeout } from 'node:timers/promises'; -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; -import type { User } from '@/models/entities/User.js'; -import type { Notification } from '@/models/entities/Notification.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { IdService } from '@/core/IdService.js'; -import { DI } from '@/di-symbols.js'; -import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; -import { PushNotificationService } from '@/core/PushNotificationService.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class CreateNotificationService implements OnApplicationShutdown { - #shutdownController = new AbortController(); - - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - @Inject(DI.notificationsRepository) - private notificationsRepository: NotificationsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - private notificationEntityService: NotificationEntityService, - private idService: IdService, - private globalEventService: GlobalEventService, - private pushNotificationService: PushNotificationService, - ) { - } - - @bindThis - public async createNotification( - notifieeId: User['id'], - type: Notification['type'], - data: Partial, - ): Promise { - if (data.notifierId && (notifieeId === data.notifierId)) { - return null; - } - - const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); - - const isMuted = profile?.mutingNotificationTypes.includes(type); - - // Create notification - const notification = await this.notificationsRepository.insert({ - id: this.idService.genId(), - createdAt: new Date(), - notifieeId: notifieeId, - type: type, - // 相手がこの通知をミュートしているようなら、既読を予めつけておく - isRead: isMuted, - ...data, - } as Partial) - .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); - - const packed = await this.notificationEntityService.pack(notification, {}); - - // Publish notification event - this.globalEventService.publishMainStream(notifieeId, 'notification', packed); - - // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する - setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { - const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); - if (fresh == null) return; // 既に削除されているかもしれない - if (fresh.isRead) return; - - //#region ただしミュートしているユーザーからの通知なら無視 - const mutings = await this.mutingsRepository.findBy({ - muterId: notifieeId, - }); - if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { - return; - } - //#endregion - - this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); - this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); - - if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); - if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); - }, () => { /* aborted, ignore it */ }); - - return notification; - } - - // TODO - //const locales = await import('../../../../locales/index.js'); - - // TODO: locale ファイルをクライアント用とサーバー用で分けたい - - @bindThis - private async emailNotificationFollow(userId: User['id'], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; - const locale = locales[userProfile.lang ?? 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ - } - - @bindThis - private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; - const locale = locales[userProfile.lang ?? 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ - } - - onApplicationShutdown(signal?: string | undefined): void { - this.#shutdownController.abort(); - } -} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 5a4df69b62..2fc2a3d54f 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -30,7 +30,7 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { AntennaService } from '@/core/AntennaService.js'; @@ -60,7 +60,7 @@ class NotificationManager { constructor( private mutingsRepository: MutingsRepository, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, notifier: { id: User['id']; }, note: Note, ) { @@ -101,7 +101,7 @@ class NotificationManager { // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する if (!mentioneesMutedUserIds.includes(this.notifier.id)) { - this.createNotificationService.createNotification(x.target, x.reason, { + this.notificationService.createNotification(x.target, x.reason, { notifierId: this.notifier.id, noteId: this.note.id, }); @@ -183,7 +183,7 @@ export class NoteCreateService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private queueService: QueueService, private noteReadService: NoteReadService, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private hashtagService: HashtagService, @@ -198,7 +198,7 @@ export class NoteCreateService implements OnApplicationShutdown { private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, - ) {} + ) { } @bindThis public async create(user: { @@ -391,7 +391,7 @@ export class NoteCreateService implements OnApplicationShutdown { // 投稿を作成 try { if (insert.hasPoll) { - // Start transaction + // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.insert(Note, insert); @@ -414,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown { return insert; } catch (e) { - // duplicate key error + // duplicate key error if (isDuplicateKeyValueError(e)) { const err = new Error('Duplicated note'); err.name = 'duplicated'; @@ -558,7 +558,7 @@ export class NoteCreateService implements OnApplicationShutdown { } }); - const nm = new NotificationManager(this.mutingsRepository, this.createNotificationService, user, note); + const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); await this.createMentionedEvents(mentionedUsers, note, nm); diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index 88173c2307..00bca4c0c4 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -1,21 +1,36 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { NotificationsRepository } from '@/models/index.js'; +import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { User } from '@/models/entities/User.js'; import type { Notification } from '@/models/entities/Notification.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { GlobalEventService } from './GlobalEventService.js'; -import { PushNotificationService } from './PushNotificationService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; +import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; +import { IdService } from '@/core/IdService.js'; @Injectable() -export class NotificationService { +export class NotificationService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + @Inject(DI.notificationsRepository) private notificationsRepository: NotificationsRepository, + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private notificationEntityService: NotificationEntityService, private userEntityService: UserEntityService, + private idService: IdService, private globalEventService: GlobalEventService, private pushNotificationService: PushNotificationService, ) { @@ -67,4 +82,93 @@ export class NotificationService { private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds }); } + + @bindThis + public async createNotification( + notifieeId: User['id'], + type: Notification['type'], + data: Partial, + ): Promise { + if (data.notifierId && (notifieeId === data.notifierId)) { + return null; + } + + const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId }); + + const isMuted = profile?.mutingNotificationTypes.includes(type); + + // Create notification + const notification = await this.notificationsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + notifieeId: notifieeId, + type: type, + // 相手がこの通知をミュートしているようなら、既読を予めつけておく + isRead: isMuted, + ...data, + } as Partial) + .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0])); + + const packed = await this.notificationEntityService.pack(notification, {}); + + // Publish notification event + this.globalEventService.publishMainStream(notifieeId, 'notification', packed); + + // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する + setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { + const fresh = await this.notificationsRepository.findOneBy({ id: notification.id }); + if (fresh == null) return; // 既に削除されているかもしれない + if (fresh.isRead) return; + + //#region ただしミュートしているユーザーからの通知なら無視 + const mutings = await this.mutingsRepository.findBy({ + muterId: notifieeId, + }); + if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { + return; + } + //#endregion + + this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed); + this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); + + if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! })); + }, () => { /* aborted, ignore it */ }); + + return notification; + } + + // TODO + //const locales = await import('../../../../locales/index.js'); + + // TODO: locale ファイルをクライアント用とサーバー用で分けたい + + @bindThis + private async emailNotificationFollow(userId: User['id'], follower: User) { + /* + const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; + const locale = locales[userProfile.lang ?? 'ja-JP']; + const i18n = new I18n(locale); + // TODO: render user information html + sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); + */ + } + + @bindThis + private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) { + /* + const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); + if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; + const locale = locales[userProfile.lang ?? 'ja-JP']; + const i18n = new I18n(locale); + // TODO: render user information html + sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); + */ + } + + onApplicationShutdown(signal?: string | undefined): void { + this.#shutdownController.abort(); + } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 3e644018d7..271ba79176 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -9,7 +9,7 @@ import { IdService } from '@/core/IdService.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import { emojiRegex } from '@/misc/emoji-regex.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; @@ -79,7 +79,7 @@ export class ReactionService { private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private perUserReactionsChart: PerUserReactionsChart, ) { } @@ -93,19 +93,19 @@ export class ReactionService { throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); } } - + // check visibility if (!await this.noteEntityService.isVisibleForMe(note, user.id)) { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } - + if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { reaction = '❤️'; } else { // TODO: cache reaction = await this.toDbReaction(reaction, user.host); } - + const record: NoteReaction = { id: this.idService.genId(), createdAt: new Date(), @@ -113,7 +113,7 @@ export class ReactionService { userId: user.id, reaction, }; - + // Create reaction try { await this.noteReactionsRepository.insert(record); @@ -123,7 +123,7 @@ export class ReactionService { noteId: note.id, userId: user.id, }); - + if (exists.reaction !== reaction) { // 別のリアクションがすでにされていたら置き換える await this.delete(user, note); @@ -136,7 +136,7 @@ export class ReactionService { throw e; } } - + // Increment reactions count const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; await this.notesRepository.createQueryBuilder().update() @@ -146,12 +146,12 @@ export class ReactionService { }) .where('id = :id', { id: note.id }) .execute(); - + this.perUserReactionsChart.update(user, note); - + // カスタム絵文字リアクションだったら絵文字情報も送る const decodedReaction = this.decodeReaction(reaction); - + const emoji = await this.emojisRepository.findOne({ where: { name: decodedReaction.name, @@ -159,7 +159,7 @@ export class ReactionService { }, select: ['name', 'host', 'originalUrl', 'publicUrl'], }); - + this.globalEventService.publishNoteStream(note.id, 'reacted', { reaction: decodedReaction.reaction, emoji: emoji != null ? { @@ -169,16 +169,16 @@ export class ReactionService { } : null, userId: user.id, }); - + // リアクションされたユーザーがローカルユーザーなら通知を作成 if (note.userHost === null) { - this.createNotificationService.createNotification(note.userId, 'reaction', { + this.notificationService.createNotification(note.userId, 'reaction', { notifierId: user.id, noteId: note.id, reaction: reaction, }); } - + //#region 配信 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note)); @@ -187,7 +187,7 @@ export class ReactionService { const reactee = await this.usersRepository.findOneBy({ id: note.userId }); dm.addDirectRecipe(reactee as RemoteUser); } - + if (['public', 'home', 'followers'].includes(note.visibility)) { dm.addFollowersRecipe(); } else if (note.visibility === 'specified') { @@ -196,7 +196,7 @@ export class ReactionService { dm.addDirectRecipe(u as RemoteUser); } } - + dm.execute(); } //#endregion @@ -209,18 +209,18 @@ export class ReactionService { noteId: note.id, userId: user.id, }); - + if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - + // Delete reaction const result = await this.noteReactionsRepository.delete(exist.id); - + if (result.affected !== 1) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - + // Decrement reactions count const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; await this.notesRepository.createQueryBuilder().update() @@ -229,14 +229,14 @@ export class ReactionService { }) .where('id = :id', { id: note.id }) .execute(); - + if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1); - + this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, userId: user.id, }); - + //#region 配信 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user)); @@ -250,7 +250,7 @@ export class ReactionService { } //#endregion } - + @bindThis public async getFallbackReaction(): Promise { const meta = await this.metaService.fetch(); @@ -300,7 +300,7 @@ export class ReactionService { // Unicode絵文字 const match = emojiRegex.exec(reaction); if (match) { - // 合字を含む1つの絵文字 + // 合字を含む1つの絵文字 const unicode = match[0]; // 異体字セレクタ除去 diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 9f09c34d4b..1c85504353 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -10,7 +10,7 @@ import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { WebhookService } from '@/core/WebhookService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -57,7 +57,7 @@ export class UserFollowingService { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, @@ -145,15 +145,15 @@ export class UserFollowingService { }, ): Promise { if (follower.id === followee.id) return; - + let alreadyFollowed = false as boolean; - + await this.followingsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), followerId: follower.id, followeeId: followee.id, - + // 非正規化 followerHost: follower.host, followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null, @@ -169,35 +169,35 @@ export class UserFollowingService { throw err; } }); - + const req = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, }); - + if (req) { await this.followRequestsRepository.delete({ followeeId: followee.id, followerId: follower.id, }); - + // 通知を作成 - this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', { + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { notifierId: followee.id, }); } - + if (alreadyFollowed) return; this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); - + //#region Increment counts await Promise.all([ this.usersRepository.increment({ id: follower.id }, 'followingCount', 1), this.usersRepository.increment({ id: followee.id }, 'followersCount', 1), ]); //#endregion - + //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(i => { @@ -211,9 +211,9 @@ export class UserFollowingService { }); } //#endregion - + this.perUserFollowingChart.update(follower, followee, true); - + // Publish follow event if (this.userEntityService.isLocalUser(follower)) { this.userEntityService.pack(followee.id, follower, { @@ -221,7 +221,7 @@ export class UserFollowingService { }).then(async packed => { this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); - + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'follow', { @@ -230,12 +230,12 @@ export class UserFollowingService { } }); } - + // Publish followed event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(async packed => { this.globalEventService.publishMainStream(followee.id, 'followed', packed); - + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'followed', { @@ -243,9 +243,9 @@ export class UserFollowingService { }); } }); - + // 通知を作成 - this.createNotificationService.createNotification(followee.id, 'follow', { + this.notificationService.createNotification(followee.id, 'follow', { notifierId: follower.id, }); } @@ -265,16 +265,16 @@ export class UserFollowingService { followerId: follower.id, followeeId: followee.id, }); - + if (following == null) { logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); return; } - + await this.followingsRepository.delete(following.id); - + this.decrementFollowing(follower, followee); - + // Publish unfollow event if (!silent && this.userEntityService.isLocalUser(follower)) { this.userEntityService.pack(followee.id, follower, { @@ -282,7 +282,7 @@ export class UserFollowingService { }).then(async packed => { this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); - + const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { this.queueService.webhookDeliver(webhook, 'unfollow', { @@ -291,33 +291,33 @@ export class UserFollowingService { } }); } - + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); this.queueService.deliver(follower, content, followee.inbox, false); } - + if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) { // local user has null host const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee)); this.queueService.deliver(followee, content, follower.inbox, false); } } - + @bindThis private async decrementFollowing( - follower: {id: User['id']; host: User['host']; }, + follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, ): Promise { this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); - + //#region Decrement following / followers counts await Promise.all([ this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1), ]); //#endregion - + //#region Update instance stats if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(i => { @@ -331,7 +331,7 @@ export class UserFollowingService { }); } //#endregion - + this.perUserFollowingChart.update(follower, followee, false); } @@ -346,23 +346,23 @@ export class UserFollowingService { requestId?: string, ): Promise { if (follower.id === followee.id) return; - + // check blocking const [blocking, blocked] = await Promise.all([ this.userBlockingService.checkBlocked(follower.id, followee.id), this.userBlockingService.checkBlocked(followee.id, follower.id), ]); - + if (blocking) throw new Error('blocking'); if (blocked) throw new Error('blocked'); - + const followRequest = await this.followRequestsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), followerId: follower.id, followeeId: followee.id, requestId, - + // 非正規化 followerHost: follower.host, followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined, @@ -371,22 +371,22 @@ export class UserFollowingService { followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); - + // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); - + this.userEntityService.pack(followee.id, followee, { detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); - + // 通知を作成 - this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', { + this.notificationService.createNotification(followee.id, 'receiveFollowRequest', { notifierId: follower.id, followRequestId: followRequest.id, }); } - + if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee)); this.queueService.deliver(follower, content, followee.inbox, false); @@ -404,26 +404,26 @@ export class UserFollowingService { ): Promise { if (this.userEntityService.isRemoteUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower)); - + if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので this.queueService.deliver(follower, content, followee.inbox, false); } } - + const request = await this.followRequestsRepository.findOneBy({ followeeId: followee.id, followerId: follower.id, }); - + if (request == null) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } - + await this.followRequestsRepository.delete({ followeeId: followee.id, followerId: follower.id, }); - + this.userEntityService.pack(followee.id, followee, { detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); @@ -440,18 +440,18 @@ export class UserFollowingService { followeeId: followee.id, followerId: follower.id, }); - + if (request == null) { throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.'); } - + await this.insertFollowingDoc(followee, follower); - + if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee)); this.queueService.deliver(followee, content, follower.inbox, false); } - + this.userEntityService.pack(followee.id, followee, { detail: true, }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); @@ -466,13 +466,13 @@ export class UserFollowingService { const requests = await this.followRequestsRepository.findBy({ followeeId: user.id, }); - + for (const request of requests) { const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId }); this.acceptFollowRequest(user, follower); } } - + /** * API following/request/reject */ diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 037dfa1a53..501ed4090a 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -3,11 +3,11 @@ import { DI } from '@/di-symbols.js'; import type { PollVotesRepository, NotesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type Bull from 'bull'; import type { EndedPollNotificationJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class EndedPollNotificationProcessorService { @@ -23,7 +23,7 @@ export class EndedPollNotificationProcessorService { @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('ended-poll-notification'); @@ -47,7 +47,7 @@ export class EndedPollNotificationProcessorService { const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; for (const userId of userIds) { - this.createNotificationService.createNotification(userId, 'pollEnded', { + this.notificationService.createNotification(userId, 'pollEnded', { noteId: note.id, }); } diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 1bbd79fe1e..2a44dc537e 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -8,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js'; import { PollService } from '@/core/PollService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; import { DI } from '@/di-symbols.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { ApiError } from '../../../error.js'; @@ -89,7 +88,6 @@ export default class extends Endpoint { private pollService: PollService, private apRendererService: ApRendererService, private globalEventService: GlobalEventService, - private createNotificationService: CreateNotificationService, private userBlockingService: UserBlockingService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 2e63eee263..4102a924ad 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { CreateNotificationService } from '@/core/CreateNotificationService.js'; +import { NotificationService } from '@/core/NotificationService.js'; export const meta = { tags: ['notifications'], @@ -27,10 +27,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( - private createNotificationService: CreateNotificationService, + private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, user, token) => { - this.createNotificationService.createNotification(user.id, 'app', { + this.notificationService.createNotification(user.id, 'app', { appAccessTokenId: token ? token.id : null, customBody: ps.body, customHeader: ps.header,