kill any from streaming API Implementation (#14251)

* chore: add JsonValue type

* refactor: kill any from Connection.ts

* refactor: fix StreamEventEmitter contains undefined instead of null

* refactor: kill any from channels

* docs(changelog): Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題

* fix license header

* fix lints
This commit is contained in:
anatawa12 2024-07-18 20:04:23 +09:00 committed by GitHub
parent ec1c392f1e
commit 10ce7bf3c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 155 additions and 83 deletions

View File

@ -2,6 +2,7 @@
### Note ### Note
- デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。 - デッキUIの新着ートをサウンドで通知する機能の追加v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。
- Streaming APIにて入力が不正な場合にはそのメッセージを無視するようになりました。 #14251
### General ### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
@ -76,6 +77,7 @@
- Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 - Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 - Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
### Misskey.js ### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)

View File

@ -209,6 +209,10 @@ type SerializedAll<T> = {
[K in keyof T]: Serialized<T[K]>; [K in keyof T]: Serialized<T[K]>;
}; };
type UndefinedAsNullAll<T> = {
[K in keyof T]: T[K] extends undefined ? null : T[K];
}
export interface InternalEventTypes { export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
@ -248,43 +252,45 @@ export interface InternalEventTypes {
userKeypairUpdated: { userId: MiUser['id']; }; userKeypairUpdated: { userId: MiUser['id']; };
} }
type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
// name/messages(spec) pairs dictionary // name/messages(spec) pairs dictionary
export type GlobalEvents = { export type GlobalEvents = {
internal: { internal: {
name: 'internal'; name: 'internal';
payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>; payload: EventTypesToEventPayload<InternalEventTypes>;
}; };
broadcast: { broadcast: {
name: 'broadcast'; name: 'broadcast';
payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>; payload: EventTypesToEventPayload<BroadcastTypes>;
}; };
main: { main: {
name: `mainStream:${MiUser['id']}`; name: `mainStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>; payload: EventTypesToEventPayload<MainEventTypes>;
}; };
drive: { drive: {
name: `driveStream:${MiUser['id']}`; name: `driveStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>; payload: EventTypesToEventPayload<DriveEventTypes>;
}; };
note: { note: {
name: `noteStream:${MiNote['id']}`; name: `noteStream:${MiNote['id']}`;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>; payload: EventTypesToEventPayload<NoteStreamEventTypes>;
}; };
userList: { userList: {
name: `userListStream:${MiUserList['id']}`; name: `userListStream:${MiUserList['id']}`;
payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>; payload: EventTypesToEventPayload<UserListEventTypes>;
}; };
roleTimeline: { roleTimeline: {
name: `roleTimelineStream:${MiRole['id']}`; name: `roleTimelineStream:${MiRole['id']}`;
payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>; payload: EventTypesToEventPayload<RoleTimelineEventTypes>;
}; };
antenna: { antenna: {
name: `antennaStream:${MiAntenna['id']}`; name: `antennaStream:${MiAntenna['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>; payload: EventTypesToEventPayload<AntennaEventTypes>;
}; };
admin: { admin: {
name: `adminStream:${MiUser['id']}`; name: `adminStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>; payload: EventTypesToEventPayload<AdminEventTypes>;
}; };
notes: { notes: {
name: 'notesStream'; name: 'notesStream';
@ -292,11 +298,11 @@ export type GlobalEvents = {
}; };
reversi: { reversi: {
name: `reversiStream:${MiUser['id']}`; name: `reversiStream:${MiUser['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>; payload: EventTypesToEventPayload<ReversiEventTypes>;
}; };
reversiGame: { reversiGame: {
name: `reversiGameStream:${MiReversiGame['id']}`; name: `reversiGameStream:${MiReversiGame['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>; payload: EventTypesToEventPayload<ReversiGameEventTypes>;
}; };
}; };

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type JsonValue = JsonArray | JsonObject | string | number | boolean | null;
export type JsonObject = {[K in string]?: JsonValue};
export type JsonArray = JsonValue[];

View File

@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js'; import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import type { JsonObject } from '@/misc/json-value.js';
import type { ChannelsService } from './ChannelsService.js'; import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type Channel from './channel.js'; import type Channel from './channel.js';
@ -28,7 +29,7 @@ export default class Connection {
private wsConnection: WebSocket.WebSocket; private wsConnection: WebSocket.WebSocket;
public subscriber: StreamEventEmitter; public subscriber: StreamEventEmitter;
private channels: Channel[] = []; private channels: Channel[] = [];
private subscribingNotes: any = {}; private subscribingNotes: Partial<Record<string, number>> = {};
private cachedNotes: Packed<'Note'>[] = []; private cachedNotes: Packed<'Note'>[] = [];
public userProfile: MiUserProfile | null = null; public userProfile: MiUserProfile | null = null;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
@ -101,7 +102,7 @@ export default class Connection {
*/ */
@bindThis @bindThis
private async onWsConnectionMessage(data: WebSocket.RawData) { private async onWsConnectionMessage(data: WebSocket.RawData) {
let obj: Record<string, any>; let obj: JsonObject;
try { try {
obj = JSON.parse(data.toString()); obj = JSON.parse(data.toString());
@ -111,6 +112,8 @@ export default class Connection {
const { type, body } = obj; const { type, body } = obj;
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
switch (type) { switch (type) {
case 'readNotification': this.onReadNotification(body); break; case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break; case 'subNote': this.onSubscribeNote(body); break;
@ -151,7 +154,7 @@ export default class Connection {
} }
@bindThis @bindThis
private readNote(body: any) { private readNote(body: JsonObject) {
const id = body.id; const id = body.id;
const note = this.cachedNotes.find(n => n.id === id); const note = this.cachedNotes.find(n => n.id === id);
@ -163,7 +166,7 @@ export default class Connection {
} }
@bindThis @bindThis
private onReadNotification(payload: any) { private onReadNotification(payload: JsonObject) {
this.notificationService.readAllNotification(this.user!.id); this.notificationService.readAllNotification(this.user!.id);
} }
@ -171,16 +174,14 @@ export default class Connection {
* 稿 * 稿
*/ */
@bindThis @bindThis
private onSubscribeNote(payload: any) { private onSubscribeNote(payload: JsonObject) {
if (!payload.id) return; if (!payload.id || typeof payload.id !== 'string') return;
if (this.subscribingNotes[payload.id] == null) { const current = this.subscribingNotes[payload.id] ?? 0;
this.subscribingNotes[payload.id] = 0; const updated = current + 1;
} this.subscribingNotes[payload.id] = updated;
this.subscribingNotes[payload.id]++; if (updated === 1) {
if (this.subscribingNotes[payload.id] === 1) {
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
} }
} }
@ -189,11 +190,14 @@ export default class Connection {
* 稿 * 稿
*/ */
@bindThis @bindThis
private onUnsubscribeNote(payload: any) { private onUnsubscribeNote(payload: JsonObject) {
if (!payload.id) return; if (!payload.id || typeof payload.id !== 'string') return;
this.subscribingNotes[payload.id]--; const current = this.subscribingNotes[payload.id];
if (this.subscribingNotes[payload.id] <= 0) { if (current == null) return;
const updated = current - 1;
this.subscribingNotes[payload.id] = updated;
if (updated <= 0) {
delete this.subscribingNotes[payload.id]; delete this.subscribingNotes[payload.id];
this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage);
} }
@ -212,17 +216,22 @@ export default class Connection {
* *
*/ */
@bindThis @bindThis
private onChannelConnectRequested(payload: any) { private onChannelConnectRequested(payload: JsonObject) {
const { channel, id, params, pong } = payload; const { channel, id, params, pong } = payload;
this.connectChannel(id, params, channel, pong); if (typeof id !== 'string') return;
if (typeof channel !== 'string') return;
if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return;
if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return;
this.connectChannel(id, params, channel, pong ?? undefined);
} }
/** /**
* *
*/ */
@bindThis @bindThis
private onChannelDisconnectRequested(payload: any) { private onChannelDisconnectRequested(payload: JsonObject) {
const { id } = payload; const { id } = payload;
if (typeof id !== 'string') return;
this.disconnectChannel(id); this.disconnectChannel(id);
} }
@ -230,7 +239,7 @@ export default class Connection {
* *
*/ */
@bindThis @bindThis
public sendMessageToWs(type: string, payload: any) { public sendMessageToWs(type: string, payload: JsonObject) {
this.wsConnection.send(JSON.stringify({ this.wsConnection.send(JSON.stringify({
type: type, type: type,
body: payload, body: payload,
@ -241,7 +250,7 @@ export default class Connection {
* *
*/ */
@bindThis @bindThis
public connectChannel(id: string, params: any, channel: string, pong = false) { public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) {
const channelService = this.channelsService.getChannelService(channel); const channelService = this.channelsService.getChannelService(channel);
if (channelService.requireCredential && this.user == null) { if (channelService.requireCredential && this.user == null) {
@ -288,7 +297,11 @@ export default class Connection {
* @param data * @param data
*/ */
@bindThis @bindThis
private onChannelMessageRequested(data: any) { private onChannelMessageRequested(data: JsonObject) {
if (typeof data.id !== 'string') return;
if (typeof data.type !== 'string') return;
if (typeof data.body === 'undefined') return;
const channel = this.channels.find(c => c.id === data.id); const channel = this.channels.find(c => c.id === data.id);
if (channel != null && channel.onMessage != null) { if (channel != null && channel.onMessage != null) {
channel.onMessage(data.type, data.body); channel.onMessage(data.type, data.body);

View File

@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import type Connection from './Connection.js'; import type Connection from './Connection.js';
/** /**
@ -81,10 +82,12 @@ export default abstract class Channel {
this.connection = connection; this.connection = connection;
} }
public send(payload: { type: string, body: JsonValue }): void
public send(type: string, payload: JsonValue): void
@bindThis @bindThis
public send(typeOrPayload: any, payload?: any) { public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
const type = payload === undefined ? typeOrPayload.type : typeOrPayload; const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
const body = payload === undefined ? typeOrPayload.body : payload; const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
this.connection.sendMessageToWs('channel', { this.connection.sendMessageToWs('channel', {
id: this.id, id: this.id,
@ -93,11 +96,11 @@ export default abstract class Channel {
}); });
} }
public abstract init(params: any): void; public abstract init(params: JsonObject): void;
public dispose?(): void; public dispose?(): void;
public onMessage?(type: string, body: any): void; public onMessage?(type: string, body: JsonValue): void;
} }
export type MiChannelService<T extends boolean> = { export type MiChannelService<T extends boolean> = {

View File

@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class AdminChannel extends Channel { class AdminChannel extends Channel {
@ -14,7 +15,7 @@ class AdminChannel extends Channel {
public static kind = 'read:admin:stream'; public static kind = 'read:admin:stream';
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
// Subscribe admin stream // Subscribe admin stream
this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.subscriber.on(`adminStream:${this.user!.id}`, data => {
this.send(data); this.send(data);

View File

@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class AntennaChannel extends Channel { class AntennaChannel extends Channel {
@ -27,8 +28,9 @@ class AntennaChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.antennaId = params.antennaId as string; if (typeof params.antennaId !== 'string') return;
this.antennaId = params.antennaId;
// Subscribe stream // Subscribe stream
this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent);

View File

@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class ChannelChannel extends Channel { class ChannelChannel extends Channel {
@ -27,8 +28,9 @@ class ChannelChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.channelId = params.channelId as string; if (typeof params.channelId !== 'string') return;
this.channelId = params.channelId;
// Subscribe stream // Subscribe stream
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class DriveChannel extends Channel { class DriveChannel extends Channel {
@ -14,7 +15,7 @@ class DriveChannel extends Channel {
public static kind = 'read:account'; public static kind = 'read:account';
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
// Subscribe drive stream // Subscribe drive stream
this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.subscriber.on(`driveStream:${this.user!.id}`, data => {
this.send(data); this.send(data);

View File

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class GlobalTimelineChannel extends Channel { class GlobalTimelineChannel extends Channel {
@ -32,12 +33,12 @@ class GlobalTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return; if (!policies.gtlAvailable) return;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class HashtagChannel extends Channel { class HashtagChannel extends Channel {
@ -28,11 +29,11 @@ class HashtagChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
if (!Array.isArray(params.q)) return;
if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return;
this.q = params.q; this.q = params.q;
if (this.q == null) return;
// Subscribe stream // Subscribe stream
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }

View File

@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class HomeTimelineChannel extends Channel { class HomeTimelineChannel extends Channel {
@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);
} }

View File

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class HybridTimelineChannel extends Channel { class HybridTimelineChannel extends Channel {
@ -34,13 +35,13 @@ class HybridTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any): Promise<void> { public async init(params: JsonObject): Promise<void> {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withReplies = params.withReplies ?? false; this.withReplies = !!(params.withReplies ?? false);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class LocalTimelineChannel extends Channel { class LocalTimelineChannel extends Channel {
@ -33,13 +34,13 @@ class LocalTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return; if (!policies.ltlAvailable) return;
this.withRenotes = params.withRenotes ?? true; this.withRenotes = !!(params.withRenotes ?? true);
this.withReplies = params.withReplies ?? false; this.withReplies = !!(params.withReplies ?? false);
this.withFiles = params.withFiles ?? false; this.withFiles = !!(params.withFiles ?? false);
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class MainChannel extends Channel { class MainChannel extends Channel {
@ -25,7 +26,7 @@ class MainChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
// Subscribe main stream channel // Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => { this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
switch (data.type) { switch (data.type) {

View File

@ -6,6 +6,7 @@
import Xev from 'xev'; import Xev from 'xev';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev(); const ev = new Xev();
@ -22,19 +23,22 @@ class QueueStatsChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
ev.addListener('queueStats', this.onStats); ev.addListener('queueStats', this.onStats);
} }
@bindThis @bindThis
private onStats(stats: any) { private onStats(stats: JsonObject) {
this.send('stats', stats); this.send('stats', stats);
} }
@bindThis @bindThis
public onMessage(type: string, body: any) { public onMessage(type: string, body: JsonValue) {
switch (type) { switch (type) {
case 'requestLog': case 'requestLog':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.id !== 'string') return;
if (typeof body.length !== 'number') return;
ev.once(`queueStatsLog:${body.id}`, statsLog => { ev.once(`queueStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog); this.send('statsLog', statsLog);
}); });

View File

@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ReversiService } from '@/core/ReversiService.js'; import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class ReversiGameChannel extends Channel { class ReversiGameChannel extends Channel {
@ -28,25 +29,41 @@ class ReversiGameChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.gameId = params.gameId as string; if (typeof params.gameId !== 'string') return;
this.gameId = params.gameId;
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send); this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
} }
@bindThis @bindThis
public onMessage(type: string, body: any) { public onMessage(type: string, body: JsonValue) {
switch (type) { switch (type) {
case 'ready': this.ready(body); break; case 'ready':
case 'updateSettings': this.updateSettings(body.key, body.value); break; if (typeof body !== 'boolean') return;
case 'cancel': this.cancelGame(); break; this.ready(body);
case 'putStone': this.putStone(body.pos, body.id); break; break;
case 'updateSettings':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.key !== 'string') return;
if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return;
this.updateSettings(body.key, body.value);
break;
case 'cancel':
this.cancelGame();
break;
case 'putStone':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
if (typeof body.pos !== 'number') return;
if (typeof body.id !== 'string') return;
this.putStone(body.pos, body.id);
break;
case 'claimTimeIsUp': this.claimTimeIsUp(); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break;
} }
} }
@bindThis @bindThis
private async updateSettings(key: string, value: any) { private async updateSettings(key: string, value: JsonObject) {
if (this.user == null) return; if (this.user == null) return;
this.reversiService.updateSettings(this.gameId!, this.user, key, value); this.reversiService.updateSettings(this.gameId!, this.user, key, value);

View File

@ -5,6 +5,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class ReversiChannel extends Channel { class ReversiChannel extends Channel {
@ -21,7 +22,7 @@ class ReversiChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.subscriber.on(`reversiStream:${this.user!.id}`, this.send); this.subscriber.on(`reversiStream:${this.user!.id}`, this.send);
} }

View File

@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class RoleTimelineChannel extends Channel { class RoleTimelineChannel extends Channel {
@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.roleId = params.roleId as string; if (typeof params.roleId !== 'string') return;
this.roleId = params.roleId;
this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent); this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent);
} }

View File

@ -6,6 +6,7 @@
import Xev from 'xev'; import Xev from 'xev';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev(); const ev = new Xev();
@ -22,19 +23,20 @@ class ServerStatsChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
ev.addListener('serverStats', this.onStats); ev.addListener('serverStats', this.onStats);
} }
@bindThis @bindThis
private onStats(stats: any) { private onStats(stats: JsonObject) {
this.send('stats', stats); this.send('stats', stats);
} }
@bindThis @bindThis
public onMessage(type: string, body: any) { public onMessage(type: string, body: JsonValue) {
switch (type) { switch (type) {
case 'requestLog': case 'requestLog':
if (typeof body !== 'object' || body === null || Array.isArray(body)) return;
ev.once(`serverStatsLog:${body.id}`, statsLog => { ev.once(`serverStatsLog:${body.id}`, statsLog => {
this.send('statsLog', statsLog); this.send('statsLog', statsLog);
}); });

View File

@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
class UserListChannel extends Channel { class UserListChannel extends Channel {
@ -36,10 +37,11 @@ class UserListChannel extends Channel {
} }
@bindThis @bindThis
public async init(params: any) { public async init(params: JsonObject) {
this.listId = params.listId as string; if (typeof params.listId !== 'string') return;
this.withFiles = params.withFiles ?? false; this.listId = params.listId;
this.withRenotes = params.withRenotes ?? true; this.withFiles = !!(params.withFiles ?? false);
this.withRenotes = !!(params.withRenotes ?? true);
// Check existence and owner // Check existence and owner
const listExist = await this.userListsRepository.exists({ const listExist = await this.userListsRepository.exists({