feat(Innertube): Add getAttestationChallenge (#869)

* feat(Innertube): Add `getAttestationChallenge`

YouTube has recently started using this InnerTube endpoint to fetch challenges for WebPo generation.

* chore: lint
This commit is contained in:
Luan
2025-01-11 19:42:18 -03:00
committed by GitHub
parent 50539bca9c
commit 33c27ddcb5
6 changed files with 85 additions and 10 deletions

View File

@@ -25,7 +25,14 @@ import * as Constants from './utils/Constants.js';
import { generateRandomString, InnertubeError, throwIfMissing, u8ToBase64 } from './utils/Utils.js';
import type { ApiResponse } from './core/Actions.js';
import type { DownloadOptions, FormatOptions, InnerTubeClient, InnerTubeConfig, SearchFilters } from './types/index.js';
import type {
DownloadOptions,
EngagementType,
FormatOptions,
InnerTubeClient,
InnerTubeConfig,
SearchFilters
} from './types/index.js';
import type { IBrowseResponse, IParsedResponse } from './parser/index.js';
import type Format from './parser/classes/misc/Format.js';
@@ -480,7 +487,7 @@ export default class Innertube {
}
/**
* Get comments for a community post.
* Gets the comments of a post.
*/
async getPostComments(post_id: string, channel_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments> {
throwIfMissing({ post_id, channel_id });
@@ -531,6 +538,20 @@ export default class Innertube {
return new Comments(this.actions, response.data);
}
/**
* Fetches an attestation challenge.
*/
async getAttestationChallenge(engagement_type: EngagementType, ids?: Record<string, any>[]) {
const payload: Record<string, any> = {
engagementType: engagement_type
};
if (ids)
payload.ids = ids;
return this.actions.execute('/att/get', { parse: true, ...payload });
}
/**
* Utility method to call an endpoint without having to use {@link Actions}.
*/

View File

@@ -1,5 +1,6 @@
import type {
IBrowseResponse,
IGetChallengeResponse,
IGetNotificationsMenuResponse,
INextResponse,
IParsedResponse,
@@ -28,6 +29,7 @@ export type InnertubeEndpoint =
| '/reel'
| '/updated_metadata'
| '/notification/get_notification_menu'
| '/att/get'
| string;
export type ParsedResponse<T> =
@@ -38,7 +40,8 @@ export type ParsedResponse<T> =
T extends '/updated_metadata' ? IUpdatedMetadataResponse :
T extends '/navigation/resolve_url' ? IResolveURLResponse :
T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
IParsedResponse;
T extends '/att/get' ? IGetChallengeResponse :
IParsedResponse;
export default class Actions {
public session: Session;
@@ -117,7 +120,7 @@ export default class Actions {
if (this.#needsLogin(data.browseId) && !this.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
}
if (Reflect.has(data, 'skip_auth_check'))
delete data.skip_auth_check;

View File

@@ -479,6 +479,25 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
if (engagement_panels.length) {
parsed_data.engagement_panels = engagement_panels;
}
if (data.bgChallenge) {
const interpreter_url = {
private_do_not_access_or_else_trusted_resource_url_wrapped_value: data.bgChallenge.interpreterUrl.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue,
private_do_not_access_or_else_safe_script_wrapped_value: data.bgChallenge.interpreterUrl.privateDoNotAccessOrElseSafeScriptWrappedValue
};
parsed_data.bg_challenge = {
interpreter_url,
interpreter_hash: data.bgChallenge.interpreterHash,
program: data.bgChallenge.program,
global_name: data.bgChallenge.globalName,
client_experiments_state_blob: data.bgChallenge.clientExperimentsStateBlob
};
}
if (data.challenge) {
parsed_data.challenge = data.challenge;
}
if (data.playerResponse) {
parsed_data.player_response = parseResponse(data.playerResponse);

View File

@@ -25,6 +25,8 @@ import type OpenPopupAction from '../classes/actions/OpenPopupAction.js';
export interface IParsedResponse {
background?: MusicThumbnail;
challenge?: string;
bg_challenge?: IBotguardChallenge;
actions?: SuperParsedResult<YTNode>;
actions_memo?: Memo;
contents?: SuperParsedResult<YTNode>;
@@ -78,6 +80,19 @@ export interface IParsedResponse {
watch_next_response?: INextResponse;
}
export interface ITrustedResource {
private_do_not_access_or_else_trusted_resource_url_wrapped_value?: string;
private_do_not_access_or_else_safe_script_wrapped_value?: string;
}
export interface IBotguardChallenge {
interpreter_url: ITrustedResource;
interpreter_hash: string;
program: string;
global_name: string;
client_experiments_state_blob: string;
}
export interface IPlaybackTracking {
videostats_watchtime_url: string;
videostats_playback_url: string;
@@ -121,11 +136,12 @@ export interface IStreamingData {
}
export type IPlayerResponse = Pick<IParsedResponse, 'captions' | 'cards' | 'endscreen' | 'microformat' | 'annotations' | 'playability_status' | 'streaming_data' | 'player_config' | 'playback_tracking' | 'storyboards' | 'video_details'>;
export type INextResponse = Pick<IParsedResponse, 'contents' | 'contents_memo' | 'continuation_contents' | 'continuation_contents_memo' | 'current_video_endpoint' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'player_overlays' | 'engagement_panels'>
export type IBrowseResponse = Pick<IParsedResponse, 'background' | 'continuation_contents' | 'continuation_contents_memo' | 'on_response_received_actions' | 'on_response_received_actions_memo' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'contents' | 'contents_memo' | 'header' | 'header_memo' | 'metadata' | 'microformat' | 'alerts' | 'sidebar' | 'sidebar_memo'>
export type INextResponse = Pick<IParsedResponse, 'contents' | 'contents_memo' | 'continuation_contents' | 'continuation_contents_memo' | 'current_video_endpoint' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'player_overlays' | 'engagement_panels'>;
export type IBrowseResponse = Pick<IParsedResponse, 'background' | 'continuation_contents' | 'continuation_contents_memo' | 'on_response_received_actions' | 'on_response_received_actions_memo' | 'on_response_received_endpoints' | 'on_response_received_endpoints_memo' | 'contents' | 'contents_memo' | 'header' | 'header_memo' | 'metadata' | 'microformat' | 'alerts' | 'sidebar' | 'sidebar_memo'>;
export type ISearchResponse = Pick<IParsedResponse, 'header' | 'header_memo' | 'contents' | 'contents_memo' | 'on_response_received_commands' | 'continuation_contents' | 'continuation_contents_memo' | 'refinements' | 'estimated_results'>;
export type IResolveURLResponse = Pick<IParsedResponse, 'endpoint'>;
export type IGetTranscriptResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>
export type IGetNotificationsMenuResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>
export type IUpdatedMetadataResponse = Pick<IParsedResponse, 'actions' | 'actions_memo' | 'continuation'>
export type IGuideResponse = Pick<IParsedResponse, 'items' | 'items_memo'>
export type IGetTranscriptResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>;
export type IGetNotificationsMenuResponse = Pick<IParsedResponse, 'actions' | 'actions_memo'>;
export type IUpdatedMetadataResponse = Pick<IParsedResponse, 'actions' | 'actions_memo' | 'continuation'>;
export type IGuideResponse = Pick<IParsedResponse, 'items' | 'items_memo'>;
export type IGetChallengeResponse = Pick<IParsedResponse, 'challenge' | 'bg_challenge'>;

View File

@@ -37,9 +37,24 @@ export interface IRawPlayerConfig {
};
}
export interface IRawTrustedResource {
privateDoNotAccessOrElseTrustedResourceUrlWrappedValue?: string;
privateDoNotAccessOrElseSafeScriptWrappedValue?: string;
}
export interface IRawBotguardChallenge {
interpreterUrl: IRawTrustedResource;
interpreterHash: string;
program: string;
globalName: string;
clientExperimentsStateBlob: string;
}
export interface IRawResponse {
responseContext?: IResponseContext;
background?: RawNode;
bgChallenge?: IRawBotguardChallenge;
challenge?: string;
contents?: RawData;
onResponseReceivedActions?: RawNode[];
onResponseReceivedEndpoints?: RawNode[];

View File

@@ -2,6 +2,7 @@ import type { SessionOptions } from '../core/index.js';
export type InnerTubeConfig = SessionOptions;
export type InnerTubeClient = 'IOS' | 'WEB' | 'MWEB' | 'ANDROID' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'YTSTUDIO_ANDROID' | 'TV' | 'TV_EMBEDDED' | 'YTKIDS' | 'WEB_EMBEDDED' | 'WEB_CREATOR';
export type EngagementType = 'ENGAGEMENT_TYPE_UNBOUND' | 'ENGAGEMENT_TYPE_VIDEO_LIKE' | 'ENGAGEMENT_TYPE_VIDEO_DISLIKE' | 'ENGAGEMENT_TYPE_SUBSCRIBE' | 'ENGAGEMENT_TYPE_PLAYBACK' | 'ENGAGEMENT_TYPE_YPC_GET_PREMIUM_PAGE' | 'ENGAGEMENT_TYPE_YPC_GET_DOWNLOAD_ACTION';
export type UploadDate = 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
export type SearchType = 'all' | 'video' | 'channel' | 'playlist' | 'movie';