Files
YouTube.js/lib/core/Actions.ts
Daniel Wykerd fb68e6bcfe feat!: better cross runtime support (#97)
* refactor: remove dependancies

removes node-forge and uuid in favor of Web APIs

* refactor!: commonjs to es6

To aid with #93 I will make all my changes in TypeScript instead.
This is the first step into making that happen.

Used: https://github.com/wessberg/cjstoesm

* refactor!: NToken and Signature TS files

Bring this PR up to speed with #93

* feat: cross platform cache (WIP)

this is untested!
should remove idb as dependecy.

* feat: EventEmitter polyfill

* refactor: remove events

* feat: HTTPClient based on Fetch API (WIP)

* refactor!: parsers refactor (WIP)

Initial TS support for parsers as per #93

This adds several type safety checks to the parser which'll help to
ensure valid data is returned by the parser.

* refactor!: parsers refactor (WIP)

Bring more in line with the existing implementations & make less verbose

* refactor!: parser refactor

I was overcomplicating things, this is much simpler and compatible with
the existing JS API

* fix: some missed parsers while refactoring

* fix: better type inferance for parseResponse

* feat(TS): typesafe YTNode casts

* feat: more type safety in YTNode and Parser

* refactor: VideoInfo download with fetch & TS (WIP)

Again, this also does some work for #93

* fix: LiveChat in VideoInfo

* refactor!: more typesafety in parser

* refactor!: VideoInfo almost completed

* refactor!: player and session refactors

- Remove the Player class' dependance on Session.
- Add additional context to the Session.

* refactor!: move auth logic to Session (WIP)

* refactor: TS port for Actions and Innertube

My fingers hurt from typing out all those types :-P

* refactor: NavigationEndpoint TS

this is still a WIP and should be improved.
NavigationEndpoint should probably be refactored further.

* refactor!: VideoInfo compiles without errors

* chore: delete old player

* fix: import errors

It compiles and runs!!

* fix: Utils import fixes

* fix: several runtime errors

* fix: video streaming

* chore: remove console.log debugging

Whoops, forgot to remove these before I pushed the previous commit

* chore: remove old unused dependencies

* fix: typescript errors

Now emitting declarations and source maps

* refactor: TS feed

* chore: delete old Feed

* refactor: move streamToIterable into Utils

* refactor: AccountManager TS

* refactor: FilterableFeed to TS

* refactor: InteractionManager to TS

* refactor: PlaylistManager to TS

* refactor: TabbedFeed to TS

* refactor: Music to TS (WIP)

more work to be done, see TODO comments

* fix: getting the tests to pass (6/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1152 ms)
      ✕ Should search on YouTube Music (705 ms)
      ✕ Should retrieve YouTube search suggestions (722 ms)
      ✓ Should retrieve YouTube Music search suggestions (233 ms)
    Comments
      ✓ Should retrieve comments (585 ms)
      ✕ Should retrieve next batch of comments (221 ms)
      ✕ Should retrieve comment replies (1 ms)
    General
      ✕ Should retrieve playlist with YouTube (732 ms)
      ✓ Should retrieve home feed (838 ms)
      ✓ Should retrieve trending content (543 ms)
      ✓ Should retrieve video info (639 ms)
      ✕ Should download video (5 ms)

* fix: tests (7/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1984 ms)
      ✕ Should search on YouTube Music (1139 ms)
      ✕ Should retrieve YouTube search suggestions (1433 ms)
      ✓ Should retrieve YouTube Music search suggestions (529 ms)
    Comments
      ✓ Should retrieve comments (324 ms)
      ✓ Should retrieve next batch of comments (395 ms)
      ✕ Should retrieve comment replies
    General
      ✕ Should retrieve playlist with YouTube (653 ms)
      ✓ Should retrieve home feed (1085 ms)
      ✓ Should retrieve trending content (513 ms)
      ✓ Should retrieve video info (921 ms)
      ✕ Should download video (3 ms)

* fix: download tests (8/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1293 ms)
      ✕ Should search on YouTube Music (927 ms)
      ✕ Should retrieve YouTube search suggestions (1250 ms)
      ✓ Should retrieve YouTube Music search suggestions (258 ms)
    Comments
      ✓ Should retrieve comments (803 ms)
      ✓ Should retrieve next batch of comments (511 ms)
      ✕ Should retrieve comment replies
    General
      ✕ Should retrieve playlist with YouTube (528 ms)
      ✓ Should retrieve home feed (1047 ms)
      ✓ Should retrieve trending content (548 ms)
      ✓ Should retrieve video info (825 ms)
      ✓ Should download video (1779 ms)

* fix: tests (9/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1276 ms)
      ✕ Should search on YouTube Music (955 ms)
      ✓ Should retrieve YouTube search suggestions (661 ms)
      ✓ Should retrieve YouTube Music search suggestions (491 ms)
    Comments
      ✓ Should retrieve comments (624 ms)
      ✓ Should retrieve next batch of comments (353 ms)
      ✕ Should retrieve comment replies
    General
      ✕ Should retrieve playlist with YouTube (672 ms)
      ✓ Should retrieve home feed (1277 ms)
      ✓ Should retrieve trending content (999 ms)
      ✓ Should retrieve video info (1106 ms)
      ✓ Should download video (2514 ms)

* feat: key based type validation for parsers

* fix: comments tests pass (10/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (938 ms)
      ✕ Should search on YouTube Music (850 ms)
      ✓ Should retrieve YouTube search suggestions (528 ms)
      ✓ Should retrieve YouTube Music search suggestions (224 ms)
    Comments
      ✓ Should retrieve comments (518 ms)
      ✓ Should retrieve next batch of comments (337 ms)
      ✓ Should retrieve comment replies (358 ms)
    General
      ✕ Should retrieve playlist with YouTube (466 ms)
      ✓ Should retrieve home feed (1051 ms)
      ✓ Should retrieve trending content (623 ms)
      ✓ Should retrieve video info (863 ms)
      ✓ Should download video (2656 ms)

* refactor: type safety checks removing @ts-ignore

* fix: playlist tests pass (11/12)

YouTube.js Tests
    Search
      ✓ Should search on YouTube (991 ms)
      ✕ Should search on YouTube Music (924 ms)
      ✓ Should retrieve YouTube search suggestions (606 ms)
      ✓ Should retrieve YouTube Music search suggestions (225 ms)
    Comments
      ✓ Should retrieve comments (393 ms)
      ✓ Should retrieve next batch of comments (284 ms)
      ✓ Should retrieve comment replies (252 ms)
    General
      ✓ Should retrieve playlist with YouTube (578 ms)
      ✓ Should retrieve home feed (1148 ms)
      ✓ Should retrieve trending content (541 ms)
      ✓ Should retrieve video info (799 ms)
      ✓ Should download video (1419 ms)

* fix: all tests pass for node 🎉

YouTube.js Tests
    Search
      ✓ Should search on YouTube (1053 ms)
      ✓ Should search on YouTube Music (761 ms)
      ✓ Should retrieve YouTube search suggestions (453 ms)
      ✓ Should retrieve YouTube Music search suggestions (221 ms)
    Comments
      ✓ Should retrieve comments (627 ms)
      ✓ Should retrieve next batch of comments (412 ms)
      ✓ Should retrieve comment replies (268 ms)
    General
      ✓ Should retrieve playlist with YouTube (565 ms)
      ✓ Should retrieve home feed (775 ms)
      ✓ Should retrieve trending content (498 ms)
      ✓ Should retrieve video info (875 ms)
      ✓ Should download video (1364 ms)

* build: working Deno bundle

Still need to test whether this bundle works in the browser

* docs: update deno example to download video

* refactor: MusicResponsiveListItem to TS

* docs: TSDoc for Parser helpers

* docs: Parser documentation for TS

* docs: add note about parseItem and parseArray

* test: remove browser tests since they're identical

* feat: browser support and proxy example

* fix: PlaylistManager TS after merge

* feat: in-browser video streaming

* refactor: cleanup the Dash example

* feat: allow custom fetch implementations

* feat: fetch debugger

* fix: OAuth login

* refactor: remove file extensions from imports

* refactor: build scripts

* fix: CustomEvent on node

* fix: LiveChat

* fix: linting

* fix: liniting in build-parser-json

* chore: update test workflow

* fix: NToken errors after lint fixes

* fix: codacy complaints

* docs: update to reflect changes

Definitly needs more work but its a start

* refactor: cleanup imports/exports

* fix: browser example

- Remove user-agent before making request.
- Fix cache on browsers

* fix: cache on node

* fix: stupid mistake

* refactor: Session#signIn to wait untill success

This also splits the 'auth' event up into 3 distinct events:
- 'auth' -> fired on success
- 'auth-pending' -> fired when pending authentication
- 'auth-error' -> fired when an error occurred

* refactor: freeze Constants

* refactor: cleanup HTTPClient Request

* refactor: debugFetch readability

* chore: lint

* refactor: replace jsdoc with tsdoc eslint plugin

remove @param annotations without descriptions

* fix: bunch of liniting warnings

* refactor: better inference on YTNode#is

As suggested by @MasterOfBob777

* fix: linting warnings

* revert: undici import

* refactor: rename `list_type` to `item_type`
2022-07-20 14:06:12 -03:00

700 lines
20 KiB
TypeScript

import Proto from '../proto/index';
import { hasKeys, InnertubeError, MissingParamError, uuidv4 } from '../utils/Utils';
import Constants from '../utils/Constants';
import Parser, { ParsedResponse } from '../parser/index';
import Session from './Session';
export interface BrowseArgs {
params?: string;
is_ytm?: boolean;
is_ctoken?: boolean;
client?: string;
}
export interface EngageArgs {
video_id?: string;
channel_id?: string;
comment_id?: string;
comment_action?: string;
params?: string;
text?: string;
target_language?: string;
}
export interface AccountArgs {
new_value?: string | boolean; // TODO: is this correct?
setting_item_id?: string;
client?: string;
}
export interface SearchArgs {
query?: string,
options?: {
period?: string,
duration?: string,
order?: string
},
client?: string,
ctoken?: string,
params?: string
filters?: any // TODO: what is this type??
}
export interface AxioslikeResponse {
success: boolean;
status_code: number;
data: any;
}
export type ActionsResponse = Promise<AxioslikeResponse>;
class Actions {
#session;
constructor(session: Session) {
this.#session = session;
}
get session() {
return this.#session;
}
/**
* Mimmics the Axios API using Fetch's Response object.
*/
async #wrap(response: Response) {
return {
success: response.ok,
status_code: response.status,
data: await response.json()
};
}
/**
* Covers `/browse` endpoint, mostly used to access
* YouTube's sections such as the home feed, etc
* and sometimes to retrieve continuations.
*
* @param id - browseId or a continuation token
* @param args - additional arguments
*/
async browse(id: string, args: BrowseArgs = {}) {
if (this.#needsLogin(id) && !this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {};
if (args.params)
data.params = args.params;
if (args.is_ctoken) {
data.continuation = id;
} else {
data.browseId = id;
}
if (args.client) {
data.client = args.client;
}
const response = await this.#session.http.fetch('/browse', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers endpoints used to perform direct interactions
* on YouTube.
*/
async engage(action: string, args: EngageArgs = {}) {
if (!this.#session.logged_in && !args.hasOwnProperty('text'))
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {};
switch (action) {
case 'like/like':
case 'like/dislike':
case 'like/removelike':
if (!hasKeys(args, 'video_id'))
throw new MissingParamError('Arguments lacks video_id');
data.target = {};
data.target.videoId = args.video_id;
if (args.params) {
data.params = args.params;
}
break;
case 'subscription/subscribe':
case 'subscription/unsubscribe':
if (!hasKeys(args, 'channel_id'))
throw new MissingParamError('Arguments lacks channel_id');
data.channelIds = [ args.channel_id ];
data.params = action === 'subscription/subscribe' ? 'EgIIAhgA' : 'CgIIAhgA';
break;
case 'comment/create_comment':
data.commentText = args.text;
if (!hasKeys(args, 'video_id'))
throw new MissingParamError('Arguments lacks video_id');
data.createCommentParams = Proto.encodeCommentParams(args.video_id);
break;
case 'comment/create_comment_reply':
if (!hasKeys(args, 'comment_id', 'video_id', 'text'))
throw new MissingParamError('Arguments lacks comment_id, video_id or text');
data.createReplyParams = Proto.encodeCommentReplyParams(args.comment_id, args.video_id);
data.commentText = args.text;
break;
case 'comment/perform_comment_action':
const target_action = (() => {
switch (args.comment_action) {
case 'like':
return Proto.encodeCommentActionParams(5, args);
case 'dislike':
return Proto.encodeCommentActionParams(4, args);
case 'translate':
return Proto.encodeCommentActionParams(22, args);
default:
break;
}
})();
data.actions = [ target_action ];
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers endpoints related to account management.
*/
async account(action: string, args: AccountArgs = {}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {
client: args.client
};
switch (action) {
case 'account/set_setting':
data.newValue = {
boolValue: args.new_value
};
data.settingItemId = args.setting_item_id;
break;
case 'account/accounts_list':
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Endpoint used for search.
*/
async search(args: SearchArgs = {}) {
const data: Record<string, any> = { client: args.client };
if (args.query) {
data.query = args.query;
}
if (args.ctoken) {
data.continuation = args.ctoken;
}
if (args.params) {
data.params = args.params;
}
if (args.filters) {
if (args.client == 'YTMUSIC') {
data.params = Proto.encodeMusicSearchFilters(args.filters);
} else {
data.params = Proto.encodeSearchFilters(args.filters);
}
}
const response = await this.#session.http.fetch('/search', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Endpoint used fo Shorts' sound search.
*/
async searchSound(args: {
query: string;
}) {
const data = {
query: args.query,
client: 'ANDROID'
};
const response = await this.#session.http.fetch('/sfv/search', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Channel management endpoints.
*
*/
async channel(action: string, args: {
new_name?: string;
new_description?: string;
client?: string;
} = {}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {
client: args.client || 'ANDROID'
};
switch (action) {
case 'channel/edit_name':
data.givenName = args.new_name;
break;
case 'channel/edit_description':
data.description = args.new_description;
break;
case 'channel/get_profile_editor':
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers endpoints used for playlist management.
*
*/
async playlist(action: string, args: {
title?: string;
ids?: string[]; // TODO: this was a string before, but I made it an array, is this correct?
playlist_id?: string;
action?: string;
} = {}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {};
switch (action) {
case 'playlist/create':
data.title = args.title;
data.videoIds = args.ids;
break;
case 'playlist/delete':
data.playlistId = args.playlist_id;
break;
case 'browse/edit_playlist':
if (!hasKeys(args, 'ids'))
throw new MissingParamError('Arguments lacks ids');
data.playlistId = args.playlist_id;
data.actions = args.ids.map((id) => {
switch (args.action) {
case 'ACTION_ADD_VIDEO':
return {
action: args.action,
addedVideoId: id
};
case 'ACTION_REMOVE_VIDEO':
return {
action: args.action,
setVideoId: id
};
default:
break;
}
});
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers endpoints used for notifications management.
*/
async notifications(action: string, args: {
pref?: string;
channel_id?: string;
ctoken?: string;
params?: string
} = {}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {};
switch (action) {
case 'modify_channel_preference':
if (!hasKeys(args, 'channel_id', 'pref'))
throw new MissingParamError('Arguments lacks channel_id or pref');
const pref_types = {
PERSONALIZED: 1,
ALL: 2,
NONE: 3
};
if (!Object.keys(pref_types).includes(args.pref.toUpperCase()))
throw new InnertubeError('Invalid preference type', args.pref);
data.params = Proto.encodeNotificationPref(args.channel_id, pref_types[args.pref.toUpperCase() as keyof typeof pref_types]);
break;
case 'get_notification_menu':
data.notificationsMenuRequestType = 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX';
if (args.ctoken)
data.ctoken = args.ctoken;
break;
case 'record_interactions':
data.serializedRecordNotificationInteractionsRequest = args.params;
break;
case 'get_unseen_count':
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/notification/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers livechat endpoints.
*/
async livechat(action: string, args: {
text?: string;
video_id?: string;
channel_id?: string;
ctoken?: string;
params?: string;
client?: string;
} = {}) {
// TODO: should client be required?
const data: Record<string, any> = { client: args.client };
switch (action) {
case 'live_chat/get_live_chat':
case 'live_chat/get_live_chat_replay':
data.continuation = args.ctoken;
break;
case 'live_chat/send_message':
if (!hasKeys(args, 'channel_id', 'video_id', 'text'))
throw new MissingParamError('Arguments lacks channel_id, video_id or text');
data.params = Proto.encodeMessageParams(args.channel_id, args.video_id);
data.clientMessageId = uuidv4();
data.richMessage = {
textSegments: [ {
text: args.text
} ]
};
break;
case 'live_chat/get_item_context_menu':
// Note: this is currently broken due to a recent refactor
// TODO: this should be implemented
break;
case 'live_chat/moderate':
data.params = args.params;
break;
case 'updated_metadata':
data.videoId = args.video_id;
if (args.ctoken)
data.continuation = args.ctoken;
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Endpoint used to retrieve video thumbnails.
*/
async thumbnails(args: {
video_id: string;
}) {
const data = {
client: 'ANDROID',
videoId: args.video_id
};
const response = await this.#session.http.fetch('/thumbnails', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Place Autocomplete endpoint, found it in the APK but
* doesn't seem to be used anywhere on YouTube (maybe for ads?).
*
* Ex:
* ```js
* const places = await session.actions.geo('place_autocomplete', { input: 'San diego cafe' });
* console.info(places.data);
* ```
*/
async geo(action: string, args: {
input: string;
}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data = {
input: args.input,
client: 'ANDROID'
};
const response = await this.#session.http.fetch(`/geo/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers endpoints used to report content.
*/
async flag(action: string, args: {
action: string;
params?: string;
}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data: Record<string, any> = {};
switch (action) {
case 'flag/flag':
data.action = args.action;
break;
case 'flag/get_form':
data.params = args.params;
break;
default:
throw new InnertubeError('Action not implemented', action);
}
const response = await this.#session.http.fetch(`/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers specific YouTube Music endpoints.
*/
async music(action: string, args: {
input?: string;
}) {
const data = {
input: args.input || '',
client: 'YTMUSIC'
};
const response = await this.#session.http.fetch(`/music/${action}`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Mostly used for pagination and specific operations.
*/
async next(args: {
video_id?: string;
ctoken?: string;
client?: string;
} = {}) {
const data: Record<string, any> = { client: args.client };
if (args.ctoken) {
data.continuation = args.ctoken;
}
if (args.video_id) {
data.videoId = args.video_id;
}
const response = await this.#session.http.fetch('/next', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Used to retrieve video info.
*/
async getVideoInfo(id: string, cpn?: string, client?: string) {
const data: Record<string, any> = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
referer: 'https://www.youtube.com',
currentUrl: `/watch?v=${id}`,
autonavState: 'STATE_OFF',
signatureTimestamp: this.#session.player.sts,
autoCaptionsDefaultOn: false,
html5Preference: 'HTML5_PREF_WANTS',
lactMilliseconds: '-1'
}
},
attestationRequest: {
omitBotguardData: true
},
videoId: id
};
if (client) {
data.client = client;
}
if (cpn) {
data.cpn = cpn;
}
const response = await this.#session.http.fetch('/player', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Covers search suggestion endpoints.
*/
async getSearchSuggestions(client: 'YOUTUBE' | 'YTMUSIC', query: string) {
if (![ 'YOUTUBE', 'YTMUSIC' ].includes(client))
throw new InnertubeError('Invalid client', client);
const response = await ({
YOUTUBE: async () => {
const params = new URLSearchParams({
q: query,
ds: 'yt',
client: 'youtube',
xssi: 't',
oe: 'UTF',
gl: this.#session.context.client.gl,
hl: this.#session.context.client.hl
});
const response = await this.#session.http.fetch(`search?${params.toString()}`, {
baseURL: Constants.URLS.YT_SUGGESTIONS,
method: 'GET'
});
return this.#wrap(response);
},
YTMUSIC: () => this.music('get_search_suggestions', {
input: query
})
}[client])();
return response;
}
/**
* Endpoint used to retrieve user mention suggestions.
*/
async getUserMentionSuggestions(args: {
input: string;
}) {
if (!this.#session.logged_in)
throw new InnertubeError('You are not signed in');
const data = {
input: args.input,
client: 'ANDROID'
};
const response = await this.#session.http.fetch('/get_user_mention_suggestions', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
return this.#wrap(response);
}
/**
* Executes an API call.
* @param action - endpoint
* @param args - call arguments
*/
async execute(action: string, args: {
[key: string]: any;
parse: true;
}) : Promise<ParsedResponse>;
async execute(action: string, args: {
[key: string]: any;
parse?: false;
}) : Promise<ActionsResponse>;
async execute(action: string, args: {
[key: string]: any;
parse?: boolean;
}): Promise<ParsedResponse | ActionsResponse> {
const data = { ...args };
if (Reflect.has(data, 'parse'))
delete data.parse;
if (Reflect.has(data, 'request'))
delete data.request;
if (Reflect.has(data, 'clientActions'))
delete data.clientActions;
if (Reflect.has(data, 'action')) {
data.actions = [ data.action ];
delete data.action;
}
if (Reflect.has(data, 'token')) {
data.continuation = data.token;
delete data.token;
}
const response = await this.#session.http.fetch(action, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
if (args.parse) {
return Parser.parseResponse(await response.json());
}
return this.#wrap(response);
}
#needsLogin(id: string) {
return [
'FElibrary',
'FEhistory',
'FEsubscriptions',
'SPaccount_notifications',
'SPaccount_privacy',
'SPtime_watched'
].includes(id);
}
}
// TODO: maybe do this inferrance in a more elegant way
export default Actions;