From 7e86bb15e05b69ed70036d7e521c387ba40118dc Mon Sep 17 00:00:00 2001 From: LuanRT Date: Sat, 18 Dec 2021 12:03:44 -0300 Subject: [PATCH] refactor (OAuth): a simpler & more efficient auth system --- README.md | 10 +++--- lib/Actions.js | 64 ++++++++++++++++++------------------ lib/Innertube.js | 82 ++++++++++++++++++++-------------------------- lib/OAuth.js | 85 +++++++++++++++++++++++++++++------------------- 4 files changed, 123 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index d28855f4..92d8d3e1 100644 --- a/README.md +++ b/README.md @@ -650,19 +650,17 @@ async function start() { const creds = fs.existsSync(creds_path) && JSON.parse(fs.readFileSync(creds_path).toString()) || {}; const youtube = await new Innertube(); - // Only triggered when signing-in. - youtube.on('auth', (data) => { + youtube.ev.on('auth', (data) => { if (data.status === 'AUTHORIZATION_PENDING') { console.info(`Hello!\nOn your phone or computer, go to ${data.verification_url} and enter the code ${data.code}`); } else if (data.status === 'SUCCESS') { - fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token, expires: data.expires })); + fs.writeFileSync(creds_path, JSON.stringify(data.credentials)); console.info('Successfully signed-in, enjoy!'); } }); - // Triggered whenever the access token is refreshed. - youtube.on('update-credentials', (data) => { - fs.writeFileSync(creds_path, JSON.stringify({ access_token: data.access_token, refresh_token: data.refresh_token, expires: data.expires })); + youtube.ev.on('update-credentials', (data) => { + fs.writeFileSync(creds_path, JSON.stringify(data.credentials)); console.info('Credentials updated!', data); }); diff --git a/lib/Actions.js b/lib/Actions.js index 3a64c009..e0f1a592 100644 --- a/lib/Actions.js +++ b/lib/Actions.js @@ -6,8 +6,8 @@ const Utils = require('./Utils'); const Constants = require('./Constants'); async function engage(session, engagement_type, args = {}) { - if (!session.logged_in) throw new Error('You are not logged in'); - + if (!session.logged_in) throw new Error('You are not signed-in'); + let data; switch (engagement_type) { case 'like/like': @@ -37,11 +37,11 @@ async function engage(session, engagement_type, args = {}) { default: } - const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${engagement_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, id: args.video_id, data })).catch((error) => error); - + const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${engagement_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, + JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, id: args.video_id, data })).catch((error) => error); + if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; - + return { success: true, status_code: response.status @@ -49,7 +49,7 @@ async function engage(session, engagement_type, args = {}) { } async function browse(session, action_type) { - if (!session.logged_in) throw new Error('You are not logged in'); + if (!session.logged_in) throw new Error('You are not signed-in'); let data; switch (action_type) { // TODO: Handle more actions @@ -62,11 +62,11 @@ async function browse(session, action_type) { default: } - const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); - + const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/browse${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, + JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); + if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; - + return { success: true, status_code: response.status, @@ -78,13 +78,13 @@ async function search(session, args = {}) { if (!args.query) throw new Error('No query was provided'); const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/search${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify({ - context: session.context, - params: Utils.encodeFilter(args.options.period, args.options.duration, args.options.order), - query: args.query - }), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); + JSON.stringify({ + context: session.context, + params: Utils.encodeFilter(args.options.period, args.options.duration, args.options.order), + query: args.query + }), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; - + return { success: true, status_code: response.status, @@ -93,8 +93,8 @@ async function search(session, args = {}) { } async function notifications(session, action_type, args = {}) { - if (!session.logged_in) throw new Error('You are not logged in'); - + if (!session.logged_in) throw new Error('You are not signed-in'); + let data; switch (action_type) { @@ -119,11 +119,11 @@ async function notifications(session, action_type, args = {}) { default: } - const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); + const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/notification/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, + JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; if (action_type === 'modify_channel_preference') return { success: true, status_code: response.status }; - + return { success: true, status_code: response.status, @@ -158,10 +158,10 @@ async function livechat(session, action_type, args = {}) { default: } - const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, params: args.params })).catch((error) => error); + const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/${action_type}${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, + JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session, params: args.params })).catch((error) => error); if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; - + return { success: true, status_code: response.status, @@ -171,10 +171,10 @@ async function livechat(session, action_type, args = {}) { async function getVideoInfo(session, args = {}) { let response; - + !args.is_desktop && (response = await Axios.get(`${Constants.URLS.YT_WATCH_PAGE}?v=${args.id}&t=8s&pbj=1`, Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: false })).catch((error) => error)) || - (response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context)), Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: true })).catch((error) => error)); + (response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/player${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, + JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context)), Constants.INNERTUBE_REQOPTS({ session, id: args.id, desktop: true })).catch((error) => error)); if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`); return response.data; @@ -196,10 +196,10 @@ async function getContinuation(session, info = {}) { data.captionsRequested = false; } - const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, - JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); + const response = await Axios.post(`${Constants.URLS.YT_BASE_URL}/youtubei/v1/next${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, + JSON.stringify(data), Constants.INNERTUBE_REQOPTS({ session })).catch((error) => error); if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; - + return { success: true, status_code: response.status, @@ -207,4 +207,4 @@ async function getContinuation(session, info = {}) { }; } -module.exports = { engage, browse, search, notifications, livechat, getVideoInfo, getContinuation }; \ No newline at end of file +module.exports = { engage, browse, search, notifications, livechat, getContinuation }; \ No newline at end of file diff --git a/lib/Innertube.js b/lib/Innertube.js index 93e0c39c..004f77ca 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -14,9 +14,8 @@ const EventEmitter = require('events'); const TimeToSeconds = require('time-to-seconds'); const CancelToken = Axios.CancelToken; -class Innertube extends EventEmitter { +class Innertube { constructor(cookie) { - super(); this.cookie = cookie || ''; this.retry_count = 0; return this.init(); @@ -46,6 +45,8 @@ class Innertube extends EventEmitter { this.auth_apisid = Utils.getStringBetweenStrings(this.cookie, 'PAPISID=', ';'); this.auth_apisid = Utils.generateSidAuth(this.auth_apisid); } + + this.ev = new EventEmitter(); } else { this.retry_count += 1; if (this.retry_count >= 10) throw new Error('Could not retrieve Innertube data'); @@ -59,54 +60,43 @@ class Innertube extends EventEmitter { return this; } - signIn(credentials = {}) { + signIn(auth_info = {}) { return new Promise(async (resolve, reject) => { - const oauth = new OAuth(credentials); - if (credentials.access_token && credentials.refresh_token) { - let token_validity = await oauth.checkTokenValidity(credentials.expires); - if (token_validity === 'VALID') { - this.access_token = credentials.access_token; - this.refresh_token = credentials.refresh_token; - this.logged_in = true; - resolve(); - } else { - oauth.refreshAccessToken(credentials.refresh_token); - oauth.on('refresh-token', (data) => { - this.access_token = data.access_token; - this.refresh_token = credentials.refresh_token; - this.logged_in = true; + const oauth = new OAuth(auth_info); + if (auth_info.access_token) { + const is_valid = await oauth.isTokenValid(auth_info.expires); - const expiration_date = new Date(new Date().getTime() + data.expires * 1000); + if (!is_valid) { + const new_tokens = await oauth.refreshAccessToken(auth_info.refresh_token); + auth_info.refresh_token = new_tokens.credentials.refresh_token; + auth_info.access_token = new_tokens.credentials.access_token; - this.emit('update-credentials', { - access_token: data.access_token, - refresh_token: credentials.refresh_token, - expires: expiration_date, - status: data.status - }); - - resolve(); + this.ev.emit('update-credentials', { + credentials: new_tokens.credentials, + status: new_tokens.status }); } + + this.access_token = auth_info.access_token; + this.refresh_token = auth_info.refresh_token; + this.logged_in = true; + + resolve(); } else { oauth.on('auth', (data) => { if (data.status === 'SUCCESS') { - this.access_token = data.access_token; - this.refresh_token = data.refresh_token; + this.access_token = data.credentials.access_token; + this.refresh_token = data.credentials.refresh_token; this.logged_in = true; - const expiration_date = new Date(new Date().getTime() + data.expires * 1000); - - this.emit('auth', { - access_token: data.access_token, - refresh_token: data.refresh_token, - expires: expiration_date, + this.ev.emit('auth', { + credentials: data.credentials, status: data.status }); resolve(); } else { - this.emit('auth', data); + this.ev.emit('auth', data); } }); } @@ -117,16 +107,16 @@ class Innertube extends EventEmitter { const response = await Actions.search(this, { query, options }); if (!response.success) throw new Error(`Could not search on YouTube: ${response.message}`); - let content = response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents; - let search_response = {}; + const content = response.data.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents; + const search = {}; - search_response.search_metadata = {}; - search_response.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query; - search_response.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query; - search_response.search_metadata.estimated_results = parseInt(response.data.estimatedResults); - search_response.videos = content.map((data) => { + search.search_metadata = {}; + search.search_metadata.query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.originalQuery.simpleText : query; + search.search_metadata.corrected_query = content[0].showingResultsForRenderer ? content[0].showingResultsForRenderer.correctedQueryEndpoint.searchEndpoint.query : query; + search.search_metadata.estimated_results = parseInt(response.data.estimatedResults); + search.videos = content.map((data) => { if (!data.videoRenderer) return; - let video = data.videoRenderer; + const video = data.videoRenderer; return { title: video.title.runs[0].text, description: video.detailedMetadataSnippets && video.detailedMetadataSnippets[0].snippetText.runs.map((item) => item.text).join('') || 'N/A', @@ -152,7 +142,7 @@ class Innertube extends EventEmitter { } }; }).filter((video_block) => video_block !== undefined); - return search_response; + return search; } async getDetails(id) { @@ -432,8 +422,8 @@ class Innertube extends EventEmitter { response.data.on('data', (chunk) => { downloaded_size += chunk.length; - let size = (selected_format.contentLength / 1024 / 1024).toFixed(2); - let percentage = Math.floor((downloaded_size / selected_format.contentLength) * 100); + const size = (selected_format.contentLength / 1024 / 1024).toFixed(2); + const percentage = Math.floor((downloaded_size / selected_format.contentLength) * 100); stream.emit('progress', { chunk_size: chunk.length, downloaded_size: (downloaded_size / 1024 / 1024).toFixed(2), percentage, size, raw_data: { chunk_size: chunk.length, downloaded: downloaded_size, size: response.headers['content-length'] } }); }); diff --git a/lib/OAuth.js b/lib/OAuth.js index 43f40c0c..167187d4 100644 --- a/lib/OAuth.js +++ b/lib/OAuth.js @@ -7,7 +7,7 @@ const EventEmitter = require('events'); const Uuid = require('uuid'); class OAuth extends EventEmitter { - constructor(creds) { + constructor(auth_info) { super(); this.refresh_interval = 5; @@ -22,7 +22,7 @@ class OAuth extends EventEmitter { this.auth_script_regex = /