mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-18 20:12:12 +00:00
feat: add support for livechats
This commit is contained in:
152
lib/Livechat.js
Normal file
152
lib/Livechat.js
Normal file
@@ -0,0 +1,152 @@
|
||||
'use strict';
|
||||
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
const EventEmitter = require('events');
|
||||
const Uuid = require("uuid");
|
||||
|
||||
class Livechat extends EventEmitter {
|
||||
constructor(session, token, channel_id, video_id) {
|
||||
super(session);
|
||||
this.ctoken = token;
|
||||
this.session = session;
|
||||
this.video_id = video_id;
|
||||
this.channel_id = channel_id;
|
||||
|
||||
this.message_queue = [];
|
||||
this.id_cache = [];
|
||||
|
||||
this.poll_intervals_ms = 0;
|
||||
this.running = true;
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
async sendMessage(text) {
|
||||
let data = {
|
||||
context: this.session.context,
|
||||
params: Utils.encodeChannelIdWithVideoId(this.channel_id, this.video_id),
|
||||
clientMessageId: `INntLiB${Uuid.v4()}`,
|
||||
richMessage: {
|
||||
textSegments: [{ text }]
|
||||
}
|
||||
};
|
||||
|
||||
const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/send_message${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.response.data.error.message };
|
||||
|
||||
const deleteMessage = async () => {
|
||||
/*
|
||||
* The first request is made to get the chat options and the delete command endpoint,
|
||||
* these options contain the required params to delete a message (a string composed of clientId, the channelId of the channel you're watching, your public channelId and the id of the message you sent).
|
||||
* All put together with some binary data and then base64ed twice (yes, twice lm*o top notch security).
|
||||
**/
|
||||
const item_menu_res = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/get_item_context_menu?params=${response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params}&pbj=1${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify({ context: this.session.context }), Constants.innertube_request_opts({ session: this.session, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (item_menu_res instanceof Error) return { success: false, status_code: item_menu_res.response.status, message: item_menu_res.response.data.error.message };
|
||||
const chat_item_menu = item_menu_res.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0];
|
||||
|
||||
const delete_message_reqbody = {
|
||||
context: this.session.context,
|
||||
params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params
|
||||
};
|
||||
|
||||
const delete_message_cmd = await Axios.post(`${Constants.urls.YT_BASE_URL}${chat_item_menu.menuServiceItemRenderer.serviceEndpoint.commandMetadata.webCommandMetadata.apiUrl}${this.session.logged_in && this.session.cookie.length < 1 ? '' : `&key=${this.session.key}`}`, JSON.stringify(delete_message_reqbody), Constants.innertube_request_opts({ session: this.session, delete_message_reqbody, id: this.video_id, desktop: true })).catch((error) => error);
|
||||
if (delete_message_cmd instanceof Error) return { success: false, status_code: delete_message_cmd.response.status, message: delete_message_cmd.response.data.error.message };
|
||||
return { success: true, status_code: response.status };
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
deleteMessage: () => deleteMessage(),
|
||||
message_data: {
|
||||
text: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.message.runs.map((item) => item.text).join(' '),
|
||||
author: {
|
||||
name: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName && response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorName.simpleText || 'N/',
|
||||
channel_id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorExternalChannelId,
|
||||
profile_picture: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.authorPhoto.thumbnails
|
||||
},
|
||||
timestamp: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.timestampUsec,
|
||||
id: response.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
enqueueActionGroup(group) {
|
||||
group.forEach((action) => {
|
||||
if (!action.addChatItemAction) return;
|
||||
const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer;
|
||||
if (!message_content) return;
|
||||
|
||||
const message = {
|
||||
text: message_content.message.runs.map((item) => item.text).join(' '),
|
||||
author: {
|
||||
name: message_content.authorName && message_content.authorName.simpleText || 'N/',
|
||||
channel_id: message_content.authorExternalChannelId,
|
||||
profile_picture: message_content.authorPhoto.thumbnails
|
||||
},
|
||||
timestamp: message_content.timestampUsec,
|
||||
id: message_content.id
|
||||
};
|
||||
|
||||
this.message_queue.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
async poll() {
|
||||
if (!this.running) return;
|
||||
|
||||
let data;
|
||||
|
||||
data = { context: this.session.context, continuation: this.ctoken };
|
||||
const livechat = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/live_chat/get_live_chat${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, desktop: true }));
|
||||
if (livechat instanceof Error) throw new Error(`Error polling livechat: ${livechat.message}`);
|
||||
|
||||
const continuation_contents = livechat.data.continuationContents;
|
||||
const action_group = continuation_contents.liveChatContinuation.actions;
|
||||
this.enqueueActionGroup(action_group);
|
||||
|
||||
// Why don't we just emit the message directly? Well, enqueueing the messages is necessary so they are not emitted in a “messy” way, funny enough that's exactly how YouTube does it in its livechat js player.
|
||||
this.message_queue.forEach((message, index) => {
|
||||
if (this.id_cache.includes(message.id)) return;
|
||||
setTimeout(() => this.emit('chat-update', message), message.timestamp / 1000 - new Date().getTime());
|
||||
this.id_cache.push(message.id);
|
||||
});
|
||||
|
||||
|
||||
data = { context: this.session.context, videoId: this.video_id };
|
||||
if (this.metadata_ctoken) data.continuation = this.metadata_ctoken;
|
||||
|
||||
const updated_metadata = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/updated_metadata${this.session.logged_in && this.session.cookie.length < 1 ? '' : `?key=${this.session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session: this.session, data, desktop: true }));
|
||||
if (updated_metadata instanceof Error) throw new Error(`Error polling updated metadata: ${updated_metadata.message}`);
|
||||
this.metadata_ctoken = updated_metadata.data.continuation.timedContinuationData.continuation;
|
||||
|
||||
const metadata = updated_metadata.data.actions;
|
||||
this.emit('update-metadata', {
|
||||
likes: metadata[1].updateToggleButtonTextAction.defaultText.simpleText,
|
||||
dislikes: metadata[2].updateToggleButtonTextAction.defaultText.simpleText,
|
||||
view_count: {
|
||||
simple_text: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.viewCount.simpleText,
|
||||
short_view_count: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.extraShortViewCount.simpleText
|
||||
}
|
||||
});
|
||||
|
||||
// Amount of time we should wait to poll the chat again.
|
||||
if (continuation_contents.liveChatContinuation.continuations[0].timedContinuationData) {
|
||||
this.poll_intervals_ms = continuation_contents.liveChatContinuation.continuations[0].timedContinuationData.timeoutMs;
|
||||
} else {
|
||||
this.poll_intervals_ms = 4000;
|
||||
}
|
||||
|
||||
await this.poll();
|
||||
this.livechat_poller = setTimeout(() => this.poll(), this.poll_intervals_ms);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false;
|
||||
clearTimeout(this.livechat_poller);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Livechat;
|
||||
24
lib/Utils.js
24
lib/Utils.js
@@ -3,6 +3,10 @@
|
||||
const Crypto = require('crypto');
|
||||
const UserAgent = require('user-agents');
|
||||
|
||||
function wait (milliseconds) {
|
||||
return new Promise((resolve, reject) => setTimeout(() => resolve(), milliseconds));
|
||||
}
|
||||
|
||||
function getRandomUserAgent(type) {
|
||||
switch (type) {
|
||||
case 'mobile':
|
||||
@@ -39,6 +43,18 @@ function createFunction(input, raw_code) { // I hate this
|
||||
return new Function(input, raw_code);
|
||||
}
|
||||
|
||||
function encodeChannelIdWithVideoId(channel_id, video_id) {
|
||||
const buff_start = `
|
||||
)*'
|
||||
`;
|
||||
const buff_middle = ``;
|
||||
const buff_end = ``;
|
||||
|
||||
// Yes, we also have to base64 these twice lol
|
||||
let encodedIds = Buffer.from([ buff_start, channel_id, buff_middle, video_id, buff_end ].join('')).toString('base64');
|
||||
return `${Buffer.from(encodedIds).toString('base64').slice(0, -4)}JTNE`;
|
||||
}
|
||||
|
||||
function encodeChannelId(id, notification_pref) {
|
||||
const buff_start = `
|
||||
`;
|
||||
@@ -47,13 +63,13 @@ function encodeChannelId(id, notification_pref) {
|
||||
``, // none
|
||||
``, // personalized
|
||||
];
|
||||
|
||||
let buff = Buffer.from(`${buff_start}${id}${buff_end[notification_pref]}`);
|
||||
return encodeURIComponent(`${buff.toString('base64')}GAAgBA==`);
|
||||
|
||||
let encodedId = Buffer.from([buff_start, id, buff_end[notification_pref]].join('')).toString('base64');
|
||||
return encodeURIComponent(`${encodedId}GAAgBA==`);
|
||||
}
|
||||
|
||||
function encodeVideoId(id) {
|
||||
return encodeURIComponent(`${Buffer.from(`` + id + `*`).toString('base64').slice(0, -1)}BQBw==`);
|
||||
}
|
||||
|
||||
module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeVideoId, encodeChannelId };
|
||||
module.exports = { wait, getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeChannelIdWithVideoId, encodeVideoId, encodeChannelId };
|
||||
Reference in New Issue
Block a user