sharkey/packages/backend/src/core/GlobalEventService.ts

401 lines
12 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
2022-09-18 03:27:08 +09:00
import { Inject, Injectable } from '@nestjs/common';
2023-04-14 13:50:05 +09:00
import * as Redis from 'ioredis';
2024-01-20 13:14:46 +09:00
import * as Reversi from 'misskey-reversi';
2023-09-29 11:29:54 +09:00
import type { MiChannel } from '@/models/Channel.js';
import type { MiUser } from '@/models/User.js';
2023-09-29 11:29:54 +09:00
import type { MiUserProfile } from '@/models/UserProfile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiAntenna } from '@/models/Antenna.js';
2023-09-29 11:29:54 +09:00
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 { MiSystemWebhook } from '@/models/SystemWebhook.js';
2023-09-29 11:29:54 +09:00
import type { MiMeta } from '@/models/Meta.js';
2024-01-19 20:51:49 +09:00
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
2023-03-10 14:22:37 +09:00
import type { Packed } from '@/misc/json-schema.js';
2022-09-18 03:27:08 +09:00
import { DI } from '@/di-symbols.js';
2022-09-21 05:33:11 +09:00
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
2023-09-29 11:29:54 +09:00
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<'UserLite'>;
unfollow: Packed<'UserDetailedNotMe'>;
meUpdated: Packed<'MeDetailed'>;
2023-09-29 11:29:54 +09:00
pageEvent: {
pageId: MiPage['id'];
event: string;
var: any;
userId: MiUser['id'];
user: Packed<'UserDetailed'>;
2023-09-29 11:29:54 +09:00
};
urlUploadFinished: {
marker?: string | null;
file: Packed<'DriveFile'>;
};
readAllNotifications: undefined;
notificationFlushed: undefined;
2023-09-29 11:29:54 +09:00
unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id'];
readAllUnreadMentions: undefined;
unreadSpecifiedNote: MiNote['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllAntennas: undefined;
unreadAntenna: MiAntenna;
readAllAnnouncements: undefined;
myTokenRegenerated: undefined;
2023-10-23 09:24:13 +09:00
signin: {
id: MiSignin['id'];
2023-10-23 11:07:27 +09:00
createdAt: string;
2023-10-23 09:24:13 +09:00
ip: string;
2023-10-23 11:07:27 +09:00
headers: Record<string, any>;
2023-10-23 09:24:13 +09:00
success: boolean;
};
2023-09-29 11:29:54 +09:00
registryUpdated: {
scope?: string[];
key: string;
value: any | null;
};
driveFileCreated: Packed<'DriveFile'>;
readAntenna: MiAntenna;
receiveFollowRequest: Packed<'UserLite'>;
2023-09-29 11:29:54 +09:00
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<'UserLite'>;
userRemoved: Packed<'UserLite'>;
2023-09-29 11:29:54 +09:00
}
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;
};
}
2024-01-19 20:51:49 +09:00
export interface ReversiEventTypes {
matched: {
game: Packed<'ReversiGameDetailed'>;
};
invited: {
user: Packed<'User'>;
};
}
export interface ReversiGameEventTypes {
changeReadyStates: {
user1: boolean;
user2: boolean;
};
updateSettings: {
userId: MiUser['id'];
key: string;
value: any;
};
2024-01-20 13:14:46 +09:00
log: Reversi.Serializer.Log & { id: string | null };
2024-01-19 20:51:49 +09:00
started: {
game: Packed<'ReversiGameDetailed'>;
};
ended: {
winnerId: MiUser['id'] | null;
game: Packed<'ReversiGameDetailed'>;
};
2024-01-21 12:05:51 +09:00
canceled: {
userId: MiUser['id'];
};
2024-01-19 20:51:49 +09:00
}
2023-09-29 11:29:54 +09:00
//#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']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
2023-09-29 11:29:54 +09:00
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
remoteUserUpdated: { id: MiUser['id']; };
localUserUpdated: { id: MiUser['id']; };
2023-09-29 11:29:54 +09:00
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;
systemWebhookCreated: MiSystemWebhook;
systemWebhookDeleted: MiSystemWebhook;
systemWebhookUpdated: MiSystemWebhook;
2023-09-29 11:29:54 +09:00
antennaCreated: MiAntenna;
antennaDeleted: MiAntenna;
antennaUpdated: MiAntenna;
avatarDecorationCreated: MiAvatarDecoration;
avatarDecorationDeleted: MiAvatarDecoration;
avatarDecorationUpdated: MiAvatarDecoration;
2023-09-29 11:29:54 +09:00
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']; };
perf(federation): Ed25519署名に対応する (#13464) * 1. ed25519キーペアを発行・Personとして公開鍵を送受信 * validate additionalPublicKeys * getAuthUserFromApIdはmainを選ぶ * :v: * fix * signatureAlgorithm * set publicKeyCache lifetime * refresh * httpMessageSignatureAcceptable * ED25519_SIGNED_ALGORITHM * ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM * remove sign additionalPublicKeys signature requirements * httpMessageSignaturesSupported * httpMessageSignaturesImplementationLevel * httpMessageSignaturesImplementationLevel: '01' * perf(federation): Use hint for getAuthUserFromApId (#13470) * Hint for getAuthUserFromApId * とどのつまりこれでいいのか? * use @misskey-dev/node-http-message-signatures * fix * signedPost, signedGet * ap-request.tsを復活させる * remove digest prerender * fix test? * fix test * add httpMessageSignaturesImplementationLevel to FederationInstance * ManyToOne * fetchPersonWithRenewal * exactKey * :v: * use const * use gen-key-pair fn. from '@misskey-dev/node-http-message-signatures' * update node-http-message-signatures * fix * @misskey-dev/node-http-message-signatures@0.0.0-alpha.11 * getAuthUserFromApIdでupdatePersonの頻度を増やす * cacheRaw.date * use requiredInputs https://github.com/misskey-dev/misskey/pull/13464#discussion_r1509964359 * update @misskey-dev/node-http-message-signatures * clean up * err msg * fix(backend): fetchInstanceMetadataのLockが永遠に解除されない問題を修正 Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> * fix httpMessageSignaturesImplementationLevel validation * fix test * fix * comment * comment * improve test * fix * use Promise.all in genRSAAndEd25519KeyPair * refreshAndprepareEd25519KeyPair * refreshAndfindKey * commetn * refactor public keys add * digestプリレンダを復活させる RFC実装時にどうするか考える * fix, async * fix * !== true * use save * Deliver update person when new key generated (not tested) https://github.com/misskey-dev/misskey/pull/13464#issuecomment-1977049061 * 循環参照で落ちるのを解消? * fix? * Revert "fix?" This reverts commit 0082f6f8e8c5d5febd14933ba9a1ac643f70ca92. * a * logger * log * change logger * 秘密鍵の変更は、フラグではなく鍵を引き回すようにする * addAllKnowingSharedInboxRecipe * nanka meccha kaeta * delivre * キャッシュ有効チェックはロック取得前に行う * @misskey-dev/node-http-message-signatures@0.0.3 * PrivateKeyPem * getLocalUserPrivateKey * fix test * if * fix ap-request * update node-http-message-signatures * fix type error * update package * fix type * update package * retry no key * @misskey-dev/node-http-message-signatures@0.0.8 * fix type error * log keyid * logger * db-resolver * JSON.stringify * HTTP Signatureがなかったり使えなかったりしそうな場合にLD Signatureを活用するように * inbox-delayed use actor if no signature * ユーザーとキーの同一性チェックはhostの一致にする * log signature parse err * save array * とりあえずtryで囲っておく * fetchPersonWithRenewalでエラーが起きたら古いデータを返す * use transactionalEntityManager * fix spdx * @misskey-dev/node-http-message-signatures@0.0.10 * add comment * fix * publicKeyに配列が入ってもいいようにする https://github.com/misskey-dev/misskey/pull/13950 * define additionalPublicKeys * fix * merge fix * refreshAndprepareEd25519KeyPair → refreshAndPrepareEd25519KeyPair * remove gen-key-pair.ts * defaultMaxListeners = 512 * Revert "defaultMaxListeners = 512" This reverts commit f2c412c18057a9300540794ccbe4dfbf6d259ed6. * genRSAAndEd25519KeyPairではキーを直列に生成する? * maxConcurrency: 8 * maxConcurrency: 16 * maxConcurrency: 8 * Revert "genRSAAndEd25519KeyPairではキーを直列に生成する?" This reverts commit d0aada55c1ed5aa98f18731ec82f3ac5eb5a6c16. * maxWorkers: '90%' * Revert "maxWorkers: '90%'" This reverts commit 9e0a93f110456320d6485a871f014f7cdab29b33. * e2e/timelines.tsで個々のテストに対するtimeoutを削除, maxConcurrency: 32 * better error handling of this.userPublickeysRepository.delete * better comment * set result to keypairEntityCache * deliverJobConcurrency: 16, deliverJobPerSec: 1024, inboxJobConcurrency: 4 * inboxJobPerSec: 64 * delete request.headers['host']; * fix * // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! * move delete host * modify comment * modify comment * fix correct → collect * refreshAndfindKey → refreshAndFindKey * modify comment * modify attachLdSignature * getApId, InboxProcessorService * TODO * [skip ci] add CHANGELOG --------- Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com> Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-07-18 01:28:17 +09:00
userKeypairUpdated: { userId: MiUser['id']; };
2023-09-29 11:29:54 +09:00
}
// 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'>>;
};
2024-01-19 20:51:49 +09:00
reversi: {
name: `reversiStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
};
reversiGame: {
name: `reversiGameStream:${MiReversiGame['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
};
2023-09-29 11:29:54 +09:00
};
// 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'];
2022-09-18 03:27:08 +09:00
@Injectable()
export class GlobalEventService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.redisForPub)
private redisForPub: Redis.Redis,
2022-09-18 03:27:08 +09:00
) {
}
@bindThis
2022-09-18 03:27:08 +09:00
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({
2022-09-18 03:27:08 +09:00
channel: channel,
message: message,
}));
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
2022-12-04 17:05:32 +09:00
@bindThis
2022-09-18 03:27:08 +09:00
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishMainStream<K extends keyof MainEventTypes>(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishDriveStream<K extends keyof DriveEventTypes>(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
body: value,
});
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishUserListStream<K extends keyof UserListEventTypes>(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishAntennaStream<K extends keyof AntennaEventTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
2023-04-12 11:40:08 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishRoleTimelineStream<K extends keyof RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
2023-04-12 11:40:08 +09:00
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
2022-09-18 03:27:08 +09:00
public publishNotesStream(note: Packed<'Note'>): void {
this.publish('notesStream', null, note);
}
2022-12-04 17:05:32 +09:00
@bindThis
2023-09-29 11:29:54 +09:00
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
2022-09-18 03:27:08 +09:00
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
2024-01-19 20:51:49 +09:00
@bindThis
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
}
2022-09-18 03:27:08 +09:00
}