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

Live Chat - Implement moderation #202

Merged
merged 5 commits into from
Oct 2, 2022
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"contributors": [
"Wykerd (https://github.com/wykerd/)",
"MasterOfBob777 (https://github.com/MasterOfBob777)",
"patrickkfkan (https://github.com/patrickkfkan)"
"patrickkfkan (https://github.com/patrickkfkan)",
"akkadaska (https://github.com/akkadaska)"
],
"directories": {
"test": "./test",
Expand Down
3 changes: 1 addition & 2 deletions src/core/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 12 additions & 7 deletions src/parser/classes/NavigationEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,6 @@ class NavigationEndpoint extends YTNode {
params: data.sendLiveChatVoteEndpoint.params
};
}

if (data?.liveChatItemContextMenuEndpoint) {
LuanRT marked this conversation as resolved.
Show resolved Hide resolved
this.live_chat_item_context_menu = {
params: data.liveChatItemContextMenuEndpoint.params
};
}
}

/**
Expand All @@ -249,6 +243,8 @@ class NavigationEndpoint extends YTNode {
return '/player';
case 'watchPlaylistEndpoint':
return '/next';
case 'liveChatItemContextMenuEndpoint':
return 'live_chat/get_item_context_menu';
}
}

Expand Down Expand Up @@ -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>;
Expand All @@ -307,7 +312,7 @@ class NavigationEndpoint extends YTNode {
if (parse && result)
return Parser.parseResponse(result.data);

return this.#call(actions, client);
LuanRT marked this conversation as resolved.
Show resolved Hide resolved
return result;
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/parser/classes/livechat/items/LiveChatAutoModMessage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
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';

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);
Expand Down
4 changes: 3 additions & 1 deletion src/parser/classes/livechat/items/LiveChatPaidSticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class LiveChatPaidSticker extends YTNode {
sticker: Thumbnail[];
purchase_amount: string;
context_menu: NavigationEndpoint;
menu_endpoint?: NavigationEndpoint;
timestamp: number;

constructor(data: any) {
Expand All @@ -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;
LuanRT marked this conversation as resolved.
Show resolved Hide resolved
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/parser/classes/livechat/items/LiveChatTextMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,6 +23,7 @@ class LiveChatTextMessage extends YTNode {
};

menu_endpoint?: NavigationEndpoint;
inline_action_buttons: ObservedArray<Button>;
timestamp: number;
id: string;

Expand All @@ -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;
}
Expand Down
9 changes: 9 additions & 0 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,22 @@ 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 {
actions,
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,
Expand Down
65 changes: 65 additions & 0 deletions src/parser/youtube/ItemMenu.ts
Original file line number Diff line number Diff line change
@@ -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;
35 changes: 34 additions & 1 deletion src/parser/youtube/LiveChat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Parser, { LiveChatContinuation } from '../index';
import Parser, { LiveChatContinuation, ParsedResponse } from '../index';
import EventEmitter from '../../utils/EventEmitterLike';
import VideoInfo from './VideoInfo';

Expand All @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down