summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-09-29 11:29:54 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-09-29 11:29:54 +0900
commitb9da1415a50f6036350c6453d4a681b58bf17d1e (patch)
tree06614d5be85f027890a969548ca816e6a36c8c78 /packages/backend/src
parent.js (diff)
downloadmisskey-b9da1415a50f6036350c6453d4a681b58bf17d1e.tar.gz
misskey-b9da1415a50f6036350c6453d4a681b58bf17d1e.tar.bz2
misskey-b9da1415a50f6036350c6453d4a681b58bf17d1e.zip
feat: 通知の受信設定を強化
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/AntennaService.ts4
-rw-r--r--packages/backend/src/core/CacheService.ts4
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts2
-rw-r--r--packages/backend/src/core/GlobalEventService.ts271
-rw-r--r--packages/backend/src/core/MetaService.ts4
-rw-r--r--packages/backend/src/core/NoteCreateService.ts6
-rw-r--r--packages/backend/src/core/NotificationService.ts47
-rw-r--r--packages/backend/src/core/ReactionService.ts3
-rw-r--r--packages/backend/src/core/RoleService.ts4
-rw-r--r--packages/backend/src/core/UserFollowingService.ts9
-rw-r--r--packages/backend/src/core/UserListService.ts78
-rw-r--r--packages/backend/src/core/WebhookService.ts4
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts2
-rw-r--r--packages/backend/src/models/UserProfile.ts28
-rw-r--r--packages/backend/src/models/json-schema/user.ts10
-rw-r--r--packages/backend/src/queue/processors/ImportUserListsProcessorService.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/pull.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/push.ts2
-rw-r--r--packages/backend/src/server/api/stream/Connection.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts4
-rw-r--r--packages/backend/src/server/api/stream/types.ts259
-rw-r--r--packages/backend/src/types.ts11
26 files changed, 431 insertions, 359 deletions
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 841ce4b84a..d9f27b8c63 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -15,7 +15,7 @@ import { DI } from '@/di-symbols.js';
import type { AntennasRepository, UserListJoiningsRepository } from '@/models/_.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
-import { StreamMessages } from '@/server/api/stream/types.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@@ -50,7 +50,7 @@ export class AntennaService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
- const { type, body } = obj.message as StreamMessages['internal']['payload'];
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'antennaCreated':
this.antennas.push({
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index 6ca684d53c..561979c4bf 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -11,7 +11,7 @@ import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
-import { StreamMessages } from '@/server/api/stream/types.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@@ -160,7 +160,7 @@ export class CacheService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
- const { type, body } = obj.message as StreamMessages['internal']['payload'];
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'userChangeSuspendedState':
case 'remoteUserUpdated': {
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 1b545a124e..9661a0aea3 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import { query } from '@/misc/prelude/url.js';
-import type { Serialized } from '@/server/api/stream/types.js';
+import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 4bc4f54c21..b74fbbe584 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -5,27 +5,254 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
+import type { MiChannel } from '@/models/Channel.js';
import type { MiUser } from '@/models/User.js';
+import type { MiUserProfile } from '@/models/UserProfile.js';
import type { MiNote } from '@/models/Note.js';
-import type { MiUserList } from '@/models/UserList.js';
import type { MiAntenna } from '@/models/Antenna.js';
-import type {
- StreamChannels,
- AdminStreamTypes,
- AntennaStreamTypes,
- BroadcastTypes,
- DriveStreamTypes,
- InternalStreamTypes,
- MainStreamTypes,
- NoteStreamTypes,
- UserListStreamTypes,
- RoleTimelineStreamTypes,
-} from '@/server/api/stream/types.js';
+import type { MiDriveFile } from '@/models/DriveFile.js';
+import type { MiDriveFolder } from '@/models/DriveFolder.js';
+import type { MiUserList } from '@/models/UserList.js';
+import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
+import type { MiSignin } from '@/models/Signin.js';
+import type { MiPage } from '@/models/Page.js';
+import type { MiWebhook } from '@/models/Webhook.js';
+import type { MiMeta } from '@/models/Meta.js';
+import { MiRole, MiRoleAssignment } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
-import { MiRole } from '@/models/_.js';
+import { Serialized } from '@/types.js';
+import type Emitter from 'strict-event-emitter-types';
+import type { EventEmitter } from 'events';
+
+//#region Stream type-body definitions
+export interface BroadcastTypes {
+ emojiAdded: {
+ emoji: Packed<'EmojiDetailed'>;
+ };
+ emojiUpdated: {
+ emojis: Packed<'EmojiDetailed'>[];
+ };
+ emojiDeleted: {
+ emojis: {
+ id?: string;
+ name: string;
+ [other: string]: any;
+ }[];
+ };
+ announcementCreated: {
+ announcement: Packed<'Announcement'>;
+ };
+}
+
+export interface MainEventTypes {
+ notification: Packed<'Notification'>;
+ mention: Packed<'Note'>;
+ reply: Packed<'Note'>;
+ renote: Packed<'Note'>;
+ follow: Packed<'UserDetailedNotMe'>;
+ followed: Packed<'User'>;
+ unfollow: Packed<'User'>;
+ meUpdated: Packed<'User'>;
+ pageEvent: {
+ pageId: MiPage['id'];
+ event: string;
+ var: any;
+ userId: MiUser['id'];
+ user: Packed<'User'>;
+ };
+ urlUploadFinished: {
+ marker?: string | null;
+ file: Packed<'DriveFile'>;
+ };
+ readAllNotifications: undefined;
+ unreadNotification: Packed<'Notification'>;
+ unreadMention: MiNote['id'];
+ readAllUnreadMentions: undefined;
+ unreadSpecifiedNote: MiNote['id'];
+ readAllUnreadSpecifiedNotes: undefined;
+ readAllAntennas: undefined;
+ unreadAntenna: MiAntenna;
+ readAllAnnouncements: undefined;
+ myTokenRegenerated: undefined;
+ signin: MiSignin;
+ registryUpdated: {
+ scope?: string[];
+ key: string;
+ value: any | null;
+ };
+ driveFileCreated: Packed<'DriveFile'>;
+ readAntenna: MiAntenna;
+ receiveFollowRequest: Packed<'User'>;
+ announcementCreated: {
+ announcement: Packed<'Announcement'>;
+ };
+}
+
+export interface DriveEventTypes {
+ fileCreated: Packed<'DriveFile'>;
+ fileDeleted: MiDriveFile['id'];
+ fileUpdated: Packed<'DriveFile'>;
+ folderCreated: Packed<'DriveFolder'>;
+ folderDeleted: MiDriveFolder['id'];
+ folderUpdated: Packed<'DriveFolder'>;
+}
+
+export interface NoteEventTypes {
+ pollVoted: {
+ choice: number;
+ userId: MiUser['id'];
+ };
+ deleted: {
+ deletedAt: Date;
+ };
+ updated: {
+ cw: string | null;
+ text: string;
+ };
+ reacted: {
+ reaction: string;
+ emoji?: {
+ name: string;
+ url: string;
+ } | null;
+ userId: MiUser['id'];
+ };
+ unreacted: {
+ reaction: string;
+ userId: MiUser['id'];
+ };
+}
+type NoteStreamEventTypes = {
+ [key in keyof NoteEventTypes]: {
+ id: MiNote['id'];
+ body: NoteEventTypes[key];
+ };
+};
+
+export interface UserListEventTypes {
+ userAdded: Packed<'User'>;
+ userRemoved: Packed<'User'>;
+}
+
+export interface AntennaEventTypes {
+ note: MiNote;
+}
+
+export interface RoleTimelineEventTypes {
+ note: Packed<'Note'>;
+}
+
+export interface AdminEventTypes {
+ newAbuseUserReport: {
+ id: MiAbuseUserReport['id'];
+ targetUserId: MiUser['id'],
+ reporterId: MiUser['id'],
+ comment: string;
+ };
+}
+//#endregion
+
+// 辞書(interface or type)から{ type, body }ユニオンを定義
+// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
+// VS Codeの展開を防止するためにEvents型を定義
+type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
+type EventUnionFromDictionary<
+ T extends object,
+ U = Events<T>
+> = U[keyof U];
+
+type SerializedAll<T> = {
+ [K in keyof T]: Serialized<T[K]>;
+};
+
+export interface InternalEventTypes {
+ userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
+ userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
+ remoteUserUpdated: { id: MiUser['id']; };
+ follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
+ unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
+ blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
+ blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
+ policiesUpdated: MiRole['policies'];
+ roleCreated: MiRole;
+ roleDeleted: MiRole;
+ roleUpdated: MiRole;
+ userRoleAssigned: MiRoleAssignment;
+ userRoleUnassigned: MiRoleAssignment;
+ webhookCreated: MiWebhook;
+ webhookDeleted: MiWebhook;
+ webhookUpdated: MiWebhook;
+ antennaCreated: MiAntenna;
+ antennaDeleted: MiAntenna;
+ antennaUpdated: MiAntenna;
+ metaUpdated: MiMeta;
+ followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
+ unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
+ updateUserProfile: MiUserProfile;
+ mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
+ unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
+ userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
+ userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
+}
+
+// name/messages(spec) pairs dictionary
+export type GlobalEvents = {
+ internal: {
+ name: 'internal';
+ payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>;
+ };
+ broadcast: {
+ name: 'broadcast';
+ payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
+ };
+ main: {
+ name: `mainStream:${MiUser['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>;
+ };
+ drive: {
+ name: `driveStream:${MiUser['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>;
+ };
+ note: {
+ name: `noteStream:${MiNote['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
+ };
+ userList: {
+ name: `userListStream:${MiUserList['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>;
+ };
+ roleTimeline: {
+ name: `roleTimelineStream:${MiRole['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>;
+ };
+ antenna: {
+ name: `antennaStream:${MiAntenna['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>;
+ };
+ admin: {
+ name: `adminStream:${MiUser['id']}`;
+ payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>;
+ };
+ notes: {
+ name: 'notesStream';
+ payload: Serialized<Packed<'Note'>>;
+ };
+};
+
+// API event definitions
+// ストリームごとのEmitterの辞書を用意
+type EventEmitterDictionary = { [x in keyof GlobalEvents]: Emitter.default<EventEmitter, { [y in GlobalEvents[x]['name']]: (e: GlobalEvents[x]['payload']) => void }> };
+// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
+type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
+export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof GlobalEvents]>;
+// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
+
+// provide stream channels union
+export type StreamChannels = GlobalEvents[keyof GlobalEvents]['name'];
@Injectable()
export class GlobalEventService {
@@ -51,7 +278,7 @@ export class GlobalEventService {
}
@bindThis
- public publishInternalEvent<K extends keyof InternalStreamTypes>(type: K, value?: InternalStreamTypes[K]): void {
+ public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
@@ -61,17 +288,17 @@ export class GlobalEventService {
}
@bindThis
- public publishMainStream<K extends keyof MainStreamTypes>(userId: MiUser['id'], type: K, value?: MainStreamTypes[K]): void {
+ public publishMainStream<K extends keyof MainEventTypes>(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void {
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
- public publishDriveStream<K extends keyof DriveStreamTypes>(userId: MiUser['id'], type: K, value?: DriveStreamTypes[K]): void {
+ public publishDriveStream<K extends keyof DriveEventTypes>(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void {
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
- public publishNoteStream<K extends keyof NoteStreamTypes>(noteId: MiNote['id'], type: K, value?: NoteStreamTypes[K]): void {
+ public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
body: value,
@@ -79,17 +306,17 @@ export class GlobalEventService {
}
@bindThis
- public publishUserListStream<K extends keyof UserListStreamTypes>(listId: MiUserList['id'], type: K, value?: UserListStreamTypes[K]): void {
+ public publishUserListStream<K extends keyof UserListEventTypes>(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void {
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
- public publishAntennaStream<K extends keyof AntennaStreamTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaStreamTypes[K]): void {
+ public publishAntennaStream<K extends keyof AntennaEventTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void {
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
- public publishRoleTimelineStream<K extends keyof RoleTimelineStreamTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineStreamTypes[K]): void {
+ public publishRoleTimelineStream<K extends keyof RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
}
@@ -99,7 +326,7 @@ export class GlobalEventService {
}
@bindThis
- public publishAdminStream<K extends keyof AdminStreamTypes>(userId: MiUser['id'], type: K, value?: AdminStreamTypes[K]): void {
+ public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
}
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index 00e1e3c1fc..508544dc07 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
import { MiMeta } from '@/models/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
-import { StreamMessages } from '@/server/api/stream/types.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@@ -46,7 +46,7 @@ export class MetaService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
- const { type, body } = obj.message as StreamMessages['internal']['payload'];
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'metaUpdated': {
this.cache = body;
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 972319ddcf..f20727ce41 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -110,9 +110,8 @@ class NotificationManager {
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
this.notificationService.createNotification(x.target, x.reason, {
- notifierId: this.notifier.id,
noteId: this.note.id,
- });
+ }, this.notifier.id);
}
}
}
@@ -515,9 +514,8 @@ export class NoteCreateService implements OnApplicationShutdown {
}).then(followings => {
for (const following of followings) {
this.notificationService.createNotification(following.followerId, 'note', {
- notifierId: user.id,
noteId: note.id,
- });
+ }, user.id);
}
});
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index 258ae44f7d..ba8798f181 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -18,6 +18,7 @@ import { NotificationEntityService } from '@/core/entities/NotificationEntitySer
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
+import { UserListService } from '@/core/UserListService.js';
@Injectable()
export class NotificationService implements OnApplicationShutdown {
@@ -38,6 +39,7 @@ export class NotificationService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private pushNotificationService: PushNotificationService,
private cacheService: CacheService,
+ private userListService: UserListService,
) {
}
@@ -74,27 +76,56 @@ export class NotificationService implements OnApplicationShutdown {
public async createNotification(
notifieeId: MiUser['id'],
type: MiNotification['type'],
- data: Partial<MiNotification>,
+ data: Omit<Partial<MiNotification>, 'notifierId'>,
+ notifierId?: MiUser['id'] | null,
): Promise<MiNotification | null> {
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
- const isMuted = profile.mutingNotificationTypes.includes(type);
- if (isMuted) return null;
+ const recieveConfig = profile.notificationRecieveConfig[type];
+ if (recieveConfig?.type === 'never') {
+ return null;
+ }
- if (data.notifierId) {
- if (notifieeId === data.notifierId) {
+ if (notifierId) {
+ if (notifieeId === notifierId) {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
- if (mutings.has(data.notifierId)) {
+ if (mutings.has(notifierId)) {
return null;
}
+
+ if (recieveConfig?.type === 'following') {
+ const isFollowing = await this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId));
+ if (!isFollowing) {
+ return null;
+ }
+ } else if (recieveConfig?.type === 'follower') {
+ const isFollower = await this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId));
+ if (!isFollower) {
+ return null;
+ }
+ } else if (recieveConfig?.type === 'mutualFollow') {
+ const [isFollowing, isFollower] = await Promise.all([
+ this.cacheService.userFollowingsCache.fetch(notifieeId).then(followings => followings.has(notifierId)),
+ this.cacheService.userFollowingsCache.fetch(notifierId).then(followings => followings.has(notifieeId)),
+ ]);
+ if (!isFollowing && !isFollower) {
+ return null;
+ }
+ } else if (recieveConfig?.type === 'list') {
+ const isMember = await this.userListService.membersCache.fetch(recieveConfig.userListId).then(members => members.has(notifierId));
+ if (!isMember) {
+ return null;
+ }
+ }
}
const notification = {
id: this.idService.genId(),
createdAt: new Date(),
type: type,
+ notifierId: notifierId,
...data,
} as MiNotification;
@@ -117,8 +148,8 @@ export class NotificationService implements OnApplicationShutdown {
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! }));
+ if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
+ if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
}, () => { /* aborted, ignore it */ });
return notification;
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index d9bde502c8..25464b19a8 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -219,10 +219,9 @@ export class ReactionService {
// リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) {
this.notificationService.createNotification(note.userId, 'reaction', {
- notifierId: user.id,
noteId: note.id,
reaction: reaction,
- });
+ }, user.id);
}
//#region 配信
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index ec4d804219..c3445abc53 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -15,7 +15,7 @@ import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { StreamMessages } from '@/server/api/stream/types.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -116,7 +116,7 @@ export class RoleService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
- const { type, body } = obj.message as StreamMessages['internal']['payload'];
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'roleCreated': {
const cached = this.rolesCache.get();
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 5b2b0205d9..230f6ef261 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -230,8 +230,7 @@ export class UserFollowingService implements OnModuleInit {
// 通知を作成
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
- notifierId: followee.id,
- });
+ }, followee.id);
}
if (alreadyFollowed) return;
@@ -304,8 +303,7 @@ export class UserFollowingService implements OnModuleInit {
// 通知を作成
this.notificationService.createNotification(followee.id, 'follow', {
- notifierId: follower.id,
- });
+ }, follower.id);
}
}
@@ -488,9 +486,8 @@ export class UserFollowingService implements OnModuleInit {
// 通知を作成
this.notificationService.createNotification(followee.id, 'receiveFollowRequest', {
- notifierId: follower.id,
followRequestId: followRequest.id,
- });
+ }, follower.id);
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts
index a71d50bba5..93dc5edbba 100644
--- a/packages/backend/src/core/UserListService.ts
+++ b/packages/backend/src/core/UserListService.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
+import * as Redis from 'ioredis';
import type { UserListJoiningsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js';
@@ -16,12 +17,22 @@ import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
+import { RedisKVCache } from '@/misc/cache.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
@Injectable()
-export class UserListService {
+export class UserListService implements OnApplicationShutdown {
public static TooManyUsersError = class extends Error {};
+ public membersCache: RedisKVCache<Set<string>>;
+
constructor(
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
@@ -32,10 +43,48 @@ export class UserListService {
private proxyAccountService: ProxyAccountService,
private queueService: QueueService,
) {
+ this.membersCache = new RedisKVCache<Set<string>>(this.redisClient, 'userListMembers', {
+ lifetime: 1000 * 60 * 30, // 30m
+ memoryCacheLifetime: 1000 * 60, // 1m
+ fetcher: (key) => this.userListJoiningsRepository.find({ where: { userListId: key }, select: ['userId'] }).then(xs => new Set(xs.map(x => x.userId))),
+ toRedisConverter: (value) => JSON.stringify(Array.from(value)),
+ fromRedisConverter: (value) => new Set(JSON.parse(value)),
+ });
+
+ this.redisForSub.on('message', this.onMessage);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise<void> {
+ const obj = JSON.parse(data);
+
+ if (obj.channel === 'internal') {
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
+ switch (type) {
+ case 'userListMemberAdded': {
+ const { userListId, memberId } = body;
+ const members = await this.membersCache.get(userListId);
+ if (members) {
+ members.add(memberId);
+ }
+ break;
+ }
+ case 'userListMemberRemoved': {
+ const { userListId, memberId } = body;
+ const members = await this.membersCache.get(userListId);
+ if (members) {
+ members.delete(memberId);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
}
@bindThis
- public async push(target: MiUser, list: MiUserList, me: MiUser) {
+ public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
const currentCount = await this.userListJoiningsRepository.countBy({
userListId: list.id,
});
@@ -50,6 +99,7 @@ export class UserListService {
userListId: list.id,
} as MiUserListJoining);
+ this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
@@ -60,4 +110,26 @@ export class UserListService {
}
}
}
+
+ @bindThis
+ public async removeMember(target: MiUser, list: MiUserList) {
+ await this.userListJoiningsRepository.delete({
+ userId: target.id,
+ userListId: list.id,
+ });
+
+ this.globalEventService.publishInternalEvent('userListMemberRemoved', { userListId: list.id, memberId: target.id });
+ this.globalEventService.publishUserListStream(list.id, 'userRemoved', await this.userEntityService.pack(target));
+ }
+
+ @bindThis
+ public dispose(): void {
+ this.redisForSub.off('message', this.onMessage);
+ this.membersCache.dispose();
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined): void {
+ this.dispose();
+ }
}
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
index 1344f0ac97..ff70f7bc0c 100644
--- a/packages/backend/src/core/WebhookService.ts
+++ b/packages/backend/src/core/WebhookService.ts
@@ -9,7 +9,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import type { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
-import { StreamMessages } from '@/server/api/stream/types.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@@ -45,7 +45,7 @@ export class WebhookService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
- const { type, body } = obj.message as StreamMessages['internal']['payload'];
+ const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'webhookCreated':
if (body.active) {
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 3dd64ce625..47fc98f479 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -452,7 +452,7 @@ export class UserEntityService implements OnModuleInit {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile!.mutedWords,
mutedInstances: profile!.mutedInstances,
- mutingNotificationTypes: profile!.mutingNotificationTypes,
+ notificationRecieveConfig: profile!.notificationRecieveConfig,
emailNotificationTypes: profile!.emailNotificationTypes,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index e4405c9da7..d6d85c5609 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -8,6 +8,7 @@ import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/ty
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';
+import { MiUserList } from './UserList.js';
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
@@ -222,16 +223,25 @@ export class MiUserProfile {
})
public mutedInstances: string[];
- @Column('enum', {
- enum: [
- ...notificationTypes,
- // マイグレーションで削除が困難なので古いenumは残しておく
- ...obsoleteNotificationTypes,
- ],
- array: true,
- default: [],
+ @Column('jsonb', {
+ default: {},
})
- public mutingNotificationTypes: typeof notificationTypes[number][];
+ public notificationRecieveConfig: {
+ [notificationType in typeof notificationTypes[number]]?: {
+ type: 'all';
+ } | {
+ type: 'never';
+ } | {
+ type: 'following';
+ } | {
+ type: 'follower';
+ } | {
+ type: 'mutualFollow';
+ } | {
+ type: 'list';
+ userListId: MiUserList['id'];
+ };
+ };
@Column('varchar', {
length: 32, array: true, default: '{}',
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index f15b225a30..0181ea50e8 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -387,13 +387,9 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false,
},
},
- mutingNotificationTypes: {
- type: 'array',
- nullable: true, optional: false,
- items: {
- type: 'string',
- nullable: false, optional: false,
- },
+ notificationRecieveConfig: {
+ type: 'object',
+ nullable: false, optional: false,
},
emailNotificationTypes: {
type: 'array',
diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
index 54ca1a86df..60a0d1605f 100644
--- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts
@@ -101,7 +101,7 @@ export class ImportUserListsProcessorService {
if (await this.userListJoiningsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
- this.userListService.push(target, list!, user);
+ this.userListService.addMember(target, list!, user);
} catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`);
}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index e065b99e93..3454597532 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
- mutingNotificationTypes: profile.mutingNotificationTypes,
+ notificationRecieveConfig: profile.notificationRecieveConfig,
isModerator: isModerator,
isSilenced: isSilenced,
isSuspended: user.isSuspended,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index b11e091957..431bb4c60a 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -165,9 +165,7 @@ export const paramDef = {
mutedInstances: { type: 'array', items: {
type: 'string',
} },
- mutingNotificationTypes: { type: 'array', items: {
- type: 'string', enum: notificationTypes,
- } },
+ notificationRecieveConfig: { type: 'object' },
emailNotificationTypes: { type: 'array', items: {
type: 'string',
} },
@@ -248,7 +246,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
- if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
+ if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
index fd1bb48a4e..eae55905d3 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -144,7 +144,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
try {
- await this.userListService.push(currentUser, userList, me);
+ await this.userListService.addMember(currentUser, userList, me);
} catch (err) {
if (err instanceof UserListService.TooManyUsersError) {
throw new ApiError(meta.errors.tooManyUsers);
diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
index 0b01061740..e90122224c 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
@@ -4,12 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import type { UserListsRepository, UserListJoiningsRepository } from '@/models/_.js';
+import type { UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GetterService } from '@/server/api/GetterService.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { UserListService } from '@/core/UserListService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -53,12 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
- @Inject(DI.userListJoiningsRepository)
- private userListJoiningsRepository: UserListJoiningsRepository,
-
- private userEntityService: UserEntityService,
+ private userListService: UserListService,
private getterService: GetterService,
- private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the list
@@ -77,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
- // Pull the user
- await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id });
-
- this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user));
+ await this.userListService.removeMember(user, userList);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts
index 9bb1a71f58..72a6a7380d 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/push.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts
@@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
try {
- await this.userListService.push(user, userList, me);
+ await this.userListService.addMember(user, userList, me);
} catch (err) {
if (err instanceof UserListService.TooManyUsersError) {
throw new ApiError(meta.errors.tooManyUsers);
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index fd91681fc1..a73071ea99 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -12,10 +12,10 @@ import type { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiUserProfile } from '@/models/_.js';
+import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
-import type { StreamEventEmitter, StreamMessages } from './types.js';
/**
* Main stream connection
@@ -122,7 +122,7 @@ export default class Connection {
}
@bindThis
- private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) {
+ private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) {
this.sendMessageToWs(data.type, data.body);
}
@@ -196,7 +196,7 @@ export default class Connection {
}
@bindThis
- private async onNoteStreamMessage(data: StreamMessages['note']['payload']) {
+ private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) {
this.sendMessageToWs('noteUpdated', {
id: data.body.id,
type: data.type,
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 87648a3a77..a48e6ba5c6 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -7,8 +7,8 @@ import { Injectable } from '@nestjs/common';
import { isUserRelated } from '@/misc/is-user-related.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel from '../channel.js';
-import type { StreamMessages } from '../types.js';
class AntennaChannel extends Channel {
public readonly chName = 'antenna';
@@ -35,7 +35,7 @@ class AntennaChannel extends Channel {
}
@bindThis
- private async onEvent(data: StreamMessages['antenna']['payload']) {
+ private async onEvent(data: GlobalEvents['antenna']['payload']) {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 76b5875343..38d3604cc5 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -9,8 +9,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel from '../channel.js';
-import { StreamMessages } from '../types.js';
class RoleTimelineChannel extends Channel {
public readonly chName = 'roleTimeline';
@@ -37,7 +37,7 @@ class RoleTimelineChannel extends Channel {
}
@bindThis
- private async onEvent(data: StreamMessages['roleTimeline']['payload']) {
+ private async onEvent(data: GlobalEvents['roleTimeline']['payload']) {
if (data.type === 'note') {
const note = data.body;
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
deleted file mode 100644
index 2436750cd6..0000000000
--- a/packages/backend/src/server/api/stream/types.ts
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { MiChannel } from '@/models/Channel.js';
-import type { MiUser } from '@/models/User.js';
-import type { MiUserProfile } from '@/models/UserProfile.js';
-import type { MiNote } from '@/models/Note.js';
-import type { MiAntenna } from '@/models/Antenna.js';
-import type { MiDriveFile } from '@/models/DriveFile.js';
-import type { MiDriveFolder } from '@/models/DriveFolder.js';
-import type { MiUserList } from '@/models/UserList.js';
-import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
-import type { MiSignin } from '@/models/Signin.js';
-import type { MiPage } from '@/models/Page.js';
-import type { Packed } from '@/misc/json-schema.js';
-import type { MiWebhook } from '@/models/Webhook.js';
-import type { MiMeta } from '@/models/Meta.js';
-import { MiRole, MiRoleAssignment } from '@/models/_.js';
-import type Emitter from 'strict-event-emitter-types';
-import type { EventEmitter } from 'events';
-
-//#region Stream type-body definitions
-export interface InternalStreamTypes {
- userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
- userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
- remoteUserUpdated: { id: MiUser['id']; };
- follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
- unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
- blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
- blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
- policiesUpdated: MiRole['policies'];
- roleCreated: MiRole;
- roleDeleted: MiRole;
- roleUpdated: MiRole;
- userRoleAssigned: MiRoleAssignment;
- userRoleUnassigned: MiRoleAssignment;
- webhookCreated: MiWebhook;
- webhookDeleted: MiWebhook;
- webhookUpdated: MiWebhook;
- antennaCreated: MiAntenna;
- antennaDeleted: MiAntenna;
- antennaUpdated: MiAntenna;
- metaUpdated: MiMeta;
- followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
- unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
- updateUserProfile: MiUserProfile;
- mute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
- unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
-}
-
-export interface BroadcastTypes {
- emojiAdded: {
- emoji: Packed<'EmojiDetailed'>;
- };
- emojiUpdated: {
- emojis: Packed<'EmojiDetailed'>[];
- };
- emojiDeleted: {
- emojis: {
- id?: string;
- name: string;
- [other: string]: any;
- }[];
- };
- announcementCreated: {
- announcement: Packed<'Announcement'>;
- };
-}
-
-export interface MainStreamTypes {
- notification: Packed<'Notification'>;
- mention: Packed<'Note'>;
- reply: Packed<'Note'>;
- renote: Packed<'Note'>;
- follow: Packed<'UserDetailedNotMe'>;
- followed: Packed<'User'>;
- unfollow: Packed<'User'>;
- meUpdated: Packed<'User'>;
- pageEvent: {
- pageId: MiPage['id'];
- event: string;
- var: any;
- userId: MiUser['id'];
- user: Packed<'User'>;
- };
- urlUploadFinished: {
- marker?: string | null;
- file: Packed<'DriveFile'>;
- };
- readAllNotifications: undefined;
- unreadNotification: Packed<'Notification'>;
- unreadMention: MiNote['id'];
- readAllUnreadMentions: undefined;
- unreadSpecifiedNote: MiNote['id'];
- readAllUnreadSpecifiedNotes: undefined;
- readAllAntennas: undefined;
- unreadAntenna: MiAntenna;
- readAllAnnouncements: undefined;
- myTokenRegenerated: undefined;
- signin: MiSignin;
- registryUpdated: {
- scope?: string[];
- key: string;
- value: any | null;
- };
- driveFileCreated: Packed<'DriveFile'>;
- readAntenna: MiAntenna;
- receiveFollowRequest: Packed<'User'>;
- announcementCreated: {
- announcement: Packed<'Announcement'>;
- };
-}
-
-export interface DriveStreamTypes {
- fileCreated: Packed<'DriveFile'>;
- fileDeleted: MiDriveFile['id'];
- fileUpdated: Packed<'DriveFile'>;
- folderCreated: Packed<'DriveFolder'>;
- folderDeleted: MiDriveFolder['id'];
- folderUpdated: Packed<'DriveFolder'>;
-}
-
-export interface NoteStreamTypes {
- pollVoted: {
- choice: number;
- userId: MiUser['id'];
- };
- deleted: {
- deletedAt: Date;
- };
- updated: {
- cw: string | null;
- text: string;
- };
- reacted: {
- reaction: string;
- emoji?: {
- name: string;
- url: string;
- } | null;
- userId: MiUser['id'];
- };
- unreacted: {
- reaction: string;
- userId: MiUser['id'];
- };
-}
-type NoteStreamEventTypes = {
- [key in keyof NoteStreamTypes]: {
- id: MiNote['id'];
- body: NoteStreamTypes[key];
- };
-};
-
-export interface UserListStreamTypes {
- userAdded: Packed<'User'>;
- userRemoved: Packed<'User'>;
-}
-
-export interface AntennaStreamTypes {
- note: MiNote;
-}
-
-export interface RoleTimelineStreamTypes {
- note: Packed<'Note'>;
-}
-
-export interface AdminStreamTypes {
- newAbuseUserReport: {
- id: MiAbuseUserReport['id'];
- targetUserId: MiUser['id'],
- reporterId: MiUser['id'],
- comment: string;
- };
-}
-//#endregion
-
-// 辞書(interface or type)から{ type, body }ユニオンを定義
-// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
-// VS Codeの展開を防止するためにEvents型を定義
-type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
-type EventUnionFromDictionary<
- T extends object,
- U = Events<T>
-> = U[keyof U];
-
-// redis通すとDateのインスタンスはstringに変換されるので
-export type Serialized<T> = {
- [K in keyof T]:
- T[K] extends Date
- ? string
- : T[K] extends (Date | null)
- ? (string | null)
- : T[K] extends Record<string, any>
- ? Serialized<T[K]>
- : T[K];
-};
-
-type SerializedAll<T> = {
- [K in keyof T]: Serialized<T[K]>;
-};
-
-// name/messages(spec) pairs dictionary
-export type StreamMessages = {
- internal: {
- name: 'internal';
- payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>;
- };
- broadcast: {
- name: 'broadcast';
- payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>;
- };
- main: {
- name: `mainStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>;
- };
- drive: {
- name: `driveStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>;
- };
- note: {
- name: `noteStream:${MiNote['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
- };
- userList: {
- name: `userListStream:${MiUserList['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
- };
- roleTimeline: {
- name: `roleTimelineStream:${MiRole['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<RoleTimelineStreamTypes>>;
- };
- antenna: {
- name: `antennaStream:${MiAntenna['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
- };
- admin: {
- name: `adminStream:${MiUser['id']}`;
- payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;
- };
- notes: {
- name: 'notesStream';
- payload: Serialized<Packed<'Note'>>;
- };
-};
-
-// API event definitions
-// ストリームごとのEmitterの辞書を用意
-type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> };
-// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
-type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
-// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
-export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
-// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
-
-// provide stream channels union
-export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 7b928263ab..9e06d30aac 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -203,3 +203,14 @@ export type ModerationLogPayloads = {
invitations: any[];
};
};
+
+export type Serialized<T> = {
+ [K in keyof T]:
+ T[K] extends Date
+ ? string
+ : T[K] extends (Date | null)
+ ? (string | null)
+ : T[K] extends Record<string, any>
+ ? Serialized<T[K]>
+ : T[K];
+};