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:
parent
ec1c392f1e
commit
10ce7bf3c4
|
@ -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`に対応)
|
||||||
|
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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[];
|
|
@ -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);
|
||||||
|
|
|
@ -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> = {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Reference in New Issue