2023-07-27 07:31:52 +02:00
|
|
|
/*
|
2024-02-13 16:59:27 +01:00
|
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
2023-07-27 07:31:52 +02:00
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
2023-04-14 06:50:05 +02:00
|
|
|
import * as Redis from 'ioredis';
|
2024-01-20 05:14:46 +01:00
|
|
|
import * as Reversi from 'misskey-reversi';
|
2023-09-29 04:29:54 +02:00
|
|
|
import type { MiChannel } from '@/models/Channel.js';
|
2023-09-20 04:33:36 +02:00
|
|
|
import type { MiUser } from '@/models/User.js';
|
2023-09-29 04:29:54 +02:00
|
|
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
2023-09-20 04:33:36 +02:00
|
|
|
import type { MiNote } from '@/models/Note.js';
|
|
|
|
import type { MiAntenna } from '@/models/Antenna.js';
|
2023-09-29 04:29:54 +02: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 { MiMeta } from '@/models/Meta.js';
|
2024-01-19 12:51:49 +01:00
|
|
|
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
|
2023-03-10 06:22:37 +01:00
|
|
|
import type { Packed } from '@/misc/json-schema.js';
|
2022-09-17 20:27:08 +02:00
|
|
|
import { DI } from '@/di-symbols.js';
|
2022-09-20 22:33:11 +02:00
|
|
|
import type { Config } from '@/config.js';
|
2022-12-04 07:03:09 +01:00
|
|
|
import { bindThis } from '@/decorators.js';
|
2023-09-29 04:29:54 +02: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'>;
|
2024-01-31 07:45:35 +01:00
|
|
|
followed: Packed<'UserLite'>;
|
|
|
|
unfollow: Packed<'UserDetailedNotMe'>;
|
|
|
|
meUpdated: Packed<'MeDetailed'>;
|
2023-09-29 04:29:54 +02:00
|
|
|
pageEvent: {
|
|
|
|
pageId: MiPage['id'];
|
|
|
|
event: string;
|
|
|
|
var: any;
|
|
|
|
userId: MiUser['id'];
|
2024-01-31 07:45:35 +01:00
|
|
|
user: Packed<'UserDetailed'>;
|
2023-09-29 04:29:54 +02:00
|
|
|
};
|
|
|
|
urlUploadFinished: {
|
|
|
|
marker?: string | null;
|
|
|
|
file: Packed<'DriveFile'>;
|
|
|
|
};
|
|
|
|
readAllNotifications: undefined;
|
2024-02-29 12:03:30 +01:00
|
|
|
notificationFlushed: undefined;
|
2023-09-29 04:29:54 +02: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 02:24:13 +02:00
|
|
|
signin: {
|
|
|
|
id: MiSignin['id'];
|
2023-10-23 04:07:27 +02:00
|
|
|
createdAt: string;
|
2023-10-23 02:24:13 +02:00
|
|
|
ip: string;
|
2023-10-23 04:07:27 +02:00
|
|
|
headers: Record<string, any>;
|
2023-10-23 02:24:13 +02:00
|
|
|
success: boolean;
|
|
|
|
};
|
2023-09-29 04:29:54 +02:00
|
|
|
registryUpdated: {
|
|
|
|
scope?: string[];
|
|
|
|
key: string;
|
|
|
|
value: any | null;
|
|
|
|
};
|
|
|
|
driveFileCreated: Packed<'DriveFile'>;
|
|
|
|
readAntenna: MiAntenna;
|
2024-01-31 07:45:35 +01:00
|
|
|
receiveFollowRequest: Packed<'UserLite'>;
|
2023-09-29 04:29:54 +02: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 {
|
2024-01-31 07:45:35 +01:00
|
|
|
userAdded: Packed<'UserLite'>;
|
|
|
|
userRemoved: Packed<'UserLite'>;
|
2023-09-29 04:29:54 +02: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 12:51:49 +01: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 05:14:46 +01:00
|
|
|
log: Reversi.Serializer.Log & { id: string | null };
|
2024-01-19 12:51:49 +01:00
|
|
|
started: {
|
|
|
|
game: Packed<'ReversiGameDetailed'>;
|
|
|
|
};
|
|
|
|
ended: {
|
|
|
|
winnerId: MiUser['id'] | null;
|
|
|
|
game: Packed<'ReversiGameDetailed'>;
|
|
|
|
};
|
2024-01-21 04:05:51 +01:00
|
|
|
canceled: {
|
|
|
|
userId: MiUser['id'];
|
|
|
|
};
|
2024-01-19 12:51:49 +01:00
|
|
|
}
|
2023-09-29 04:29:54 +02: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']; };
|
2024-02-23 10:15:39 +01:00
|
|
|
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
|
2023-09-29 04:29:54 +02:00
|
|
|
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
|
|
|
remoteUserUpdated: { id: MiUser['id']; };
|
2024-02-25 04:36:10 +01:00
|
|
|
localUserUpdated: { id: MiUser['id']; };
|
2023-09-29 04:29:54 +02: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;
|
|
|
|
antennaCreated: MiAntenna;
|
|
|
|
antennaDeleted: MiAntenna;
|
|
|
|
antennaUpdated: MiAntenna;
|
2023-10-21 11:38:07 +02:00
|
|
|
avatarDecorationCreated: MiAvatarDecoration;
|
|
|
|
avatarDecorationDeleted: MiAvatarDecoration;
|
|
|
|
avatarDecorationUpdated: MiAvatarDecoration;
|
2023-09-29 04:29:54 +02: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']; };
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 12:51:49 +01:00
|
|
|
reversi: {
|
|
|
|
name: `reversiStream:${MiUser['id']}`;
|
|
|
|
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
|
|
|
|
};
|
|
|
|
reversiGame: {
|
|
|
|
name: `reversiGameStream:${MiReversiGame['id']}`;
|
|
|
|
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
|
|
|
|
};
|
2023-09-29 04:29:54 +02: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-17 20:27:08 +02:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class GlobalEventService {
|
|
|
|
constructor(
|
|
|
|
@Inject(DI.config)
|
|
|
|
private config: Config,
|
|
|
|
|
2023-04-09 10:09:27 +02:00
|
|
|
@Inject(DI.redisForPub)
|
|
|
|
private redisForPub: Redis.Redis,
|
2022-09-17 20:27:08 +02:00
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
2022-12-04 07:03:09 +01:00
|
|
|
@bindThis
|
2022-09-17 20:27:08 +02: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 };
|
|
|
|
|
2023-04-09 10:09:27 +02:00
|
|
|
this.redisForPub.publish(this.config.host, JSON.stringify({
|
2022-09-17 20:27:08 +02:00
|
|
|
channel: channel,
|
|
|
|
message: message,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishInternalEvent<K extends keyof InternalEventTypes>(type: K, value?: InternalEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2022-09-17 20:27:08 +02: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 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishMainStream<K extends keyof MainEventTypes>(userId: MiUser['id'], type: K, value?: MainEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishDriveStream<K extends keyof DriveEventTypes>(userId: MiUser['id'], type: K, value?: DriveEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish(`noteStream:${noteId}`, type, {
|
|
|
|
id: noteId,
|
|
|
|
body: value,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishUserListStream<K extends keyof UserListEventTypes>(listId: MiUserList['id'], type: K, value?: UserListEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishAntennaStream<K extends keyof AntennaEventTypes>(antennaId: MiAntenna['id'], type: K, value?: AntennaEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
|
|
|
|
2023-04-12 04:40:08 +02:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishRoleTimelineStream<K extends keyof RoleTimelineEventTypes>(roleId: MiRole['id'], type: K, value?: RoleTimelineEventTypes[K]): void {
|
2023-04-12 04:40:08 +02:00
|
|
|
this.publish(`roleTimelineStream:${roleId}`, type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
|
|
|
|
2022-12-04 07:03:09 +01:00
|
|
|
@bindThis
|
2022-09-17 20:27:08 +02:00
|
|
|
public publishNotesStream(note: Packed<'Note'>): void {
|
|
|
|
this.publish('notesStream', null, note);
|
|
|
|
}
|
|
|
|
|
2022-12-04 09:05:32 +01:00
|
|
|
@bindThis
|
2023-09-29 04:29:54 +02:00
|
|
|
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
|
2022-09-17 20:27:08 +02:00
|
|
|
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
|
|
|
}
|
2024-01-19 12:51:49 +01: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-17 20:27:08 +02:00
|
|
|
}
|