diff --git a/lib/Constants.js b/lib/Constants.js index 62767895..2d4ce37e 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -175,7 +175,14 @@ const formatVideoData = (data, context, desktop) => { return video_details; }; -const filters = (order) => { // TODO: Refactor this crazy thing + +const base64_alphabet = { + normal: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split(''), + reverse: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'.split('') +}; + +const filters = (order) => { + // TODO: Refactor this with protobuf encoding switch (order) { case 'any,any,relevance': return 'EgIQAQ%3D%3D'; @@ -325,4 +332,4 @@ const filters = (order) => { // TODO: Refactor this crazy thing } }; -module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, filters }; \ No newline at end of file +module.exports = { urls, oauth, oauth_reqopts, default_headers, innertube_request_opts, video_details_reqbody, stream_headers, formatVideoData, base64_alphabet, filters }; \ No newline at end of file diff --git a/lib/Innertube.js b/lib/Innertube.js index 54a79575..a66cca69 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -5,6 +5,7 @@ const Stream = require('stream'); const OAuth = require('./OAuth'); const Utils = require('./Utils'); const Player = require('./Player'); +const NToken = require('./NToken'); const Actions = require('./Actions'); const Livechat = require('./Livechat'); const Constants = require('./Constants'); @@ -308,12 +309,12 @@ class Innertube extends EventEmitter { format.url = format.url || format.signatureCipher || format.cipher; if (format.signatureCipher || format.cipher) { - format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player.sig_decipher_sc, this.player.encodeN).decipher(); + format.url = new SigDecipher(format.url, this.context.client.clientVersion, this.player).decipher(); } else { const url_components = new URL(format.url); url_components.searchParams.set('cver', this.context.client.clientVersion); url_components.searchParams.set('ratebypass', 'yes'); - url_components.searchParams.set('n', this.player.encodeN(url_components.searchParams.get('n'))); + url_components.searchParams.set('n', new NToken(this.player.ntoken_sc).transform(url_components.searchParams.get('n'))); format.url = url_components.toString(); } diff --git a/lib/Livechat.js b/lib/Livechat.js index 12e7bcb6..85882f74 100644 --- a/lib/Livechat.js +++ b/lib/Livechat.js @@ -21,10 +21,10 @@ class Livechat extends EventEmitter { this.poll(); } - + enqueueActionGroup(group) { group.forEach((action) => { - if (!action.addChatItemAction) return; //TODO: handle different action types */ + if (!action.addChatItemAction) return; //TODO: handle different action types const message_content = action.addChatItemAction.item.liveChatTextMessageRenderer; if (!message_content) return; @@ -38,7 +38,7 @@ class Livechat extends EventEmitter { timestamp: message_content.timestampUsec, id: message_content.id }; - + this.message_queue.push(message); }); } @@ -62,9 +62,9 @@ class Livechat extends EventEmitter { setTimeout(() => this.emit('chat-update', message), message.timestamp / 1000 - new Date().getTime()); this.id_cache.push(message.id); }); - + this.message_queue = []; - + data = { context: this.session.context, videoId: this.video_id }; if (this.metadata_ctoken) data.continuation = this.metadata_ctoken; @@ -81,23 +81,23 @@ class Livechat extends EventEmitter { short_view_count: metadata[0].updateViewershipAction.viewCount.videoViewCountRenderer.extraShortViewCount.simpleText } }); - + this.livechat_poller = setTimeout(async () => await this.poll(), this.poll_intervals_ms); } - + async sendMessage(text) { const message = await Actions.livechat(this.session, 'live_chat/send_message', { text, channel_id: this.channel_id, video_id: this.video_id }); if (!message.success) return message; - + const deleteMessage = async () => { const menu = await Actions.livechat(this.session, 'live_chat/get_item_context_menu', { params: { params: message.data.actions[0].addChatItemAction.item.liveChatTextMessageRenderer.contextMenuEndpoint.liveChatItemContextMenuEndpoint.params, pbj: 1 } }); if (!menu.success) return menu; - + const chat_item_menu = menu.data.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0]; - + const cmd = await Actions.livechat(this.session, 'live_chat/moderate', { cmd_params: chat_item_menu.menuServiceItemRenderer.serviceEndpoint.moderateLiveChatEndpoint.params }); if (!cmd.success) return cmd; - + return { success: true, status_code: cmd.status_code }; }; @@ -117,9 +117,9 @@ class Livechat extends EventEmitter { } }; } - + async blockUser(msg_params) { - /* TODO: Implement this */ + /* TODO: Implement this */ throw new Error('Not implemented'); } diff --git a/lib/NToken.js b/lib/NToken.js new file mode 100644 index 00000000..a5e95c9f --- /dev/null +++ b/lib/NToken.js @@ -0,0 +1,129 @@ +'use strict'; + +const Utils = require('./Utils'); +const Constants = require('./Constants'); + +class NToken { + constructor(raw_code) { + this.raw_code = raw_code; + this.null_placeholder_regex = /c\[(.*?)\]=c/g; + this.transformation_args_regex = /c\[(.*?)\]\((.+?)\)/g; + } + + transform(n) { + let n_token = n.split(''); + let transformations = this.getTransformationData(this.raw_code); + + // Identifies the necessary transformation functions and emulates them accordingly. + transformations = transformations.map((el, i) => { + if (el != null && typeof el != 'number') { + const is_reverse_base64 = el.includes('case 65:'); + if (el.includes('function(d){for(var')) { + el = (arr) => this.pushSplice(arr); + } else if (el.includes('d.push(e)')) { + el = (arr, item) => this.push(arr, item); + } else if (el.includes('d.reverse()')) { + el = (arr) => this.reverse(arr); + } else if (el.includes('d.length;d.splice(e,1)')) { + el = (arr, index) => this.spliceOnce(arr, index); + } else if (el.includes('d[0])[0])')) { + el = (arr, index) => this.spliceTwice(arr, index); + } else if (el.includes('reverse().forEach')) { + el = (arr, index) => this.spliceReverseUnshift(arr, index); + } else if (el.includes('f=d[0];d[0]')) { + el = (arr, index) => this.swapFirstItem(arr, index); + } else if (el.includes('unshift(d.pop())')) { + el = (arr, index) => this.unshiftPop(arr, index); + } else if (el.includes('switch')) { + el = (arr, e) => this.translateAB(arr, e, is_reverse_base64); + } else if (el === 'b') { + el = n_token; + } + } + return el; + }); + + // Fills the null placeholders with a copy of the transformations array. + let null_placeholder_positions = [...this.raw_code.matchAll(this.null_placeholder_regex)].map((item) => parseInt(item[1])); + null_placeholder_positions.forEach((pos) => transformations[pos] = transformations); + + // Parses and emulates calls to functions of the transformations array. + let transformation_args = [...Utils.getStringBetweenStrings(this.raw_code.replace(/\n/g, ''), 'try{', '}catch').matchAll(this.transformation_args_regex)].map((params) => ({ index: params[1], params: params[2] })); + transformation_args.forEach((data) => { + const index = data.index; + const param_index = data.params.split(',').map((param) => param.match(/c\[(.*?)\]/)[1]); + transformations[index](transformations[param_index[0]], transformations[param_index[1]]); + }); + + return n_token.join(''); + } + + getTransformationData() { + let transformation_data = '[' + Utils.getStringBetweenStrings(this.raw_code, 'c=[', '];c') + ']'; + transformation_data = transformation_data + .replace(/function\(d,e\)/g, '"function(d,e)') + .replace(/function\(d\)/g, '"function(d)') + .replace(/,b,/g, ',"b",') + .replace(/,b/g, ',"b"') + .replace(/b,/g, '"b",') + .replace(/b]/g, '"b"]') + .replace(/},/g, '}",') + .replace(/""/g, '') + .replace(/length]\)}"/g, 'length])}'); + + return JSON.parse(transformation_data); + } + + translateAB(arr, e, is_reverse_base64) { + let characters = is_reverse_base64 && Constants.base64_alphabet.reverse || Constants.base64_alphabet.normal; + arr.forEach(function(char, index, loc) { + this.push(loc[index] = characters[(characters.indexOf(char) - characters.indexOf(this[index]) + 64) % characters.length]); + }, e.split('')); + } + + unshiftPop(arr, index) { + index = (index % arr.length + arr.length) % arr.length; + for (; index--;) { + arr.unshift(arr.pop()); + } + } + + swapFirstItem(arr, index) { + let oldValue = arr[0]; + index = (index % arr.length + arr.length) % arr.length; + arr[0] = arr[index]; + arr[index] = oldValue; + } + + spliceReverseUnshift(arr, index) { + index = (index % arr.length + arr.length) % arr.length; + arr.splice(-index).reverse().forEach(function(f) { + arr.unshift(f); + }); + } + + spliceOnce(arr, index) { + index = (index % arr.length + arr.length) % arr.length; + arr.splice(index, 1); + } + + spliceTwice(arr, index) { + index = (index % arr.length + arr.length) % arr.length; + arr.splice(0, 1, arr.splice(index, 1, arr[0])[0]); + } + + pushSplice(arr) { + for (let index = arr.length; index;) + arr.push(arr.splice(--index, 1)[0]); + } + + push(arr, item) { + arr.push(item); + } + + reverse(arr) { + arr.reverse(); + } +} + +module.exports = NToken; \ No newline at end of file diff --git a/lib/Player.js b/lib/Player.js index 758d0027..92c5283d 100644 --- a/lib/Player.js +++ b/lib/Player.js @@ -36,8 +36,7 @@ class Player { } getNEncoder(data) { - const raw_code = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");'; - this.encodeN = Utils.createFunction('a', raw_code); + this.ntoken_sc = 'var b=a.split("")' + Utils.getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}') + '} return b.join("");'; } } diff --git a/lib/SigDecipher.js b/lib/SigDecipher.js index d16ef9de..6cbfc32a 100644 --- a/lib/SigDecipher.js +++ b/lib/SigDecipher.js @@ -1,13 +1,13 @@ 'use strict'; +const NToken = require('./NToken'); const QueryString = require('querystring'); class SigDecipher { - constructor(url, cver, func_code, encode_n) { + constructor(url, cver, player) { this.url = url; this.cver = cver; - this.func_code = func_code; - this.encode_n = encode_n; + this.player = player; this.func_regex = /(.{2}):function\(.*?\){(.*?)}/g; this.actions_regex = /;.{2}\.(.{2})\(.*?,(.*?)\)/g; } @@ -33,7 +33,7 @@ class SigDecipher { let actions; let signature = args.s.split(''); - while ((actions = this.actions_regex.exec(this.func_code)) !== null) { + while ((actions = this.actions_regex.exec(this.player.sig_decipher_sc)) !== null) { switch (actions[1]) { case functions[0]: reverse(signature, actions[2]); @@ -49,10 +49,11 @@ class SigDecipher { } const url_components = new URL(args.url); + args.sp !== undefined ? url_components.searchParams.set(args.sp, signature.join('')) : url_components.searchParams.set('signature', signature.join('')); url_components.searchParams.set('cver', this.cver); url_components.searchParams.set('ratebypass', 'yes'); - url_components.searchParams.set('n', this.encode_n(url_components.searchParams.get('n'))); + url_components.searchParams.set('n', new NToken(this.player.ntoken_sc).transform(url_components.searchParams.get('n'))); return url_components.toString(); } @@ -60,7 +61,7 @@ class SigDecipher { let func; let func_name = []; - while ((func = this.func_regex.exec(this.func_code)) !== null) { + while ((func = this.func_regex.exec(this.player.sig_decipher_sc)) !== null) { if (func[0].includes('reverse()')) { func_name[0] = func[1]; } else if (func[0].includes('splice')) { diff --git a/lib/Utils.js b/lib/Utils.js index 88e12029..7cb8cd89 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -37,13 +37,9 @@ function escapeStringRegexp(string) { return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d"); } -function createFunction(input, raw_code) { // I hate this - return new Function(input, raw_code); -} - function encodeNotificationPref(channel_id, index) { const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); - + const buf = youtube_proto.NotificationPreferences.encode({ channel_id, pref_id: { @@ -52,24 +48,24 @@ function encodeNotificationPref(channel_id, index) { number_0: 0, number_1: 4 }); - + return encodeURIComponent(Buffer.from(buf).toString('base64')); } function generateMessageParams(channel_id, video_id) { const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/proto/youtube.proto`)); - + const buf = youtube_proto.LiveMessageParams.encode({ params: { ids: { - channel_id, - video_id + channel_id, + video_id } }, number_0: 1, number_1: 4 }); - + return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64'); } @@ -87,4 +83,4 @@ function generateCommentParams(video_id) { return encodeURIComponent(Buffer.from(buf).toString('base64')); } -module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, generateMessageParams, generateCommentParams, encodeNotificationPref }; \ No newline at end of file +module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, generateMessageParams, generateCommentParams, encodeNotificationPref }; \ No newline at end of file