diff --git a/lib/Actions.js b/lib/Actions.js index 4365923b..94957e39 100644 --- a/lib/Actions.js +++ b/lib/Actions.js @@ -4,168 +4,89 @@ const Axios = require('axios'); const Utils = require('./Utils'); const Constants = require('./Constants'); -async function subscribe(session, video_id, channel_id) { - if (!session.logged_in) throw new Error('You must be logged in to subscribe to a channel'); - let data = { - context: session.context, - channelIds: [channel_id] - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/subscription/subscribe${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else if (response.data.responseContext) { - return { - success: true, - status_code: response.status, - }; +async function engage(session, engagement_type, args = {}) { + if (!session.logged_in) throw new Error('You must be signed-in to interact with a video/channel'); + let data = {}; + switch (engagement_type) { + case 'like/like': + case 'like/dislike': + case 'like/removelike': + data = { + context: session.context, + target: { + videoId: args.video_id + } + }; + break; + case 'subscription/subscribe': + case 'subscription/unsubscribe': + data = { + context: session.context, + channelIds: [args.channel_id] + }; + break; + case 'comment/create_comment': + data = { + context: session.context, + commentText: args.text, + createCommentParams: Utils.encodeVideoId(args.video_id) + }; + break; + 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_request_opts({ 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 + }; } -async function unsubscribe(session, video_id, channel_id) { - if (!session.logged_in) throw new Error('You must be logged in to unsubscribe from a channel'); - let data = { - context: session.context, - channelIds: [channel_id] - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/subscription/unsubscribe${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else if (response.data.responseContext) { - return { - success: true, - status_code: response.status, - }; - } -} - -async function likeVideo(session, video_id) { - if (!session.logged_in) throw new Error('You must be logged in to like a video'); - let data = { - context: session.context, - target: { - videoId: video_id - } - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/like/like${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else if (response.data.responseContext) { - return { - success: true, - status_code: response.status, - }; - } -} - -async function dislikeVideo(session, video_id) { - if (!session.logged_in) throw new Error('You must be logged in to like a video'); - let data = { - context: session.context, - target: { - videoId: video_id - } - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/like/dislike${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else if (response.data.responseContext) { - return { - success: true, - status_code: response.status, - }; - } -} - -async function removeLike(session, video_id) { - if (!session.logged_in) throw new Error('You must be logged in to remove a like/dislike.'); - let data = { - context: session.context, - target: { - videoId: video_id - } - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/like/removelike${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else if (response.data.responseContext) { - return { - success: true, - status_code: response.status, - }; - } -} - -async function commentVideo(session, video_id, text) { - if (!text) throw new Error('No text was provided'); - if (!session.logged_in) throw new Error('You must be logged in to post a comment.'); - - let data = { - context: session.context, - commentText: text, - createCommentParams: Utils.encodeId(video_id) - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/comment/create_comment${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, id: video_id, data })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else if (response.data.responseContext) { - return { - success: true, - status_code: response.status, - }; - } -} - -async function getNotifications(session) { +async function notifications(session, action_type, args = {}) { if (!session.logged_in) throw new Error('You must be logged in to fetch notifications'); - let data = { - context: session.context, - notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' - }; - - const response = await Axios.post(`${Constants.urls.YT_BASE_URL}/youtubei/v1/notification/get_notification_menu${session.logged_in && session.cookie.length < 1 ? '' : `?key=${session.key}`}`, JSON.stringify(data), Constants.innertube_request_opts({ session, data, desktop: true })).catch((error) => error); - if (response instanceof Error) { - return { - success: false, - status_code: response.response.status, - message: response.message - }; - } else { - return { - success: true, - status_code: response.status, - data: response.data - }; + let data; + switch (action_type) { + case 'modify_channel_preference': + let pref_types = { ALL: 0, NONE: 1, PERSONALIZED: 2 }; + data = { + context: session.context, + params: Utils.encodeChannelId(args.channel_id, pref_types[args.pref.toUpperCase()]) + }; + break; + case 'get_notification_menu': + data = { + context: session.context, + notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' + }; + break; + case 'get_unseen_count': + data = { + context: session.context + }; + break; + 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_request_opts({ session, data, desktop: true })).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, + data: response.data + }; } -module.exports = { subscribe, unsubscribe, likeVideo, dislikeVideo, removeLike, commentVideo, getNotifications }; \ No newline at end of file +async function getContinuation(session, continuation_token) { + let data = { context: session.context }; + 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_request_opts({ session, data, desktop: true })).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, + data: response.data + }; +} + +module.exports = { engage, notifications, getContinuation }; \ No newline at end of file diff --git a/lib/Constants.js b/lib/Constants.js index e7a6a2f0..ca1bd13f 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -161,15 +161,16 @@ const formatVideoData = (data, context, desktop) => { video_details.description = data[2].playerResponse.videoDetails.shortDescription; video_details.thumbnail = data[2].playerResponse.videoDetails.thumbnail.thumbnails.slice(-1)[0]; - // actions - video_details.like = like => {}; - video_details.dislike = dislike => {}; - video_details.removeLike = remove_like => {}; - video_details.subscribe = subscribe => {}; - video_details.unsubscribe = unsubscribe => {}; - video_details.comment = comment => {}; + // Actions + video_details.like = () => {}; + video_details.dislike = () => {}; + video_details.removeLike = () => {}; + video_details.subscribe = () => {}; + video_details.unsubscribe = () => {}; + video_details.comment = () => {}; + video_details.setNotificationPref = () => {}; - // additional metadata + // Additional metadata video_details.metadata = metadata; } return video_details; diff --git a/lib/Innertube.js b/lib/Innertube.js index d953abd1..5bfb5822 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -137,20 +137,22 @@ class Innertube extends EventEmitter { const data = await this.requestVideoInfo(id, false); const video_data = Constants.formatVideoData(data, this, false); - video_data.like = like => Actions.likeVideo(this, id); - video_data.dislike = dislike => Actions.dislikeVideo(this, id); - video_data.removeLike = remove_like => Actions.removeLike(this, id); - video_data.subscribe = subscribe => Actions.subscribe(this, id, video_data.metadata.channel_id); - video_data.unsubscribe = unsubscribe => Actions.unsubscribe(this, id, video_data.metadata.channel_id); - video_data.comment = comment => Actions.commentVideo(this, id, comment); + video_data.like = () => Actions.engage(this, 'like/like', { video_id: id }); + video_data.dislike = () => Actions.engage(this, 'like/dislike', { video_id: id }); + video_data.removeLike = () => Actions.engage(this, 'like/removelike', { video_id: id }); + video_data.subscribe = () => Actions.engage(this, 'subscription/subscribe', { video_id: id, channel_id: video_data.metadata.channel_id }); + video_data.unsubscribe = () => Actions.engage(this, 'subscription/unsubscribe', { video_id: id, channel_id: video_data.metadata.channel_id }); + video_data.comment = text => Actions.engage(this, 'comment/create_comment', { video_id: id, text }); + video_data.setNotificationPref = pref => Actions.notifications(this, 'modify_channel_preference', { channel_id: video_data.metadata.channel_id, pref: pref || 'NONE' }); return video_data; } async getNotifications() { - const response = await Actions.getNotifications(this); - const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0].multiPageMenuNotificationSectionRenderer.items; - return contents.map((notification) => { + const response = await Actions.notifications(this, 'get_notification_menu'); + const contents = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0]; + if (!contents.multiPageMenuNotificationSectionRenderer) return { error: 'You don\'t have any notification.' }; + return contents.multiPageMenuNotificationSectionRenderer.items.map((notification) => { if (!notification.notificationRenderer) return; notification = notification.notificationRenderer; return { @@ -159,13 +161,18 @@ class Innertube extends EventEmitter { channel_name: notification.contextualMenu.menuRenderer.items[1].menuServiceItemRenderer.text.runs[1].text, channel_thumbnail: notification.thumbnail.thumbnails[0], video_thumbnail: notification.videoThumbnail.thumbnails[0], - video_url: 'https://youtu.be/' + notification.navigationEndpoint.watchEndpoint.videoId, + video_url: `https://youtu.be/${notification.navigationEndpoint.watchEndpoint.videoId}`, read: notification.read, notification_id: notification.notificationId, }; }).filter((notification_block) => notification_block); } + async getUnseenNotificationsCount() { + const response = await Actions.notifications(this, 'get_unseen_count'); + return response.data.unseenCount; + } + async requestVideoInfo(id, desktop) { let response; if (!desktop) { diff --git a/lib/Utils.js b/lib/Utils.js index c36260a2..2c023662 100644 --- a/lib/Utils.js +++ b/lib/Utils.js @@ -39,8 +39,21 @@ function createFunction(input, raw_code) { // I hate this return new Function(input, raw_code); } -function encodeId(id) { - return encodeURI(new Buffer.from(` ` + id + `*`).toString('base64').replace('==', '') + 'BQBw=='); +function encodeChannelId(id, notification_pref) { + const buff_start = ` +`; + const buff_end = [ + ``, // all + ``, // none + ``, // personalized + ]; + + let buff = Buffer.from(`${buff_start}${id}${buff_end[notification_pref]}`); + return encodeURIComponent(`${buff.toString('base64')}GAAgBA==`); } -module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeId }; \ No newline at end of file +function encodeVideoId(id) { + return encodeURIComponent(`${Buffer.from(` ` + id + `*`).toString('base64').slice(0, -1)}BQBw==`); +} + +module.exports = { getRandomUserAgent, generateSidAuth, getStringBetweenStrings, createFunction, encodeVideoId, encodeChannelId }; \ No newline at end of file