diff --git a/src/parser/classes/comments/CommentThread.ts b/src/parser/classes/comments/CommentThread.ts index af3b9ec9c..6104a1bcf 100644 --- a/src/parser/classes/comments/CommentThread.ts +++ b/src/parser/classes/comments/CommentThread.ts @@ -2,6 +2,7 @@ import { Parser } from '../../index.js'; import Button from '../Button.js'; import ContinuationItem from '../ContinuationItem.js'; import Comment from './Comment.js'; +import CommentView from './CommentView.js'; import CommentReplies from './CommentReplies.js'; import { InnertubeError } from '../../../utils/Utils.js'; @@ -17,15 +18,20 @@ export default class CommentThread extends YTNode { #actions?: Actions; #continuation?: ContinuationItem; - comment: Comment | null; - replies?: ObservedArray; + comment: Comment | CommentView | null; + replies?: ObservedArray; comment_replies_data: CommentReplies | null; is_moderated_elq_comment: boolean; has_replies: boolean; constructor(data: RawNode) { super(); - this.comment = Parser.parseItem(data.comment, Comment); + + if (Reflect.has(data, 'commentViewModel')) { + this.comment = Parser.parseItem(data.commentViewModel, CommentView); + } else { + this.comment = Parser.parseItem(data.comment, Comment); + } this.comment_replies_data = Parser.parseItem(data.replies, CommentReplies); this.is_moderated_elq_comment = data.isModeratedElqComment; this.has_replies = !!this.comment_replies_data; @@ -51,7 +57,7 @@ export default class CommentThread extends YTNode { if (!response.on_response_received_endpoints_memo) throw new InnertubeError('Unexpected response.', response); - this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment).map((comment) => { + this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment, CommentView).map((comment) => { comment.setActions(this.#actions); return comment; })); diff --git a/src/parser/classes/comments/CommentView.ts b/src/parser/classes/comments/CommentView.ts new file mode 100644 index 000000000..c9e7bf693 --- /dev/null +++ b/src/parser/classes/comments/CommentView.ts @@ -0,0 +1,88 @@ +import { YTNode } from '../../helpers.js'; +import type { RawNode } from '../../index.js'; + +import type Actions from '../../../core/Actions.js'; +import Author from '../misc/Author.js'; +import Text from '../misc/Text.js'; + +export default class CommentView extends YTNode { + static type = 'CommentView'; + + #actions?: Actions; + + comment_id: string; + is_pinned: boolean; + keys: { + comment: string; + comment_surface: string; + toolbar_state: string; + toolbar_surface: string; + shared: string; + }; + + content?: Text; + published_time?: string; + author_is_channel_owner?: boolean; + like_count?: string; + reply_count?: string; + is_member?: boolean; + member_badge?: { + url: string, + a11y: string; + }; + author?: Author; + + is_liked?: boolean; + is_disliked?: boolean; + is_hearted?: boolean; + + constructor(data: RawNode) { + super(); + + this.comment_id = data.commentId; + this.is_pinned = !!data.pinnedText; + + this.keys = { + comment: data.commentKey, + comment_surface: data.commentSurfaceKey, + toolbar_state: data.toolbarStateKey, + toolbar_surface: data.toolbarSurfaceKey, + shared: data.sharedKey + }; + } + + applyMutations(comment?: RawNode, toolbar_state?: RawNode) { + if (comment) { + this.content = Text.fromAttributed(comment.properties.content); + this.published_time = comment.properties.publishedTime; + this.author_is_channel_owner = !!comment.author.isCreator; + + this.like_count = comment.toolbar.likeCountNotliked ? comment.toolbar.likeCountNotliked : '0'; + this.reply_count = comment.toolbar.replyCount ? comment.toolbar.replyCount : '0'; + + this.is_member = !!comment.author.sponsorBadgeUrl; + + if (Reflect.has(comment.author, 'sponsorBadgeUrl')) { + this.member_badge = { + url: comment.author.sponsorBadgeUrl, + a11y: comment.author.A11y + }; + } + + this.author = new Author({ + simpleText: comment.author.displayName, + navigationEndpoint: comment.avatar.endpoint + }, comment.author, comment.avatar.image, comment.author.channelId); + } + + 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'; + } + } + + setActions(actions: Actions | undefined) { + this.#actions = actions; + } +} \ No newline at end of file diff --git a/src/parser/classes/misc/Author.ts b/src/parser/classes/misc/Author.ts index 02025225c..6d5a6e617 100644 --- a/src/parser/classes/misc/Author.ts +++ b/src/parser/classes/misc/Author.ts @@ -25,10 +25,21 @@ export default class Author { this.name = nav_text?.text || 'N/A'; this.thumbnails = thumbs ? Thumbnail.fromResponse(thumbs) : []; this.endpoint = ((nav_text?.runs?.[0] as TextRun) as TextRun)?.endpoint || nav_text?.endpoint; - this.badges = Array.isArray(badges) ? Parser.parseArray(badges) : observe([] as YTNode[]); - this.is_moderator = this.badges?.some((badge: any) => badge.icon_type == 'MODERATOR'); - this.is_verified = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED'); - this.is_verified_artist = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST'); + + if (badges) { + if (Array.isArray(badges)) { + this.badges = Parser.parseArray(badges); + this.is_moderator = this.badges?.some((badge: any) => badge.icon_type == 'MODERATOR'); + this.is_verified = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED'); + this.is_verified_artist = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST'); + } else { + this.badges = observe([] as YTNode[]); + this.is_verified = !!badges.isVerified; + this.is_verified_artist = !!badges.isArtist; + } + } else { + this.badges = observe([] as YTNode[]); + } // @TODO: Refactor this mess. this.url = diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index aefe60eaf..27b97f950 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -78,6 +78,7 @@ export { default as CommentsHeader } from './classes/comments/CommentsHeader.js' export { default as CommentSimplebox } from './classes/comments/CommentSimplebox.js'; export { default as CommentsSimplebox } from './classes/comments/CommentsSimplebox.js'; export { default as CommentThread } from './classes/comments/CommentThread.js'; +export { default as CommentView } from './classes/comments/CommentView.js'; export { default as CreatorHeart } from './classes/comments/CreatorHeart.js'; export { default as EmojiPicker } from './classes/comments/EmojiPicker.js'; export { default as PdgCommentChip } from './classes/comments/PdgCommentChip.js'; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index b91eb1d57..7c54bc903 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -25,6 +25,7 @@ import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem.j import Format from './classes/misc/Format.js'; import VideoDetails from './classes/misc/VideoDetails.js'; import NavigationEndpoint from './classes/NavigationEndpoint.js'; +import CommentView from './classes/comments/CommentView.js'; import type { KeyInfo } from './generator.js'; import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.js'; @@ -43,7 +44,8 @@ export type ParserError = { classdata: RawNode, error: unknown } | { - error_type: 'mutation_data_missing' + error_type: 'mutation_data_missing', + classname: string } | { error_type: 'mutation_data_invalid', total: number, @@ -108,7 +110,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) case 'mutation_data_missing': Log.warn(TAG, new InnertubeError( - 'Mutation data required for processing MusicMultiSelectMenuItems, but none found.\n' + + `Mutation data required for processing ${classname}, but none found.\n` + `This is a bug, please report it at ${Platform.shim.info.bugs_url}` ) ); @@ -316,6 +318,10 @@ export function parseResponse(data: applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations); + if (on_response_received_endpoints_memo) { + applyCommentsMutations(on_response_received_endpoints_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations); + } + const continuation = data.continuation ? parseC(data.continuation) : null; if (continuation) { parsed_data.continuation = continuation; @@ -683,3 +689,28 @@ export function applyMutations(memo: Memo, mutations: RawNode[]) { } } } + +export function applyCommentsMutations(memo: Memo, mutations: RawNode[]) { + const comment_view_items = memo.getType(CommentView); + + if (comment_view_items.length > 0) { + if (!mutations) { + ERROR_HANDLER({ + error_type: 'mutation_data_missing', + classname: 'CommentView' + }); + } + + for (const comment_view of comment_view_items) { + const comment_mutation = mutations + .find((mutation) => mutation.payload?.commentEntityPayload?.key === comment_view.keys.comment) + ?.payload?.commentEntityPayload; + + const toolbar_state_mutation = mutations + .find((mutation) => mutation.payload?.engagementToolbarStateEntityPayload?.key === comment_view.keys.toolbar_state) + ?.payload?.engagementToolbarStateEntityPayload; + + comment_view.applyMutations(comment_mutation, toolbar_state_mutation); + } + } +}