Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Kids): Add blockChannel command to easily block channels #503

Merged
merged 7 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/API/interaction-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Handles direct interactions.
* [.unsubscribe(video_id)](#unsubscribe)
* [.comment(video_id, text)](#comment)
* [.translate(text, target_language, args?)](#translate)
* [.setNotificationPreferences(channel_id, type)](#setnotificationpreferences)
* [.setNotificationPreferences(channel_id, type)](#setnotificationpreferences)

<a name="like"></a>
### like(video_id)
Expand Down
16 changes: 15 additions & 1 deletion docs/API/kids.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ YouTube Kids is a modified version of the YouTube app, with a simplified interfa
* [.getInfo(video_id)](#getinfo)
* [.getChannel(channel_id)](#getchannel)
* [.getHomeFeed()](#gethomefeed)
* [.blockChannel(channel_id)](#blockchannel)

<a name="search"></a>
### search(query)
Expand Down Expand Up @@ -110,4 +111,17 @@ Retrieves the home feed.
- Returns available categories.

- `<feed>#page`
- Returns the original InnerTube response(s), parsed and sanitized.
- Returns the original InnerTube response(s), parsed and sanitized.

</details>

<a name="blockChannel"></a>
### blockChannel(channel_id)

Retrieves the list of supervised accounts that the signed-in user has access to and blocks the given channel for each of them.

**Returns:** `Promise.<ApiResponse[]>`

| Param | Type | Description |
| --- | --- | --- |
| channel_id | `string` | Channel id |
23 changes: 23 additions & 0 deletions examples/blockchannel/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Innertube, UniversalCache } from 'youtubei.js';

(async () => {
const yt = await Innertube.create({ cache: new UniversalCache(true, './credcache') });

yt.session.on('auth-pending', (data) => {
console.log(`Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`);
});
yt.session.on('auth', async () => {
console.log('Sign in successful');
await yt.session.oauth.cacheCredentials();
});
yt.session.on('update-credentials', async () => {
await yt.session.oauth.cacheCredentials();
});

// Attempt to sign in
await yt.session.signIn();

// Block Channel for all kids / profiles on the signed-in account.
const resp = await yt.kids.blockChannel('UCpbpfcZfo-hoDAx2m1blFhg');
console.info('Blocked channel for ', resp.length, ' profiles.');
})();
42 changes: 41 additions & 1 deletion src/core/clients/Kids.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import Parser from '../../parser/index.js';
import Channel from '../../parser/ytkids/Channel.js';
import HomeFeed from '../../parser/ytkids/HomeFeed.js';
import Search from '../../parser/ytkids/Search.js';
import VideoInfo from '../../parser/ytkids/VideoInfo.js';
import type Session from '../Session.js';
import { type ApiResponse } from '../Actions.js';

import { generateRandomString } from '../../utils/Utils.js';
import { InnertubeError, generateRandomString } from '../../utils/Utils.js';

import {
BrowseEndpoint, NextEndpoint,
PlayerEndpoint, SearchEndpoint
} from '../endpoints/index.js';

import { BlocklistPickerEndpoint } from '../endpoints/kids/index.js';

import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.js';

export default class Kids {
#session: Session;

Expand Down Expand Up @@ -80,4 +86,38 @@ export default class Kids {
);
return new HomeFeed(this.#session.actions, response);
}

/**
* Retrieves the list of supervised accounts that the signed-in user has
* access to, and blocks the given channel for each of them.
* @param channel_id - The channel id to block.
* @returns A list of API responses.
*/
async blockChannel(channel_id: string): Promise<ApiResponse[]> {
if (!this.#session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');

const blocklist_payload = BlocklistPickerEndpoint.build({ channel_id: channel_id });
const response = await this.#session.actions.execute(BlocklistPickerEndpoint.PATH, blocklist_payload );
const popup = response.data.command.confirmDialogEndpoint;
const popup_fragment = { contents: popup.content, engagementPanels: [] };
const kid_picker = Parser.parseResponse(popup_fragment);
const kids = kid_picker.contents_memo?.getType(KidsBlocklistPickerItem);

if (!kids)
throw new InnertubeError('Could not find any kids profiles or supervised accounts.');

// Iterate through the kids and block the channel if not already blocked.
const responses: ApiResponse[] = [];

for (const kid of kids) {
if (!kid.block_button?.is_toggled) {
kid.setActions(this.#session.actions);
// Block channel and add to the response list.
responses.push(await kid.blockChannel());
}
}

return responses;
}
}
3 changes: 2 additions & 1 deletion src/core/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export * as Music from './music/index.js';
export * as Notification from './notification/index.js';
export * as Playlist from './playlist/index.js';
export * as Subscription from './subscription/index.js';
export * as Upload from './upload/index.js';
export * as Upload from './upload/index.js';
export * as Kids from './kids/index.js';
12 changes: 12 additions & 0 deletions src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { IBlocklistPickerRequest, BlocklistPickerRequestEndpointOptions } from '../../../types/index.js';

export const PATH = '/kids/get_kids_blocklist_picker';

/**
* Builds a `/kids/get_kids_blocklist_picker` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: BlocklistPickerRequestEndpointOptions): IBlocklistPickerRequest {
return { blockedForKidsContent: { external_channel_id: options.channel_id } };
}
1 change: 1 addition & 0 deletions src/core/endpoints/kids/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.js';
2 changes: 1 addition & 1 deletion src/parser/classes/ToggleButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class ToggleButton extends YTNode {
this.toggled_tooltip = data.toggledTooltip;
this.is_toggled = data.isToggled;
this.is_disabled = data.isDisabled;
this.icon_type = data.defaultIcon.iconType;
this.icon_type = data.defaultIcon?.iconType;

const acc_label =
data?.defaultText?.accessibility?.accessibilityData?.label ||
Expand Down
22 changes: 22 additions & 0 deletions src/parser/classes/ytkids/KidsBlocklistPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Text from '../misc/Text.js';
import { YTNode } from '../../helpers.js';
import Button from '../Button.js';
import Parser, { type RawNode } from '../../index.js';
import KidsBlocklistPickerItem from './KidsBlocklistPickerItem.js';

export default class KidsBlocklistPicker extends YTNode {
static type = 'KidsBlocklistPicker';

title: Text;
child_rows: KidsBlocklistPickerItem[] | null;
done_button: Button | null;
successful_toast_action_message: Text;

constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.child_rows = Parser.parse(data.childRows, true, [ KidsBlocklistPickerItem ]);
this.done_button = Parser.parseItem(data.doneButton, [ Button ]);
this.successful_toast_action_message = new Text(data.successfulToastActionMessage);
}
}
49 changes: 49 additions & 0 deletions src/parser/classes/ytkids/KidsBlocklistPickerItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Text from '../misc/Text.js';
import { YTNode } from '../../helpers.js';
import Parser, { type RawNode } from '../../index.js';
import ToggleButton from '../ToggleButton.js';
import Thumbnail from '../misc/Thumbnail.js';
import type Actions from '../../../core/Actions.js';
import { InnertubeError } from '../../../utils/Utils.js';
import { type ApiResponse } from '../../../core/Actions.js';

export default class KidsBlocklistPickerItem extends YTNode {
static type = 'KidsBlocklistPickerItem';

#actions?: Actions;

child_display_name: Text;
child_account_description: Text;
avatar: Thumbnail[];
block_button: ToggleButton | null;
blocked_entity_key: string;

constructor(data: RawNode) {
super();
this.child_display_name = new Text(data.childDisplayName);
this.child_account_description = new Text(data.childAccountDescription);
this.avatar = Thumbnail.fromResponse(data.avatar);
this.block_button = Parser.parseItem(data.blockButton, [ ToggleButton ]);
this.blocked_entity_key = data.blockedEntityKey;
}

async blockChannel(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('An active caller must be provide to perform this operation.');

const button = this.block_button;

if (!button)
throw new InnertubeError('Block button was not found.', { child_display_name: this.child_display_name });

if (button.is_toggled)
throw new InnertubeError('This channel is already blocked.', { child_display_name: this.child_display_name });

const response = await button.endpoint.call(this.#actions, { parse: false });
return response;
}

setActions(actions: Actions | undefined) {
this.#actions = actions;
}
}
2 changes: 2 additions & 0 deletions src/parser/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ export { default as WatchNextEndScreen } from './classes/WatchNextEndScreen.js';
export { default as WatchNextTabbedResults } from './classes/WatchNextTabbedResults.js';
export { default as YpcTrailer } from './classes/YpcTrailer.js';
export { default as AnchoredSection } from './classes/ytkids/AnchoredSection.js';
export { default as KidsBlocklistPicker } from './classes/ytkids/KidsBlocklistPicker.js';
export { default as KidsBlocklistPickerItem } from './classes/ytkids/KidsBlocklistPickerItem.js';
export { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.js';
export { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.js';
export { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.js';
10 changes: 10 additions & 0 deletions src/types/Endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,14 @@ export interface IEditPlaylistRequest extends ObjectSnakeToCamel<EditPlaylistEnd
playlistDescription?: string;
playlistName?: string;
}[];
}

export type BlocklistPickerRequestEndpointOptions = {
channel_id: string;
}

export type IBlocklistPickerRequest = {
blockedForKidsContent: {
external_channel_id: string;
}
}
Loading