mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-26 08:08:54 +00:00
feat(LiveChat): add support for moderation & more (#202)
* Live Chat - Implement moderation * Live Chat - Implement class ItemMenu * fix moderation method Co-authored-by: LuanRT <luan.lrt4@gmail.com>
This commit is contained in:
@@ -455,8 +455,7 @@ class Actions {
|
||||
};
|
||||
break;
|
||||
case 'live_chat/get_item_context_menu':
|
||||
// Note: this is currently broken due to a recent refactor
|
||||
// TODO: this should be implemented
|
||||
data.params = args.params;
|
||||
break;
|
||||
case 'live_chat/moderate':
|
||||
data.params = args.params;
|
||||
|
||||
@@ -230,12 +230,6 @@ class NavigationEndpoint extends YTNode {
|
||||
params: data.sendLiveChatVoteEndpoint.params
|
||||
};
|
||||
}
|
||||
|
||||
if (data?.liveChatItemContextMenuEndpoint) {
|
||||
this.live_chat_item_context_menu = {
|
||||
params: data.liveChatItemContextMenuEndpoint.params
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,6 +243,8 @@ class NavigationEndpoint extends YTNode {
|
||||
return '/player';
|
||||
case 'watchPlaylistEndpoint':
|
||||
return '/next';
|
||||
case 'liveChatItemContextMenuEndpoint':
|
||||
return 'live_chat/get_item_context_menu';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +293,15 @@ class NavigationEndpoint extends YTNode {
|
||||
const response = await actions.engage(this.metadata.api_url, { video_id: this.like.target.video_id, params: this.like.params });
|
||||
return response;
|
||||
}
|
||||
|
||||
if (this.live_chat_item_context_menu) {
|
||||
if (!this.metadata.api_url)
|
||||
throw new Error('Live Chat Item Context Menu endpoint requires an api_url, but was not parsed from the response.');
|
||||
const response = await actions.livechat(this.metadata.api_url, {
|
||||
params: this.live_chat_item_context_menu.params
|
||||
});
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
async call(actions: Actions, client: string | undefined, parse: true) : Promise<ParsedResponse | undefined>;
|
||||
@@ -307,7 +312,7 @@ class NavigationEndpoint extends YTNode {
|
||||
if (parse && result)
|
||||
return Parser.parseResponse(result.data);
|
||||
|
||||
return this.#call(actions, client);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Text from '../../misc/Text';
|
||||
import Parser from '../../../index';
|
||||
import { YTNode } from '../../../helpers';
|
||||
import { ObservedArray, YTNode } from '../../../helpers';
|
||||
import NavigationEndpoint from '../../NavigationEndpoint';
|
||||
import Button from '../../Button';
|
||||
|
||||
class LiveChatAutoModMessage extends YTNode {
|
||||
static type = 'LiveChatAutoModMessage';
|
||||
@@ -8,12 +10,16 @@ class LiveChatAutoModMessage extends YTNode {
|
||||
auto_moderated_item;
|
||||
header_text: Text;
|
||||
|
||||
menu_endpoint?: NavigationEndpoint;
|
||||
moderation_buttons: ObservedArray<Button>;
|
||||
timestamp: number;
|
||||
id: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
|
||||
this.moderation_buttons = Parser.parseArray<Button>(data.moderationButtons, [ Button ]);
|
||||
this.auto_moderated_item = Parser.parse(data.autoModeratedItem);
|
||||
this.header_text = new Text(data.headerText);
|
||||
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
|
||||
|
||||
@@ -23,6 +23,7 @@ class LiveChatPaidSticker extends YTNode {
|
||||
sticker: Thumbnail[];
|
||||
purchase_amount: string;
|
||||
context_menu: NavigationEndpoint;
|
||||
menu_endpoint?: NavigationEndpoint;
|
||||
timestamp: number;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -42,7 +43,8 @@ class LiveChatPaidSticker extends YTNode {
|
||||
this.author_name_text_color = data.authorNameTextColor;
|
||||
this.sticker = Thumbnail.fromResponse(data.sticker);
|
||||
this.purchase_amount = new Text(data.purchaseAmountText).toString();
|
||||
this.context_menu = new NavigationEndpoint(data.contextMenuEndpoint);
|
||||
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
|
||||
this.context_menu = this.menu_endpoint;
|
||||
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import MetadataBadge from '../../MetadataBadge';
|
||||
import LiveChatAuthorBadge from '../../LiveChatAuthorBadge';
|
||||
import Parser from '../../../index';
|
||||
|
||||
import { YTNode } from '../../../helpers';
|
||||
import { ObservedArray, YTNode } from '../../../helpers';
|
||||
import Button from '../../Button';
|
||||
|
||||
class LiveChatTextMessage extends YTNode {
|
||||
static type = 'LiveChatTextMessage';
|
||||
@@ -22,6 +23,7 @@ class LiveChatTextMessage extends YTNode {
|
||||
};
|
||||
|
||||
menu_endpoint?: NavigationEndpoint;
|
||||
inline_action_buttons: ObservedArray<Button>;
|
||||
timestamp: number;
|
||||
id: string;
|
||||
|
||||
@@ -47,6 +49,7 @@ class LiveChatTextMessage extends YTNode {
|
||||
this.author.is_verified_artist = badges ? badges.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') : null;
|
||||
|
||||
this.menu_endpoint = new NavigationEndpoint(data.contextMenuEndpoint);
|
||||
this.inline_action_buttons = Parser.parseArray<Button>(data.inlineActionButtons, [ Button ]);
|
||||
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
|
||||
this.id = data.id;
|
||||
}
|
||||
|
||||
@@ -230,6 +230,13 @@ export default class Parser {
|
||||
const actions_memo = this.#getMemo();
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const live_chat_item_context_menu_supported_renderers = data.liveChatItemContextMenuSupportedRenderers
|
||||
? Parser.parseItem(data.liveChatItemContextMenuSupportedRenderers)
|
||||
: null;
|
||||
const live_chat_item_context_menu_supported_renderers_memo = this.#getMemo();
|
||||
this.#clearMemo();
|
||||
|
||||
this.applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
|
||||
|
||||
return {
|
||||
@@ -237,6 +244,8 @@ export default class Parser {
|
||||
actions_memo,
|
||||
contents,
|
||||
contents_memo,
|
||||
live_chat_item_context_menu_supported_renderers,
|
||||
live_chat_item_context_menu_supported_renderers_memo,
|
||||
on_response_received_actions,
|
||||
on_response_received_actions_memo,
|
||||
on_response_received_endpoints,
|
||||
|
||||
65
src/parser/youtube/ItemMenu.ts
Normal file
65
src/parser/youtube/ItemMenu.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import Actions from '../../core/Actions';
|
||||
|
||||
import Menu from '../classes/menus/Menu';
|
||||
import MenuServiceItem from '../classes/menus/MenuServiceItem';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint';
|
||||
import Button from '../classes/Button';
|
||||
|
||||
import { ParsedResponse } from '..';
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
import { ObservedArray, YTNode } from '../helpers';
|
||||
|
||||
class ItemMenu {
|
||||
#page: ParsedResponse;
|
||||
#actions: Actions;
|
||||
#items: ObservedArray<YTNode>;
|
||||
|
||||
constructor(data: ParsedResponse, actions: Actions) {
|
||||
this.#page = data;
|
||||
this.#actions = actions;
|
||||
|
||||
const menu = data?.live_chat_item_context_menu_supported_renderers;
|
||||
|
||||
if (!menu || !menu.is(Menu))
|
||||
throw new InnertubeError('Response did not have a "live_chat_item_context_menu_supported_renderers" property. The call may have failed.');
|
||||
|
||||
this.#items = menu.as(Menu).items;
|
||||
}
|
||||
|
||||
async selectItem(icon_type: string): Promise<ParsedResponse>
|
||||
async selectItem(button: Button): Promise<ParsedResponse>
|
||||
async selectItem(item: string | Button): Promise<ParsedResponse> {
|
||||
let endpoint: NavigationEndpoint;
|
||||
|
||||
if (item instanceof Button) {
|
||||
endpoint = item.endpoint;
|
||||
} else {
|
||||
const button = this.#items.find((button) => {
|
||||
if (!button.is(MenuServiceItem)) {
|
||||
return false;
|
||||
}
|
||||
const menuServiceItem = button.as(MenuServiceItem);
|
||||
return menuServiceItem.icon_type === item;
|
||||
});
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError(`Button "${item}" not found.`);
|
||||
|
||||
endpoint = button.as(MenuServiceItem).endpoint;
|
||||
}
|
||||
|
||||
const response = await endpoint.callTest(this.#actions, { parse: true });
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
items(): ObservedArray<YTNode> {
|
||||
return this.#items;
|
||||
}
|
||||
|
||||
page(): ParsedResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
export default ItemMenu;
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { LiveChatContinuation } from '../index';
|
||||
import Parser, { LiveChatContinuation, ParsedResponse } from '../index';
|
||||
import EventEmitter from '../../utils/EventEmitterLike';
|
||||
import VideoInfo from './VideoInfo';
|
||||
|
||||
@@ -22,12 +22,22 @@ import ShowLiveChatTooltipCommand from '../classes/livechat/ShowLiveChatTooltipC
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
import { ObservedArray, YTNode } from '../helpers';
|
||||
import LiveChatTextMessage from '../classes/livechat/items/LiveChatTextMessage';
|
||||
import LiveChatPaidMessage from '../classes/livechat/items/LiveChatPaidMessage';
|
||||
import LiveChatPaidSticker from '../classes/livechat/items/LiveChatPaidSticker';
|
||||
import LiveChatAutoModMessage from '../classes/livechat/items/LiveChatAutoModMessage';
|
||||
import LiveChatMembershipItem from '../classes/livechat/items/LiveChatMembershipItem';
|
||||
import LiveChatViewerEngagementMessage from '../classes/livechat/items/LiveChatViewerEngagementMessage';
|
||||
import ItemMenu from './ItemMenu';
|
||||
import Button from '../classes/Button';
|
||||
|
||||
export type ChatAction =
|
||||
AddChatItemAction | AddBannerToLiveChatCommand | AddLiveChatTickerItemAction |
|
||||
MarkChatItemAsDeletedAction | MarkChatItemsByAuthorAsDeletedAction | RemoveBannerForLiveChatCommand |
|
||||
ReplaceChatItemAction | ReplayChatItemAction | ShowLiveChatActionPanelAction | ShowLiveChatTooltipCommand;
|
||||
|
||||
export type ChatItemHasMenuEndpoint = LiveChatAutoModMessage | LiveChatMembershipItem | LiveChatPaidMessage | LiveChatPaidSticker | LiveChatTextMessage | LiveChatViewerEngagementMessage;
|
||||
|
||||
export interface LiveMetadata {
|
||||
title: UpdateTitleAction | undefined;
|
||||
description: UpdateDescriptionAction | undefined;
|
||||
@@ -180,6 +190,29 @@ class LiveChat extends EventEmitter {
|
||||
return data.actions.array().as(AddChatItemAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves given chat item's menu.
|
||||
*/
|
||||
async getItemMenu(item: ChatItemHasMenuEndpoint): Promise<ItemMenu> {
|
||||
if (!item.menu_endpoint)
|
||||
throw new InnertubeError('This item does not have a menu.', item);
|
||||
|
||||
const response = await item.menu_endpoint.call(this.#actions, undefined, true);
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Could not retrieve item menu.', item);
|
||||
|
||||
return new ItemMenu(response, this.#actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to "clicking" a button.
|
||||
*/
|
||||
async selectButton(button: Button): Promise<ParsedResponse> {
|
||||
const response = await button.endpoint.callTest(this.#actions, { parse: true });
|
||||
return response;
|
||||
}
|
||||
|
||||
async #wait(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user