diff --git a/src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts b/src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts new file mode 100644 index 00000000..41d68b24 --- /dev/null +++ b/src/parser/classes/livechat/RemoveChatItemByAuthorAction.ts @@ -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; \ No newline at end of file diff --git a/src/parser/map.ts b/src/parser/map.ts index 3602e057..73347c96 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -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 = { MarkChatItemsByAuthorAsDeletedAction, RemoveBannerForLiveChatCommand, RemoveChatItemAction, + RemoveChatItemByAuthorAction, ReplaceChatItemAction, ReplayChatItemAction, ShowLiveChatActionPanelAction, diff --git a/src/parser/youtube/LiveChat.ts b/src/parser/youtube/LiveChat.ts index 54561717..bfc61877 100644 --- a/src/parser/youtube/LiveChat.ts +++ b/src/parser/youtube/LiveChat.ts @@ -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) { + 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> { - 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 { + async getItemMenu(item: ChatItemWithMenu): Promise { if (!item.menu_endpoint) throw new InnertubeError('This item does not have a menu.', item);