Skip to content
Draft
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
39 changes: 26 additions & 13 deletions src/components/PollResults.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import Icon from 'smelte/src/components/Icon';
import { Theme } from '../ts/chat-constants';
import { createEventDispatcher } from 'svelte';
import { showProfileIcons } from '../ts/storage';
import { port, showProfileIcons } from '../ts/storage';
import ProgressLinear from 'smelte/src/components/ProgressLinear';
import { endPoll } from '../ts/chat-actions';
import Button from 'smelte/src/components/Button';

export let poll: Ytc.ParsedPoll;

Expand Down Expand Up @@ -59,18 +61,20 @@
{/if}
{/each}
</div>
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
<Icon
slot="activator"
class="cursor-pointer text-lg"
on:click={() => { dismissed = true; }}
>
close
</Icon>
Dismiss
</Tooltip>
</div>
{#if !poll.item.action}
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
<Icon
slot="activator"
class="cursor-pointer text-lg"
on:click={() => { dismissed = true; }}
>
close
</Icon>
Dismiss
</Tooltip>
</div>
{/if}
</div>
{#if !shorten && !dismissed}
<div class="mt-1 inline-flex flex-row gap-2 break-words w-full overflow-visible" transition:slide|local={{ duration: 300 }}>
Expand All @@ -85,6 +89,15 @@
</div>
<ProgressLinear progress={(choice.ratio || 0.001) * 100} color="gray"/>
{/each}
{#if poll.item.action}
<div class="mt-1 whitespace-pre-line flex justify-end" transition:slide|global={{ duration: 300 }}>
<Button on:click={() => endPoll(poll, $port)} small>
<span forceDark forceTLColor={Theme.DARK} class="cursor-pointer">
{poll.item.action.text}
</span>
</Button>
</div>
{/if}
{/if}
</div>
{/if}
11 changes: 11 additions & 0 deletions src/scripts/chat-background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ const executeChatAction = (
interceptor?.port?.postMessage(message);
};

const executePollAction = (
port: Chat.Port,
message: Chat.executePollActionMsg
): void => {
const interceptor = findInterceptorFromClient(port);
interceptor?.port?.postMessage(message);
};

const sendChatUserActionResponse = (
port: Chat.Port,
message: Chat.chatUserActionResponse
Expand Down Expand Up @@ -418,6 +426,9 @@ chrome.runtime.onConnect.addListener((port) => {
case 'executeChatAction':
executeChatAction(port, message);
break;
case 'executePollAction':
executePollAction(port, message);
break;
case 'chatUserActionResponse':
sendChatUserActionResponse(port, message);
break;
Expand Down
56 changes: 47 additions & 9 deletions src/scripts/chat-interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fixLeaks } from '../ts/ytc-fix-memleaks';
import { frameIsReplay as isReplay, checkInjected } from '../ts/chat-utils';
import { chatReportUserOptions, ChatUserActions, isLiveTL } from '../ts/chat-constants';
import { chatReportUserOptions, ChatUserActions, ChatPollActions, isLiveTL } from '../ts/chat-constants';
import sha1 from 'sha-1';

function injectedFunction(): void {
Expand Down Expand Up @@ -112,15 +112,51 @@ const chatLoaded = async (): Promise<void> => {
});
};

const getCookie = (name: string): string => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return (parts.pop() ?? '').split(';').shift() ?? '';
return '';
};

async function handlePollAction(msg: any): Promise<void> {
try {
if (msg.action !== ChatPollActions.END_POLL) return;
const poll = msg.poll;
const params = poll.item.action?.params;
const apiPath = poll.item.action?.api || '/youtubei/v1/live_chat/live_chat_action';
if (!params) return;
const currentDomain = (location.protocol + '//' + location.host);
const baseContext = ytcfg.data_.INNERTUBE_CONTEXT;
const time = Math.floor(Date.now() / 1000);
const sapisid = getCookie('__Secure-3PAPISID') || getCookie('SAPISID');
const auth = sapisid ? `SAPISIDHASH ${time}_${sha1(`${time} ${sapisid} ${currentDomain}`)}` : null;
const authuser = (ytcfg as any)?.data_?.SESSION_INDEX;
const pageId = (ytcfg as any)?.data_?.DELEGATED_SESSION_ID as string | undefined;
await fetcher(`${currentDomain}${apiPath}?prettyPrint=false`, {
headers: {
...(authuser != null ? { 'X-Goog-AuthUser': String(authuser) } : {}),
...(pageId ? { 'X-Goog-PageId': pageId } : {}),
...(auth != null ? { Authorization: auth } : {})
},
method: 'POST' as const,
mode: 'same-origin' as const,
body: JSON.stringify({
context: baseContext,
params
})
});
} catch (e) {
console.debug('Error executing poll action', e);
}
}

// eslint-disable-next-line @typescript-eslint/no-misused-promises
port.onMessage.addListener(async (msg) => {
const getCookie = (name: string): string => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return (parts.pop() ?? '').split(';').shift() ?? '';
return '';
};

if (msg.type === 'executePollAction') {
await handlePollAction(msg);
return;
}
if (msg.type !== 'executeChatAction') return;
const message = msg.message;
const debugAction = msg.action === ChatUserActions.DELETE_MESSAGE;
Expand All @@ -135,7 +171,7 @@ const chatLoaded = async (): Promise<void> => {
const currentDomain = (location.protocol + '//' + location.host);
const apiKey = ytcfg.data_.INNERTUBE_API_KEY;
const contextMenuUrl = `${currentDomain}/youtubei/v1/live_chat/get_item_context_menu?params=` +
`${encodeURIComponent(message.params)}&pbj=1&key=${apiKey}&prettyPrint=false`;
`${encodeURIComponent(message.params)}&pbj=1&prettyPrint=false`;
const baseContext = ytcfg.data_.INNERTUBE_CONTEXT;
// Do not override Innertube headers like X-Goog-Visitor-Id here. Those can differ from
// ytcfg.context.client.visitorData in subtle ways and cause YT to treat the request as logged out.
Expand All @@ -147,6 +183,7 @@ const chatLoaded = async (): Promise<void> => {
const visitorId = (ytcfg as any)?.data_?.VISITOR_DATA ?? baseContext?.client?.visitorData;
const clientName = (ytcfg as any)?.data_?.INNERTUBE_CLIENT_NAME;
const clientVersion = (ytcfg as any)?.data_?.INNERTUBE_CLIENT_VERSION;
const pageId = (ytcfg as any)?.data_?.DELEGATED_SESSION_ID as string | undefined;
const heads = {
headers: {
'Content-Type': 'application/json',
Expand All @@ -155,6 +192,7 @@ const chatLoaded = async (): Promise<void> => {
...(visitorId != null ? { 'X-Goog-Visitor-Id': String(visitorId) } : {}),
...(clientName != null ? { 'X-Youtube-Client-Name': String(clientName) } : {}),
...(clientVersion != null ? { 'X-Youtube-Client-Version': String(clientVersion) } : {}),
...(pageId ? { 'X-Goog-PageId': pageId } : {}),
'X-Origin': currentDomain,
...(auth != null ? { Authorization: auth } : {})
},
Expand Down
21 changes: 20 additions & 1 deletion src/ts/chat-actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { writable } from 'svelte/store';
import { ChatReportUserOptions, ChatUserActions } from './chat-constants';
import { ChatReportUserOptions, ChatUserActions, ChatPollActions } from './chat-constants';
import { reportDialog } from './storage';

export function useBanHammer(
Expand Down Expand Up @@ -28,3 +28,22 @@ export function useBanHammer(
});
}
}

/**
* Ends a poll that is currently active in the live chat
* @param poll The ParsedPoll object containing information about the poll to end
* @param port The port to communicate with the background script
*/
export function endPoll(
poll: Ytc.ParsedPoll,
port: Chat.Port | null
): void {
if (!port) return;

// Use a dedicated executePollAction message type for poll operations
port?.postMessage({
type: 'executePollAction',
poll,
action: ChatPollActions.END_POLL
});
}
4 changes: 4 additions & 0 deletions src/ts/chat-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export enum ChatUserActions {
DELETE_MESSAGE = 'DELETE_MESSAGE',
}

export enum ChatPollActions {
END_POLL = 'END_POLL',
}

export enum ChatReportUserOptions {
UNWANTED_SPAM = 'UNWANTED_SPAM',
PORN_OR_SEX = 'PORN_OR_SEX',
Expand Down
24 changes: 20 additions & 4 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,16 @@ const parseRedirectBanner = (renderer: Ytc.AddChatItem, actionId: string, showti
src: fixUrl(baseRenderer.authorPhoto?.thumbnails[0].url ?? ''),
alt: 'Redirect profile icon'
};
const url = baseRenderer.inlineActionButton?.buttonRenderer.command.urlEndpoint?.url ||
(baseRenderer.inlineActionButton?.buttonRenderer.command.watchEndpoint?.videoId ?
"/watch?v=" + baseRenderer.inlineActionButton?.buttonRenderer.command.watchEndpoint?.videoId
const buttonRenderer = baseRenderer.inlineActionButton?.buttonRenderer;
const url = buttonRenderer?.command.urlEndpoint?.url ||
(buttonRenderer?.command.watchEndpoint?.videoId ?
"/watch?v=" + buttonRenderer?.command.watchEndpoint?.videoId
: '');
const buttonRendererText = buttonRenderer?.text;
const buttonText = buttonRendererText && (
('runs' in buttonRendererText && parseMessageRuns(buttonRendererText.runs))
|| ('simpleText' in buttonRendererText && [{ type: 'text', text: buttonRendererText.simpleText }] as Ytc.ParsedTextRun[])
) || [];
const item: Ytc.ParsedRedirect = {
type: 'redirect',
actionId: actionId,
Expand All @@ -129,7 +135,7 @@ const parseRedirectBanner = (renderer: Ytc.AddChatItem, actionId: string, showti
profileIcon: profileIcon,
action: {
url: fixUrl(url),
text: parseMessageRuns(baseRenderer.inlineActionButton?.buttonRenderer.text?.runs),
text: buttonText,
}
},
showtime: showtime,
Expand Down Expand Up @@ -268,6 +274,15 @@ const parsePollRenderer = (baseRenderer: Ytc.PollRenderer): Ytc.ParsedPoll | und
src: fixUrl(baseRenderer.header.pollHeaderRenderer.thumbnail?.thumbnails[0].url ?? ''),
alt: 'Poll profile icon'
};
// only allow action if all the relevant fields are present for it
const buttonRenderer = baseRenderer.button?.buttonRenderer;
const actionButton = buttonRenderer?.command?.commandMetadata?.webCommandMetadata?.apiUrl &&
buttonRenderer?.text && 'simpleText' in buttonRenderer?.text &&
buttonRenderer?.command?.liveChatActionEndpoint?.params && {
api: buttonRenderer.command.commandMetadata.webCommandMetadata.apiUrl,
text: buttonRenderer.text.simpleText,
params: buttonRenderer.command.liveChatActionEndpoint.params
} || undefined;
// TODO implement 'selected' field? YT doesn't use it in results.
return {
type: 'poll',
Expand All @@ -284,6 +299,7 @@ const parsePollRenderer = (baseRenderer: Ytc.PollRenderer): Ytc.ParsedPoll | und
percentage: choice.votePercentage?.simpleText
};
}),
action: actionButton
}
};
}
Expand Down
9 changes: 8 additions & 1 deletion src/ts/typings/chat.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,17 @@ declare namespace Chat {
reportOption?: ChatReportUserOptions;
}

interface executePollActionMsg {
type: 'executePollAction';
poll: Ytc.ParsedPoll;
action: ChatPollActions;
}

type BackgroundMessage =
RegisterInterceptorMsg | RegisterClientMsg | processJsonMsg |
setInitialDataMsg | updatePlayerProgressMsg | setThemeMsg | getThemeMsg |
RegisterYtcInterceptorMsg | sendLtlMessageMsg | executeChatActionMsg | chatUserActionResponse;
RegisterYtcInterceptorMsg | sendLtlMessageMsg | executeChatActionMsg |
executePollActionMsg | chatUserActionResponse;

type Port = Omit<chrome.runtime.Port, 'postMessage' | 'onMessage'> & {
postMessage: (message: BackgroundMessage | BackgroundResponse) => void;
Expand Down
12 changes: 9 additions & 3 deletions src/ts/typings/ytc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ declare namespace Ytc {
icon?: string;
accessibility?: AccessibilityObj;
isDisabled?: boolean;
text?: RunsObj; // | SimpleTextObj;
text?: RunsObj | SimpleTextObj;
command: {
commandMetadata?: {
webCommandMetadata?: {
Expand Down Expand Up @@ -315,7 +315,9 @@ declare namespace Ytc {
}
}
displayVoteResults?: boolean;
button?: ButtonRenderer;
button?: {
buttonRenderer: ButtonRenderer;
}
}

interface PollChoice {
Expand Down Expand Up @@ -520,8 +522,12 @@ declare namespace Ytc {
ratio?: number;
percentage?: string;
}>;
action?: {
api: string;
params: string;
text: string;
}
}
// TODO add 'action' for ending poll button
}

interface ParsedRemoveBanner {
Expand Down
Loading