refactor: improve live chat polling (#220)

* dev: add RemoveChatItemByAuthorAction renderer parser

* dev: improve live chat polling
This commit is contained in:
LuanRT
2022-10-12 16:16:07 -03:00
committed by GitHub
parent 477c030084
commit 1b2862c00f
3 changed files with 78 additions and 67 deletions

View File

@@ -0,0 +1,14 @@
import { YTNode } from '../../helpers';
class RemoveChatItemByAuthorAction extends YTNode {
static type = 'RemoveChatItemByAuthorAction';
external_channel_id: string;
constructor(data: any) {
super();
this.external_channel_id = data.externalChannelId;
}
}
export default RemoveChatItemByAuthorAction;

View File

@@ -112,6 +112,7 @@ import { default as MarkChatItemAsDeletedAction } from './classes/livechat/MarkC
import { default as MarkChatItemsByAuthorAsDeletedAction } from './classes/livechat/MarkChatItemsByAuthorAsDeletedAction';
import { default as RemoveBannerForLiveChatCommand } from './classes/livechat/RemoveBannerForLiveChatCommand';
import { default as RemoveChatItemAction } from './classes/livechat/RemoveChatItemAction';
import { default as RemoveChatItemByAuthorAction } from './classes/livechat/RemoveChatItemByAuthorAction';
import { default as ReplaceChatItemAction } from './classes/livechat/ReplaceChatItemAction';
import { default as ReplayChatItemAction } from './classes/livechat/ReplayChatItemAction';
import { default as ShowLiveChatActionPanelAction } from './classes/livechat/ShowLiveChatActionPanelAction';
@@ -389,6 +390,7 @@ const map: Record<string, YTNodeConstructor> = {
MarkChatItemsByAuthorAsDeletedAction,
RemoveBannerForLiveChatCommand,
RemoveChatItemAction,
RemoveChatItemByAuthorAction,
ReplaceChatItemAction,
ReplayChatItemAction,
ShowLiveChatActionPanelAction,

View File

@@ -20,8 +20,10 @@ import AddBannerToLiveChatCommand from '../classes/livechat/AddBannerToLiveChatC
import RemoveBannerForLiveChatCommand from '../classes/livechat/RemoveBannerForLiveChatCommand';
import ShowLiveChatTooltipCommand from '../classes/livechat/ShowLiveChatTooltipCommand';
import { InnertubeError } from '../../utils/Utils';
import Proto from '../../proto/index';
import { InnertubeError, uuidv4 } from '../../utils/Utils';
import { ObservedArray, YTNode } from '../helpers';
import LiveChatTextMessage from '../classes/livechat/items/LiveChatTextMessage';
import LiveChatPaidMessage from '../classes/livechat/items/LiveChatPaidMessage';
import LiveChatPaidSticker from '../classes/livechat/items/LiveChatPaidSticker';
@@ -36,7 +38,7 @@ export type ChatAction =
MarkChatItemAsDeletedAction | MarkChatItemsByAuthorAsDeletedAction | RemoveBannerForLiveChatCommand |
ReplaceChatItemAction | ReplayChatItemAction | ShowLiveChatActionPanelAction | ShowLiveChatTooltipCommand;
export type ChatItemHasMenuEndpoint = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
export type ChatItemWithMenu = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
export interface LiveMetadata {
title: UpdateTitleAction | undefined;
@@ -52,9 +54,6 @@ class LiveChat extends EventEmitter {
#continuation;
#mcontinuation?: string;
#lc_polling_interval_ms = 1000;
#md_polling_interval_ms = 5000;
initial_info?: LiveChatContinuation;
metadata?: LiveMetadata;
@@ -83,117 +82,113 @@ class LiveChat extends EventEmitter {
}
#pollLivechat() {
const lc_poller = setTimeout(() => {
(async () => {
const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat';
const response = await this.#actions.livechat(endpoint, { ctoken: this.#continuation });
(async () => {
const endpoint = this.is_replay ? 'live_chat/get_live_chat_replay' : 'live_chat/get_live_chat';
const response = await this.#actions.execute(endpoint, { continuation: this.#continuation });
const data = Parser.parseResponse(response.data);
const contents = data.continuation_contents;
const data = Parser.parseResponse(response.data);
const contents = data.continuation_contents;
if (!(contents instanceof LiveChatContinuation))
throw new InnertubeError('Continuation is not a LiveChatContinuation');
if (!(contents instanceof LiveChatContinuation))
throw new InnertubeError('Continuation is not a LiveChatContinuation');
this.#continuation = contents.continuation.token;
this.#lc_polling_interval_ms = contents.continuation.timeout_ms;
this.#continuation = contents.continuation.token;
// Header only exists in the first request
if (contents.header) {
this.initial_info = contents;
this.emit('start', contents);
} else {
await this.#emitSmoothedActions(contents.actions);
}
// Header only exists in the first request
if (contents.header) {
this.initial_info = contents;
this.emit('start', contents);
} else {
await this.#emitSmoothedActions(contents.actions);
}
clearTimeout(lc_poller);
// If there are no actions then we wait 1000 milliseconds, otherwise
// The amount of items on the action queue will determine the polling interval.
if (!contents.actions.length && !contents.header)
await this.#wait(1000);
this.running && this.#pollLivechat();
})().catch((err) => Promise.reject(err));
}, this.#lc_polling_interval_ms);
if (this.running)
this.#pollLivechat();
})();
}
/**
* Ensures actions are emitted at the right speed.
* This was adapted from YouTube's compiled code (Android).
* This was adapted from YouTube's compiled code (Android & Web).
*/
async #emitSmoothedActions(actions: ObservedArray<YTNode>) {
async #emitSmoothedActions(action_queue: YTNode[]) {
const base = 1E4;
let delay = actions.length < base / 80 ? 1 : 0;
let delay = action_queue.length < base / 80 ? 1 : Math.ceil(action_queue.length / (base / 80));
const emit_delay_ms =
delay == 1 ? (
delay = base / actions.length,
delay = base / action_queue.length,
delay *= Math.random() + 0.5,
delay = Math.min(1E3, delay),
delay = Math.max(80, delay)
) : delay = 80;
for (const action of actions) {
for (const action of action_queue) {
await this.#wait(emit_delay_ms);
this.emit('chat-update', action);
}
}
#pollMetadata() {
const md_poller = setTimeout(() => {
(async () => {
const payload = {
video_id: this.#video_info.basic_info.id,
ctoken: undefined as string | undefined
};
(async () => {
const payload: {
videoId: string | undefined;
continuation?: string;
} = { videoId: this.#video_info.basic_info.id };
if (this.#mcontinuation) {
payload.ctoken = this.#mcontinuation;
}
if (this.#mcontinuation) {
payload.continuation = this.#mcontinuation;
}
const response = await this.#actions.livechat('updated_metadata', payload);
const data = Parser.parseResponse(response.data);
const response = await this.#actions.execute('/updated_metadata', payload);
const data = Parser.parseResponse(response.data);
this.#mcontinuation = data.continuation?.token;
this.#md_polling_interval_ms = data.continuation?.timeout_ms || this.#md_polling_interval_ms;
this.#mcontinuation = data.continuation?.token;
this.metadata = {
title: data.actions?.array().firstOfType(UpdateTitleAction) || this.metadata?.title,
description: data.actions?.array().firstOfType(UpdateDescriptionAction) || this.metadata?.description,
views: data.actions?.array().firstOfType(UpdateViewershipAction) || this.metadata?.views,
likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction) || this.metadata?.likes,
date: data.actions?.array().firstOfType(UpdateDateTextAction) || this.metadata?.date
};
this.metadata = {
title: data.actions?.array().firstOfType(UpdateTitleAction) || this.metadata?.title,
description: data.actions?.array().firstOfType(UpdateDescriptionAction) || this.metadata?.description,
views: data.actions?.array().firstOfType(UpdateViewershipAction) || this.metadata?.views,
likes: data.actions?.array().firstOfType(UpdateToggleButtonTextAction) || this.metadata?.likes,
date: data.actions?.array().firstOfType(UpdateDateTextAction) || this.metadata?.date
};
this.emit('metadata-update', this.metadata);
this.emit('metadata-update', this.metadata);
clearTimeout(md_poller);
await this.#wait(5000);
this.running && this.#pollMetadata();
})().catch((err) => Promise.reject(err));
}, this.#md_polling_interval_ms);
if (this.running)
this.#pollMetadata();
})();
}
/**
* Sends a message.
*/
async sendMessage(text: string): Promise<ObservedArray<AddChatItemAction>> {
const response = await this.#actions.livechat('live_chat/send_message', {
text,
...{
video_id: this.#video_info.basic_info.id,
channel_id: this.#video_info.basic_info.channel_id
}
const response = await this.#actions.execute('/live_chat/send_message', {
params: Proto.encodeMessageParams(this.#video_info.basic_info.channel_id as string, this.#video_info.basic_info.id as string),
richMessage: { textSegments: [ { text } ] },
clientMessageId: uuidv4(),
parse: true
});
const data = Parser.parseResponse(response.data);
if (!data.actions)
if (!response.actions)
throw new InnertubeError('Response did not have an "actions" property. The call may have failed.');
return data.actions.array().as(AddChatItemAction);
return response.actions.array().as(AddChatItemAction);
}
/**
* Retrieves given chat item's menu.
*/
async getItemMenu(item: ChatItemHasMenuEndpoint): Promise<ItemMenu> {
async getItemMenu(item: ChatItemWithMenu): Promise<ItemMenu> {
if (!item.menu_endpoint)
throw new InnertubeError('This item does not have a menu.', item);