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
6 changes: 6 additions & 0 deletions src/components/ChatSummary.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Icon from 'smelte/src/components/Icon';
import { Theme } from '../ts/chat-constants';
import { createEventDispatcher } from 'svelte';
import { showTimestamps } from '../ts/storage';

export let summary: Ytc.ParsedSummary;

Expand Down Expand Up @@ -55,6 +56,11 @@
<span class="align-middle">{run.text}</span>
{/if}
{/each}
{#if summary.timestamp && $showTimestamps}
<span class="text-xs ml-1 text-gray-400 dark:text-gray-600 align-middle">
{summary.timestamp}
</span>
{/if}
</div>
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
Expand Down
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}
9 changes: 7 additions & 2 deletions src/components/RedirectBanner.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import { slide, fade } from 'svelte/transition';
import MessageRun from './MessageRuns.svelte';
import Tooltip from './common/Tooltip.svelte';
import Button from 'smelte/src/components/Button';
import Icon from 'smelte/src/components/Icon';
import { Theme } from '../ts/chat-constants';
import { createEventDispatcher } from 'svelte';
import { showProfileIcons } from '../ts/storage';
import Button from 'smelte/src/components/Button';
import { showProfileIcons, showTimestamps } from '../ts/storage';

export let redirect: Ytc.ParsedRedirect;

Expand Down Expand Up @@ -53,6 +53,11 @@
</Icon>
</span>
<span class="align-middle">Live Redirect Notice</span>
{#if redirect.timestamp && $showTimestamps}
<span class="text-xs ml-1 text-gray-400 dark:text-gray-600 align-middle">
{redirect.timestamp}
</span>
{/if}
</div>
<div class="flex-none self-end" style="transform: translateY(3px);">
<Tooltip offsetY={0} small>
Expand Down
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
104 changes: 79 additions & 25 deletions src/scripts/chat-interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { fixLeaks } from '../ts/ytc-fix-memleaks';
import { frameIsReplay as isReplay, checkInjected } from '../ts/chat-utils';
import sha1 from 'sha-1';
import { chatReportUserOptions, ChatUserActions, isLiveTL } from '../ts/chat-constants';
import { chatReportUserOptions, ChatUserActions, ChatPollActions, isLiveTL } from '../ts/chat-constants';

function injectedFunction(): void {
const currentDomain = (location.protocol + '//' + location.host);
Expand Down Expand Up @@ -84,9 +84,69 @@ const chatLoaded = async (): Promise<void> => {
});
};

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

function parseServiceEndpoint(baseContext: any, serviceEndpoint: any, prop: string): { params: string, context: any } {
const { clickTrackingParams, [prop]: { params } } = serviceEndpoint;
const clonedContext = JSON.parse(JSON.stringify(baseContext));
clonedContext.clickTracking = {
clickTrackingParams
};
return {
params,
context: clonedContext
};
}

/**
* Executes a poll action (e.g., ending a poll)
*/
async function handlePollAction(msg: any, fetcher: (...args: any[]) => Promise<any>): Promise<void> {
try {
const currentDomain = (location.protocol + '//' + location.host);
const apiKey = ytcfg.data_.INNERTUBE_API_KEY;
const baseContext = ytcfg.data_.INNERTUBE_CONTEXT;
const time = Math.floor(Date.now() / 1000);
const SAPISID = getCookie('__Secure-3PAPISID');
const sha = sha1(`${time} ${SAPISID} ${currentDomain}`);
const auth = `SAPISIDHASH ${time}_${sha} SAPISID1PHASH ${time}_${sha} SAPISID3PHASH ${time}_${sha}`;
const heads = {
headers: {
'Content-Type': 'application/json',
Accept: '*/*',
Authorization: auth
},
method: 'POST'
};

if (msg.action === ChatPollActions.END_POLL) {
const poll = msg.poll;
const params = poll.item.action?.params || '';
const url = poll.item.action?.url || '/youtubei/v1/live_chat/live_chat_action';

// Call YouTube API to end the poll
await fetcher(`${currentDomain}${url}?key=${apiKey}&prettyPrint=false`, {
...heads,
body: JSON.stringify({
params,
context: baseContext
})
});
}
} catch (e) {
console.debug('Error executing poll action', e);
}
}

/**
* Executes a chat action (e.g., blocking or reporting a user)
*/
async function handleChatAction(msg: any, fetcher: (...args: any[]) => Promise<any>): Promise<void> {
const message = msg.message;
if (message.params == null) return;
let success = true;
Expand All @@ -95,18 +155,12 @@ const chatLoaded = async (): Promise<void> => {
// const action = msg.action;
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;
function getCookie(name: string): string {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return (parts.pop() ?? '').split(';').shift() ?? '';
return '';
}
const time = Math.floor(Date.now() / 1000);
const SAPISID = getCookie('__Secure-3PAPISID');
const sha = sha1(`${time} ${SAPISID} ${currentDomain}`);
const auth = `SAPISIDHASH ${time}_${sha}`;
const auth = `SAPISIDHASH ${time}_${sha} SAPISID1PHASH ${time}_${sha} SAPISID3PHASH ${time}_${sha}`;
const heads = {
headers: {
'Content-Type': 'application/json',
Expand All @@ -119,19 +173,8 @@ const chatLoaded = async (): Promise<void> => {
...heads,
body: JSON.stringify({ context: baseContext })
});
function parseServiceEndpoint(serviceEndpoint: any, prop: string): { params: string, context: any } {
const { clickTrackingParams, [prop]: { params } } = serviceEndpoint;
const clonedContext = JSON.parse(JSON.stringify(baseContext));
clonedContext.clickTracking = {
clickTrackingParams
};
return {
params,
context: clonedContext
};
}
if (msg.action === ChatUserActions.BLOCK) {
const { params, context } = parseServiceEndpoint(
const { params, context } = parseServiceEndpoint(baseContext,
res.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[1]
.menuNavigationItemRenderer.navigationEndpoint.confirmDialogEndpoint
.content.confirmDialogRenderer.confirmButton.buttonRenderer.serviceEndpoint,
Expand All @@ -145,7 +188,7 @@ const chatLoaded = async (): Promise<void> => {
})
});
} else if (msg.action === ChatUserActions.REPORT_USER) {
const { params, context } = parseServiceEndpoint(
const { params, context } = parseServiceEndpoint(baseContext,
res.liveChatItemContextMenuSupportedRenderers.menuRenderer.items[0].menuServiceItemRenderer.serviceEndpoint,
'getReportFormEndpoint'
);
Expand Down Expand Up @@ -182,6 +225,17 @@ const chatLoaded = async (): Promise<void> => {
message,
success
});
}

// eslint-disable-next-line @typescript-eslint/no-misused-promises
port.onMessage.addListener(async (msg: any) => {
if (msg.type === 'executePollAction') {
return handlePollAction(msg, fetcher);
}
if (msg.type === 'executeChatAction') {
return handleChatAction(msg, fetcher);
}
return;
});
});

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 @@ -51,6 +51,10 @@ export enum ChatUserActions {
REPORT_USER = 'REPORT_USER',
}

export enum ChatPollActions {
END_POLL = 'END_POLL',
}

export enum ChatReportUserOptions {
UNWANTED_SPAM = 'UNWANTED_SPAM',
PORN_OR_SEX = 'PORN_OR_SEX',
Expand Down
26 changes: 22 additions & 4 deletions src/ts/chat-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const parseChatSummary = (renderer: Ytc.AddChatItem, actionId: string, showtime:
message: splitRuns[2],
},
showtime: showtime,
timestamp: formatTimestamp(Date.now() * 1000),
};
return item;
}
Expand All @@ -116,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 @@ -128,10 +135,11 @@ 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,
timestamp: formatTimestamp(Date.now() * 1000),
};
return item;
}
Expand Down Expand Up @@ -266,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 @@ -282,6 +299,7 @@ const parsePollRenderer = (baseRenderer: Ytc.PollRenderer): Ytc.ParsedPoll | und
percentage: choice.votePercentage?.simpleText
};
}),
action: actionButton
}
};
}
Expand Down
Loading