mirror of
https://codeberg.org/yeentown/barkey
synced 2024-11-26 08:45:14 +00:00
refactor(backend): integrate CreateNotificationService to NotificationService
This commit is contained in:
parent
0944c1cd6f
commit
89e2c302dd
10 changed files with 205 additions and 234 deletions
|
@ -3,7 +3,7 @@ import type { UserProfilesRepository, UsersRepository } from '@/models/index.js'
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
|
||||||
export const ACHIEVEMENT_TYPES = [
|
export const ACHIEVEMENT_TYPES = [
|
||||||
'notes1',
|
'notes1',
|
||||||
|
@ -90,7 +90,7 @@ export class AchievementService {
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: 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,
|
achievement: type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { AntennaService } from './AntennaService.js';
|
||||||
import { AppLockService } from './AppLockService.js';
|
import { AppLockService } from './AppLockService.js';
|
||||||
import { AchievementService } from './AchievementService.js';
|
import { AchievementService } from './AchievementService.js';
|
||||||
import { CaptchaService } from './CaptchaService.js';
|
import { CaptchaService } from './CaptchaService.js';
|
||||||
import { CreateNotificationService } from './CreateNotificationService.js';
|
|
||||||
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
||||||
import { CustomEmojiService } from './CustomEmojiService.js';
|
import { CustomEmojiService } from './CustomEmojiService.js';
|
||||||
import { DeleteAccountService } from './DeleteAccountService.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 $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppLockService };
|
||||||
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService };
|
||||||
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService };
|
||||||
const $CreateNotificationService: Provider = { provide: 'CreateNotificationService', useExisting: CreateNotificationService };
|
|
||||||
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
||||||
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
|
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
|
||||||
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
|
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
|
||||||
|
@ -250,7 +248,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
AppLockService,
|
AppLockService,
|
||||||
AchievementService,
|
AchievementService,
|
||||||
CaptchaService,
|
CaptchaService,
|
||||||
CreateNotificationService,
|
|
||||||
CreateSystemUserService,
|
CreateSystemUserService,
|
||||||
CustomEmojiService,
|
CustomEmojiService,
|
||||||
DeleteAccountService,
|
DeleteAccountService,
|
||||||
|
@ -368,7 +365,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$AppLockService,
|
$AppLockService,
|
||||||
$AchievementService,
|
$AchievementService,
|
||||||
$CaptchaService,
|
$CaptchaService,
|
||||||
$CreateNotificationService,
|
|
||||||
$CreateSystemUserService,
|
$CreateSystemUserService,
|
||||||
$CustomEmojiService,
|
$CustomEmojiService,
|
||||||
$DeleteAccountService,
|
$DeleteAccountService,
|
||||||
|
@ -487,7 +483,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
AppLockService,
|
AppLockService,
|
||||||
AchievementService,
|
AchievementService,
|
||||||
CaptchaService,
|
CaptchaService,
|
||||||
CreateNotificationService,
|
|
||||||
CreateSystemUserService,
|
CreateSystemUserService,
|
||||||
CustomEmojiService,
|
CustomEmojiService,
|
||||||
DeleteAccountService,
|
DeleteAccountService,
|
||||||
|
@ -604,7 +599,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$AppLockService,
|
$AppLockService,
|
||||||
$AchievementService,
|
$AchievementService,
|
||||||
$CaptchaService,
|
$CaptchaService,
|
||||||
$CreateNotificationService,
|
|
||||||
$CreateSystemUserService,
|
$CreateSystemUserService,
|
||||||
$CustomEmojiService,
|
$CustomEmojiService,
|
||||||
$DeleteAccountService,
|
$DeleteAccountService,
|
||||||
|
@ -714,4 +708,4 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
//#endregion
|
//#endregion
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {}
|
export class CoreModule { }
|
||||||
|
|
|
@ -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<Notification>,
|
|
||||||
): Promise<Notification | null> {
|
|
||||||
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<Notification>)
|
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,7 @@ import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.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 { WebhookService } from '@/core/WebhookService.js';
|
||||||
import { HashtagService } from '@/core/HashtagService.js';
|
import { HashtagService } from '@/core/HashtagService.js';
|
||||||
import { AntennaService } from '@/core/AntennaService.js';
|
import { AntennaService } from '@/core/AntennaService.js';
|
||||||
|
@ -60,7 +60,7 @@ class NotificationManager {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
private createNotificationService: CreateNotificationService,
|
private notificationService: NotificationService,
|
||||||
notifier: { id: User['id']; },
|
notifier: { id: User['id']; },
|
||||||
note: Note,
|
note: Note,
|
||||||
) {
|
) {
|
||||||
|
@ -101,7 +101,7 @@ class NotificationManager {
|
||||||
|
|
||||||
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
|
||||||
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
|
||||||
this.createNotificationService.createNotification(x.target, x.reason, {
|
this.notificationService.createNotification(x.target, x.reason, {
|
||||||
notifierId: this.notifier.id,
|
notifierId: this.notifier.id,
|
||||||
noteId: this.note.id,
|
noteId: this.note.id,
|
||||||
});
|
});
|
||||||
|
@ -183,7 +183,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private noteReadService: NoteReadService,
|
private noteReadService: NoteReadService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private notificationService: NotificationService,
|
||||||
private relayService: RelayService,
|
private relayService: RelayService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private hashtagService: HashtagService,
|
private hashtagService: HashtagService,
|
||||||
|
@ -198,7 +198,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(user: {
|
public async create(user: {
|
||||||
|
@ -391,7 +391,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
// 投稿を作成
|
// 投稿を作成
|
||||||
try {
|
try {
|
||||||
if (insert.hasPoll) {
|
if (insert.hasPoll) {
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
await transactionalEntityManager.insert(Note, insert);
|
await transactionalEntityManager.insert(Note, insert);
|
||||||
|
|
||||||
|
@ -414,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
return insert;
|
return insert;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// duplicate key error
|
// duplicate key error
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
const err = new Error('Duplicated note');
|
const err = new Error('Duplicated note');
|
||||||
err.name = 'duplicated';
|
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);
|
await this.createMentionedEvents(mentionedUsers, note, nm);
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,36 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { User } from '@/models/entities/User.js';
|
||||||
import type { Notification } from '@/models/entities/Notification.js';
|
import type { Notification } from '@/models/entities/Notification.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { GlobalEventService } from './GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { PushNotificationService } from './PushNotificationService.js';
|
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||||
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
@Inject(DI.notificationsRepository)
|
||||||
private notificationsRepository: NotificationsRepository,
|
private notificationsRepository: NotificationsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.mutingsRepository)
|
||||||
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
private notificationEntityService: NotificationEntityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
) {
|
) {
|
||||||
|
@ -67,4 +82,93 @@ export class NotificationService {
|
||||||
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
|
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
|
||||||
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
|
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async createNotification(
|
||||||
|
notifieeId: User['id'],
|
||||||
|
type: Notification['type'],
|
||||||
|
data: Partial<Notification>,
|
||||||
|
): Promise<Notification | null> {
|
||||||
|
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<Notification>)
|
||||||
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.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 PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
|
||||||
import { emojiRegex } from '@/misc/emoji-regex.js';
|
import { emojiRegex } from '@/misc/emoji-regex.js';
|
||||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||||
|
@ -79,7 +79,7 @@ export class ReactionService {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private notificationService: NotificationService,
|
||||||
private perUserReactionsChart: PerUserReactionsChart,
|
private perUserReactionsChart: PerUserReactionsChart,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -93,19 +93,19 @@ export class ReactionService {
|
||||||
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check visibility
|
// check visibility
|
||||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
||||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '❤️';
|
reaction = '❤️';
|
||||||
} else {
|
} else {
|
||||||
// TODO: cache
|
// TODO: cache
|
||||||
reaction = await this.toDbReaction(reaction, user.host);
|
reaction = await this.toDbReaction(reaction, user.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
const record: NoteReaction = {
|
const record: NoteReaction = {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
@ -113,7 +113,7 @@ export class ReactionService {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
reaction,
|
reaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create reaction
|
// Create reaction
|
||||||
try {
|
try {
|
||||||
await this.noteReactionsRepository.insert(record);
|
await this.noteReactionsRepository.insert(record);
|
||||||
|
@ -123,7 +123,7 @@ export class ReactionService {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists.reaction !== reaction) {
|
if (exists.reaction !== reaction) {
|
||||||
// 別のリアクションがすでにされていたら置き換える
|
// 別のリアクションがすでにされていたら置き換える
|
||||||
await this.delete(user, note);
|
await this.delete(user, note);
|
||||||
|
@ -136,7 +136,7 @@ export class ReactionService {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment reactions count
|
// Increment reactions count
|
||||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
|
@ -146,12 +146,12 @@ export class ReactionService {
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
this.perUserReactionsChart.update(user, note);
|
this.perUserReactionsChart.update(user, note);
|
||||||
|
|
||||||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||||
const decodedReaction = this.decodeReaction(reaction);
|
const decodedReaction = this.decodeReaction(reaction);
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOne({
|
const emoji = await this.emojisRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
name: decodedReaction.name,
|
name: decodedReaction.name,
|
||||||
|
@ -159,7 +159,7 @@ export class ReactionService {
|
||||||
},
|
},
|
||||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||||
reaction: decodedReaction.reaction,
|
reaction: decodedReaction.reaction,
|
||||||
emoji: emoji != null ? {
|
emoji: emoji != null ? {
|
||||||
|
@ -169,16 +169,16 @@ export class ReactionService {
|
||||||
} : null,
|
} : null,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
||||||
if (note.userHost === null) {
|
if (note.userHost === null) {
|
||||||
this.createNotificationService.createNotification(note.userId, 'reaction', {
|
this.notificationService.createNotification(note.userId, 'reaction', {
|
||||||
notifierId: user.id,
|
notifierId: user.id,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note));
|
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 });
|
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
||||||
dm.addDirectRecipe(reactee as RemoteUser);
|
dm.addDirectRecipe(reactee as RemoteUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
} else if (note.visibility === 'specified') {
|
} else if (note.visibility === 'specified') {
|
||||||
|
@ -196,7 +196,7 @@ export class ReactionService {
|
||||||
dm.addDirectRecipe(u as RemoteUser);
|
dm.addDirectRecipe(u as RemoteUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
dm.execute();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -209,18 +209,18 @@ export class ReactionService {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) {
|
if (exist == null) {
|
||||||
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete reaction
|
// Delete reaction
|
||||||
const result = await this.noteReactionsRepository.delete(exist.id);
|
const result = await this.noteReactionsRepository.delete(exist.id);
|
||||||
|
|
||||||
if (result.affected !== 1) {
|
if (result.affected !== 1) {
|
||||||
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrement reactions count
|
// Decrement reactions count
|
||||||
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
||||||
await this.notesRepository.createQueryBuilder().update()
|
await this.notesRepository.createQueryBuilder().update()
|
||||||
|
@ -229,14 +229,14 @@ export class ReactionService {
|
||||||
})
|
})
|
||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
|
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
||||||
|
@ -250,7 +250,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getFallbackReaction(): Promise<string> {
|
public async getFallbackReaction(): Promise<string> {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
@ -300,7 +300,7 @@ export class ReactionService {
|
||||||
// Unicode絵文字
|
// Unicode絵文字
|
||||||
const match = emojiRegex.exec(reaction);
|
const match = emojiRegex.exec(reaction);
|
||||||
if (match) {
|
if (match) {
|
||||||
// 合字を含む1つの絵文字
|
// 合字を含む1つの絵文字
|
||||||
const unicode = match[0];
|
const unicode = match[0];
|
||||||
|
|
||||||
// 異体字セレクタ除去
|
// 異体字セレクタ除去
|
||||||
|
|
|
@ -10,7 +10,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.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 { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -57,7 +57,7 @@ export class UserFollowingService {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private createNotificationService: CreateNotificationService,
|
private notificationService: NotificationService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
|
@ -145,15 +145,15 @@ export class UserFollowingService {
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (follower.id === followee.id) return;
|
if (follower.id === followee.id) return;
|
||||||
|
|
||||||
let alreadyFollowed = false as boolean;
|
let alreadyFollowed = false as boolean;
|
||||||
|
|
||||||
await this.followingsRepository.insert({
|
await this.followingsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
|
|
||||||
// 非正規化
|
// 非正規化
|
||||||
followerHost: follower.host,
|
followerHost: follower.host,
|
||||||
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null,
|
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : null,
|
||||||
|
@ -169,35 +169,35 @@ export class UserFollowingService {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const req = await this.followRequestsRepository.findOneBy({
|
const req = await this.followRequestsRepository.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
await this.followRequestsRepository.delete({
|
await this.followRequestsRepository.delete({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.createNotificationService.createNotification(follower.id, 'followRequestAccepted', {
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
notifierId: followee.id,
|
notifierId: followee.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
//#region Increment counts
|
//#region Increment counts
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
|
||||||
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
|
||||||
]);
|
]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(i => {
|
this.federatedInstanceService.fetch(follower.host).then(i => {
|
||||||
|
@ -211,9 +211,9 @@ export class UserFollowingService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
this.perUserFollowingChart.update(follower, followee, true);
|
this.perUserFollowingChart.update(follower, followee, true);
|
||||||
|
|
||||||
// Publish follow event
|
// Publish follow event
|
||||||
if (this.userEntityService.isLocalUser(follower)) {
|
if (this.userEntityService.isLocalUser(follower)) {
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
|
@ -221,7 +221,7 @@ export class UserFollowingService {
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||||
this.globalEventService.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'follow', {
|
this.queueService.webhookDeliver(webhook, 'follow', {
|
||||||
|
@ -230,12 +230,12 @@ export class UserFollowingService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish followed event
|
// Publish followed event
|
||||||
if (this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isLocalUser(followee)) {
|
||||||
this.userEntityService.pack(follower.id, followee).then(async packed => {
|
this.userEntityService.pack(follower.id, followee).then(async packed => {
|
||||||
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
|
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'followed', {
|
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,
|
notifierId: follower.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -265,16 +265,16 @@ export class UserFollowingService {
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (following == null) {
|
if (following == null) {
|
||||||
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.followingsRepository.delete(following.id);
|
await this.followingsRepository.delete(following.id);
|
||||||
|
|
||||||
this.decrementFollowing(follower, followee);
|
this.decrementFollowing(follower, followee);
|
||||||
|
|
||||||
// Publish unfollow event
|
// Publish unfollow event
|
||||||
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
if (!silent && this.userEntityService.isLocalUser(follower)) {
|
||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
|
@ -282,7 +282,7 @@ export class UserFollowingService {
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||||
this.globalEventService.publishMainStream(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'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
for (const webhook of webhooks) {
|
for (const webhook of webhooks) {
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
||||||
|
@ -291,33 +291,33 @@ export class UserFollowingService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
||||||
// local user has null host
|
// local user has null host
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
||||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
this.queueService.deliver(followee, content, follower.inbox, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async decrementFollowing(
|
private async decrementFollowing(
|
||||||
follower: {id: User['id']; host: User['host']; },
|
follower: { id: User['id']; host: User['host']; },
|
||||||
followee: { id: User['id']; host: User['host']; },
|
followee: { id: User['id']; host: User['host']; },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
//#region Decrement following / followers counts
|
//#region Decrement following / followers counts
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
||||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
||||||
]);
|
]);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(i => {
|
this.federatedInstanceService.fetch(follower.host).then(i => {
|
||||||
|
@ -331,7 +331,7 @@ export class UserFollowingService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
this.perUserFollowingChart.update(follower, followee, false);
|
this.perUserFollowingChart.update(follower, followee, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,23 +346,23 @@ export class UserFollowingService {
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (follower.id === followee.id) return;
|
if (follower.id === followee.id) return;
|
||||||
|
|
||||||
// check blocking
|
// check blocking
|
||||||
const [blocking, blocked] = await Promise.all([
|
const [blocking, blocked] = await Promise.all([
|
||||||
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||||
this.userBlockingService.checkBlocked(followee.id, follower.id),
|
this.userBlockingService.checkBlocked(followee.id, follower.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (blocking) throw new Error('blocking');
|
if (blocking) throw new Error('blocking');
|
||||||
if (blocked) throw new Error('blocked');
|
if (blocked) throw new Error('blocked');
|
||||||
|
|
||||||
const followRequest = await this.followRequestsRepository.insert({
|
const followRequest = await this.followRequestsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
requestId,
|
requestId,
|
||||||
|
|
||||||
// 非正規化
|
// 非正規化
|
||||||
followerHost: follower.host,
|
followerHost: follower.host,
|
||||||
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined,
|
followerInbox: this.userEntityService.isRemoteUser(follower) ? follower.inbox : undefined,
|
||||||
|
@ -371,22 +371,22 @@ export class UserFollowingService {
|
||||||
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
|
||||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
|
||||||
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
// Publish receiveRequest event
|
// Publish receiveRequest event
|
||||||
if (this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isLocalUser(followee)) {
|
||||||
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
|
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
|
||||||
|
|
||||||
this.userEntityService.pack(followee.id, followee, {
|
this.userEntityService.pack(followee.id, followee, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||||
|
|
||||||
// 通知を作成
|
// 通知を作成
|
||||||
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||||
notifierId: follower.id,
|
notifierId: follower.id,
|
||||||
followRequestId: followRequest.id,
|
followRequestId: followRequest.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
|
const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee));
|
||||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||||
|
@ -404,26 +404,26 @@ export class UserFollowingService {
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.userEntityService.isRemoteUser(followee)) {
|
if (this.userEntityService.isRemoteUser(followee)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
||||||
|
|
||||||
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
|
||||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
this.queueService.deliver(follower, content, followee.inbox, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = await this.followRequestsRepository.findOneBy({
|
const request = await this.followRequestsRepository.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.followRequestsRepository.delete({
|
await this.followRequestsRepository.delete({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userEntityService.pack(followee.id, followee, {
|
this.userEntityService.pack(followee.id, followee, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||||
|
@ -440,18 +440,18 @@ export class UserFollowingService {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
|
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.insertFollowingDoc(followee, follower);
|
await this.insertFollowingDoc(followee, follower);
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
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));
|
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.queueService.deliver(followee, content, follower.inbox, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userEntityService.pack(followee.id, followee, {
|
this.userEntityService.pack(followee.id, followee, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||||
|
@ -466,13 +466,13 @@ export class UserFollowingService {
|
||||||
const requests = await this.followRequestsRepository.findBy({
|
const requests = await this.followRequestsRepository.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const request of requests) {
|
for (const request of requests) {
|
||||||
const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId });
|
const follower = await this.usersRepository.findOneByOrFail({ id: request.followerId });
|
||||||
this.acceptFollowRequest(user, follower);
|
this.acceptFollowRequest(user, follower);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API following/request/reject
|
* API following/request/reject
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { PollVotesRepository, NotesRepository } from '@/models/index.js';
|
import type { PollVotesRepository, NotesRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.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 { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type Bull from 'bull';
|
||||||
import type { EndedPollNotificationJobData } from '../types.js';
|
import type { EndedPollNotificationJobData } from '../types.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EndedPollNotificationProcessorService {
|
export class EndedPollNotificationProcessorService {
|
||||||
|
@ -23,7 +23,7 @@ export class EndedPollNotificationProcessorService {
|
||||||
@Inject(DI.pollVotesRepository)
|
@Inject(DI.pollVotesRepository)
|
||||||
private pollVotesRepository: PollVotesRepository,
|
private pollVotesRepository: PollVotesRepository,
|
||||||
|
|
||||||
private createNotificationService: CreateNotificationService,
|
private notificationService: NotificationService,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('ended-poll-notification');
|
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)])];
|
const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])];
|
||||||
|
|
||||||
for (const userId of userIds) {
|
for (const userId of userIds) {
|
||||||
this.createNotificationService.createNotification(userId, 'pollEnded', {
|
this.notificationService.createNotification(userId, 'pollEnded', {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js';
|
||||||
import { PollService } from '@/core/PollService.js';
|
import { PollService } from '@/core/PollService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
@ -89,7 +88,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private pollService: PollService,
|
private pollService: PollService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private createNotificationService: CreateNotificationService,
|
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notifications'],
|
tags: ['notifications'],
|
||||||
|
@ -27,10 +27,10 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
private createNotificationService: CreateNotificationService,
|
private notificationService: NotificationService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, user, token) => {
|
super(meta, paramDef, async (ps, user, token) => {
|
||||||
this.createNotificationService.createNotification(user.id, 'app', {
|
this.notificationService.createNotification(user.id, 'app', {
|
||||||
appAccessTokenId: token ? token.id : null,
|
appAccessTokenId: token ? token.id : null,
|
||||||
customBody: ps.body,
|
customBody: ps.body,
|
||||||
customHeader: ps.header,
|
customHeader: ps.header,
|
||||||
|
|
Loading…
Reference in a new issue