mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2025-01-10 05:33:09 +02:00
683b4aafb2
`useNoteCapture` already subscribes to all updates for a note, so we can tell it when a note gets replied to, too Since I'm not actually adding any extra subscription in the client, just an extra callback, there should be no overhead when replies are not coming in. Also, all the timelines already call `useNoteCapture` for each note displayed, so we know the whole `GlobalEventService` thing works fine. Many thanks to VueJS for taking care of all the DOM complications
344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
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 { 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 { MiWebhook } from '@/models/Webhook.js';
|
|
import type { MiMeta } from '@/models/Meta.js';
|
|
import { MiAvatarDecoration, 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 { 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: {
|
|
id: MiSignin['id'];
|
|
createdAt: string;
|
|
ip: string;
|
|
headers: Record<string, any>;
|
|
success: boolean;
|
|
};
|
|
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'];
|
|
};
|
|
replied: {
|
|
id: MiNote['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;
|
|
avatarDecorationCreated: MiAvatarDecoration;
|
|
avatarDecorationDeleted: MiAvatarDecoration;
|
|
avatarDecorationUpdated: MiAvatarDecoration;
|
|
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 {
|
|
constructor(
|
|
@Inject(DI.config)
|
|
private config: Config,
|
|
|
|
@Inject(DI.redisForPub)
|
|
private redisForPub: Redis.Redis,
|
|
) {
|
|
}
|
|
|
|
@bindThis
|
|
private publish(channel: StreamChannels, type: string | null, value?: any): void {
|
|
const message = type == null ? value : value == null ?
|
|
{ type: type, body: null } :
|
|
{ type: type, body: value };
|
|
|
|
this.redisForPub.publish(this.config.host, JSON.stringify({
|
|
channel: channel,
|
|
message: message,
|
|
}));
|
|
}
|
|
|
|
@bindThis
|
|
public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
|
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
|
}
|
|
|
|
@bindThis
|
|
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
|
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
|
}
|
|
|
|
@bindThis
|
|
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 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 NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
|
|
this.publish(`noteStream:${noteId}`, type, {
|
|
id: noteId,
|
|
body: value,
|
|
});
|
|
}
|
|
|
|
@bindThis
|
|
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 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 RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
|
|
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
|
}
|
|
|
|
@bindThis
|
|
public publishNotesStream(note: Packed<'Note'>): void {
|
|
this.publish('notesStream', null, note);
|
|
}
|
|
|
|
@bindThis
|
|
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);
|
|
}
|
|
}
|