Skip to content

Commit

Permalink
feat(CommentView): Implement comment interaction methods
Browse files Browse the repository at this point in the history
  • Loading branch information
LuanRT committed Apr 11, 2024
1 parent a624963 commit 1c08bfe
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 6 deletions.
163 changes: 158 additions & 5 deletions src/parser/classes/comments/CommentView.ts
@@ -1,15 +1,26 @@
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../index.js';

import type Actions from '../../../core/Actions.js';
import NavigationEndpoint from '../NavigationEndpoint.js';
import Author from '../misc/Author.js';
import Text from '../misc/Text.js';
import CommentReplyDialog from './CommentReplyDialog.js';
import { InnertubeError } from '../../../utils/Utils.js';
import * as Proto from '../../../proto/index.js';

import type Actions from '../../../core/Actions.js';
import type { ApiResponse } from '../../../core/Actions.js';
import type { RawNode } from '../../index.js';

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

#actions?: Actions;

like_command?: NavigationEndpoint;
dislike_command?: NavigationEndpoint;
unlike_command?: NavigationEndpoint;
undislike_command?: NavigationEndpoint;
reply_command?: NavigationEndpoint;

comment_id: string;
is_pinned: boolean;
keys: {
Expand All @@ -32,6 +43,8 @@ export default class CommentView extends YTNode {
};
author?: Author;

test: any;

is_liked?: boolean;
is_disliked?: boolean;
is_hearted?: boolean;
Expand All @@ -51,7 +64,7 @@ export default class CommentView extends YTNode {
};
}

applyMutations(comment?: RawNode, toolbar_state?: RawNode) {
applyMutations(comment?: RawNode, toolbar_state?: RawNode, toolbar_surface?: RawNode) {
if (comment) {
this.content = Text.fromAttributed(comment.properties.content);
this.published_time = comment.properties.publishedTime;
Expand All @@ -78,10 +91,150 @@ export default class CommentView extends YTNode {
if (toolbar_state) {
this.is_hearted = toolbar_state.heartState === 'TOOLBAR_HEART_STATE_HEARTED';
this.is_liked = toolbar_state.likeState === 'TOOLBAR_LIKE_STATE_LIKED';
this.is_disliked = toolbar_state.likeState === 'TOOLBAR_HEART_STATE_DISLIKED';
this.is_disliked = toolbar_state.likeState === 'TOOLBAR_LIKE_STATE_DISLIKED';
}

if (toolbar_surface && !Reflect.has(toolbar_surface, 'prepareAccountCommand')) {
this.like_command = new NavigationEndpoint(toolbar_surface.likeCommand);
this.dislike_command = new NavigationEndpoint(toolbar_surface.dislikeCommand);
this.unlike_command = new NavigationEndpoint(toolbar_surface.unlikeCommand);
this.undislike_command = new NavigationEndpoint(toolbar_surface.undislikeCommand);
this.reply_command = new NavigationEndpoint(toolbar_surface.replyCommand);
}
}

/**
* Likes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the like command is not found.
*/
async like(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');

if (!this.like_command)
throw new InnertubeError('Like command not found.');

if (this.is_liked)
throw new InnertubeError('This comment is already liked.', { comment_id: this.comment_id });

return this.like_command.call(this.#actions);
}

/**
* Dislikes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the dislike command is not found.
*/
async dislike(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');

if (!this.dislike_command)
throw new InnertubeError('Dislike command not found.');

if (this.is_disliked)
throw new InnertubeError('This comment is already disliked.', { comment_id: this.comment_id });

return this.dislike_command.call(this.#actions);
}

/**
* Unlikes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the unlike command is not found.
*/
async unlike(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');

if (!this.unlike_command)
throw new InnertubeError('Unlike command not found.');

if (!this.is_liked)
throw new InnertubeError('This comment is not liked.', { comment_id: this.comment_id });

return this.unlike_command.call(this.#actions);
}

/**
* Undislikes the comment.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the undislike command is not found.
*/
async undislike(): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');

if (!this.undislike_command)
throw new InnertubeError('Undislike command not found.');

if (!this.is_disliked)
throw new InnertubeError('This comment is not disliked.', { comment_id: this.comment_id });

return this.undislike_command.call(this.#actions);
}

/**
* Replies to the comment.
* @param comment_text - The text of the reply.
* @returns A promise that resolves to the API response.
* @throws If the Actions instance is not set for this comment or if the reply command is not found.
*/
async reply(comment_text: string): Promise<ApiResponse> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');

if (!this.reply_command)
throw new InnertubeError('Reply command not found.');

const dialog = this.reply_command.dialog?.as(CommentReplyDialog);

if (!dialog)
throw new InnertubeError('Reply dialog not found.');

const reply_button = dialog.reply_button;

if (!reply_button)
throw new InnertubeError('Reply button not found in the dialog.');

if (!reply_button.endpoint)
throw new InnertubeError('Reply button endpoint not found.');

return reply_button.endpoint.call(this.#actions, { commentText: comment_text });
}

/**
* Translates the comment to the specified target language.
* @param target_language - The target language to translate the comment to, e.g. 'en', 'ja'.
* @returns A Promise that resolves to an ApiResponse object with the translated content, if available.
* @throws if the Actions instance is not set for this comment or if the comment content is not found.
*/
async translate(target_language: string): Promise<ApiResponse & { content?: string }> {
if (!this.#actions)
throw new InnertubeError('Actions instance not set for this comment.');

if (!this.content)
throw new InnertubeError('Comment content not found.', { comment_id: this.comment_id });

// Emojis must be removed otherwise InnerTube throws a 400 status code at us.
const text = this.content.toString().replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu, '');

const payload = {
text,
target_language
};

const action = Proto.encodeCommentActionParams(22, payload);
const response = await this.#actions.execute('comment/perform_comment_action', { action, client: 'ANDROID' });

// XXX: Should move this to Parser#parseResponse
const mutations = response.data.frameworkUpdates?.entityBatchUpdate?.mutations;
const content = mutations?.[0]?.payload?.commentEntityPayload?.translatedContent?.content;

return { ...response, content };
}

setActions(actions: Actions | undefined) {
this.#actions = actions;
}
Expand Down
5 changes: 4 additions & 1 deletion src/parser/parser.ts
Expand Up @@ -710,7 +710,10 @@ export function applyCommentsMutations(memo: Memo, mutations: RawNode[]) {
.find((mutation) => mutation.payload?.engagementToolbarStateEntityPayload?.key === comment_view.keys.toolbar_state)
?.payload?.engagementToolbarStateEntityPayload;

comment_view.applyMutations(comment_mutation, toolbar_state_mutation);
const engagement_toolbar = mutations.find((mutation) => mutation.entityKey === comment_view.keys.toolbar_surface)
?.payload?.engagementToolbarSurfaceEntityPayload;

comment_view.applyMutations(comment_mutation, toolbar_state_mutation, engagement_toolbar);
}
}
}

0 comments on commit 1c08bfe

Please sign in to comment.