Skip to content

Instantly share code, notes, and snippets.

@Explosion-Scratch
Created September 23, 2023 17:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Explosion-Scratch/e8857930850ff5b8b963a05c40fea2fb to your computer and use it in GitHub Desktop.
Save Explosion-Scratch/e8857930850ff5b8b963a05c40fea2fb to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Bard
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://*.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=textarea.online
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// ==/UserScript==
(async function () {
"use strict";
console.log("ACTIVATED - Bard running");
let captions = {};
let captionText = "";
let elements = {};
let addEl = once(addElement);
interceptXHR((response, url, method, body) => {
if (url.includes("timedtext")) {
captions = JSON.parse(response);
captionText = captions.events
.filter((i) => i.segs)
.map((i) =>
i.segs
.map((j) => j.utf8)
.filter((j) => j.trim().length)
.join(" ")
)
.join(" ")
.replace(/\s+/g, " ");
unsafeWindow.captions = captions;
if (elements.textarea) {
elements.textarea.value = captionText;
}
addEl(captionText, elements);
elements.button.onclick = () => summarize(elements, b, captionText);
}
}, unsafeWindow);
await new Promise((r) => r());
console.log(BardBot);
let b = new BardBot({ xmlhttprequest: GM_xmlhttpRequest });
unsafeWindow.bard = b;
console.log(unsafeWindow.bard, unsafeWindow, unsafeWindow.title);
return;
console.log("Working: ", b);
let a = await b.doSendMessage({ prompt: "list the last 3 presidents" });
console.log("Response: ", a);
})();
async function summarize(elements, bot, captionText) {
elements.button.innerText = "Summarizing...";
const prompt = `Summarize the following transcript from a video called "${
unsafeWindow.document.querySelector("#title:has(h1)").innerText
}" in a few short bullet points.\nTranscript:\n${captionText}`;
console.log("Summarizing", prompt, bot);
let r = await bot.query({ prompt });
console.log("Got output:", r);
let text = r.text;
let style = elements.style.innerHTML;
elements.div.innerHTML = `
<h3>Transcript</h3>
<div class="result"></div>
<style></style>
`;
elements.style = elements.div.querySelector("style");
elements.output = elements.div.querySelector(".result");
elements.style.innerHTML = style;
elements.output.innerText = text;
}
async function addElement(captions, elements) {
let div = document.createElement("div");
div.innerHTML = `
<h3>Summarize YouTube video</h3>
<span class='desc'>Make sure that the correct captions track has been selected as this is used for summarization</span>
<i><b>Current captions</b></i>
<textarea></textarea>
<button>Summarize</button>
<style></style>
`;
div.id = "yt_summarize_userscript";
elements.textarea = div.querySelector("textarea");
elements.button = div.querySelector("button");
elements.style = div.querySelector("style");
elements.div = div;
elements.style.innerHTML =
`h3 {
font-size: 1.2em;
width: 100%;
text-align: center;
}
.desc {
font-size: 0.8em;
color: gray;
margin-top: 0.5em;
margin-bottom: 1em;
font-style: italic;
text-align: center;
display: block;
width: 100%;
text-align: left;
}
textarea {
width: 100%;
height: 100px;
font-size: .7em;
margin-bottom: 1em;
resize: none;
border-radius: 5px;
border: 1px solid gray;
padding: .3em;
box-sizing: border-box;
background-color: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
button {
padding: .5em 1em;
border-radius: 5px;
border: 1px solid gray;
background-color: black;
font-size: .7em;
color: white;
font-weight: bold;
cursor: pointer;
font-size: 1em;
text-transform: uppercase;
width: 100%;
display: block;
text-align: center;
transition: all .3s ease-in-out;
}
button:hover {
background-color: white;
color: black;
}`.replace(/\n(.+)\{/gi, `\n#${div.id} $1 {`) +
`\n\n#${div.id}{margin: 30px 0; border-radius: 6px; border: 1px dashed gray; display: flex; justify-content: center; flex-direction: column; font-size: 15px; background: white; padding: 20px 40px;}`;
console.log(elements.style.innerHTML);
elements.textarea.value = captions;
let ref = await waitForElement("#bottom-row", {
document: unsafeWindow.document,
});
ref.style.display = "block";
console.log(ref, unsafeWindow.document);
ref.insertAdjacentElement("afterbegin", div);
}
async function waitForElement(
selector,
{ checkInterval = 300, timeout = 30000, document = window.document } = {}
) {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const intervalId = setInterval(() => {
const element = document.querySelector(selector);
if (element) {
clearInterval(intervalId);
resolve(element);
} else if (Date.now() - startTime > timeout) {
clearInterval(intervalId);
reject(
new Error(`Timeout waiting for element with selector ${selector}`)
);
}
}, checkInterval);
});
}
function once(fn) {
let called = false;
return function (...args) {
if (!called) {
called = true;
return fn(...args);
}
};
}
class BardBot {
/**
* The conversation context.
* @type {Object}
*/
conversationContext = undefined;
/**
* The logging function.
* @type {Function}
*/
logFunc = undefined;
/**
* Creates a new instance of the BardBot class.
* @param {Object} options - The options for the BardBot instance.
* @param {Function} [options.log] - The logging function.
* @param {Object} [options.context] - The conversation context.
* @param {Array} [options.context.ids] - The conversation context IDs.
* @param {Object} [options.context.session] - The conversation context session.
* @param {XMLHttpRequest} options.xmlhttprequest - The XMLHttpRequest instance.
* @returns {BardBot} - The new BardBot instance.
*/
constructor({ log, context: { ids, session } = {}, xmlhttprequest }) {
this.xmlhttprequest = xmlhttprequest;
if (!xmlhttprequest) {
this.xmlhttprequest = window.XMLHttpRequest;
}
if (ids && session) {
this.setContext({ ids, session });
}
if (log) {
this.logFunc = log;
}
return this;
}
/**
* Sets the conversation context.
* @param {Object} options - The conversation context options.
* @param {Array} options.ids - The conversation context IDs.
* @param {Object} options.session - The conversation context session.
*/
setContext({ ids, session }) {
this.conversationContext = {
ids,
requestParams: session,
};
}
/**
* Gets the conversation context.
* @returns {Object} - The conversation context.
* @property {Array} ids - The conversation context IDs.
* @property {Object} session - The conversation context session.
*/
getContext() {
return {
ids: this.conversationContext.ids,
session: this.conversationContext.requestParams,
};
}
/**
* Queries the Bard chatbot.
* @param {Object} options - The query options.
* @param {Function} [options.onEvent] - The event function.
* @param {Boolean} [options.log] - Whether to log the query.
* @param {Object} [options.context] - The conversation context.
* @param {Object} options.prompt - The prompt for the query.
* @param {AbortSignal} [options.signal] - The abort signal for the query.
* @returns {Object} - The response from the Bard chatbot.
* @property {String} text - The response text.
* @property {Array} ids - The conversation context IDs.
* @property {Array} responses - The response objects.
* @property {Array} searches - The search objects.
* @property {Array} payload - The payload objects.
*/
async query(params) {
params = {
onEvent: () => {},
...params,
};
if (params.signal) {
this.signal = params.signal;
}
if (params.log) {
this.logFunc = params.log;
}
if (params.context) {
this.conversationContext = params.context;
}
if (!this.conversationContext) {
this.conversationContext = {
requestParams: await this.#getParams(),
contextIds: ["", "", ""],
};
}
const { requestParams, contextIds } = this.conversationContext;
if (
!(
this.conversationContext.requestParams &&
this.conversationContext.contextIds
)
) {
throw new Error("Context invalid");
}
/*const resp = await fetch(
'https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?bl=' +
requestParams.blValue +
'&_reqid=' +
generateReqId() +
'&rt=c',
{
method: 'POST',
signal: params.signal,
body: new URLSearchParams({
at: requestParams.atValue,
'f.req': JSON.stringify([null, `[[${JSON.stringify(params.prompt)}],null,${JSON.stringify(contextIds)}]`]),
}),
},
).then((res) => res.text())*/
const resp = await new Promise((resolve, reject) => {
this.xmlhttprequest({
method: "POST",
url:
"https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?bl=" +
requestParams.blValue +
"&_reqid=" +
this.#generateReqId() +
"&rt=c",
data: new URLSearchParams({
at: requestParams.atValue,
"f.req": JSON.stringify([
null,
`[[${JSON.stringify(params.prompt)}],null,${JSON.stringify(
contextIds
)}]`,
]),
}).toString(),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
responseType: "text",
onload: function (response) {
resolve(response.responseText);
},
onerror: function (response) {
reject(new Error("Request failed"));
},
});
});
const { text, ids, ...res } = this.#parseResponse(resp);
this.conversationContext.contextIds = ids;
params.onEvent({
type: "UPDATE_ANSWER",
data: { text },
});
params.onEvent({ type: "DONE" });
return { text, ids, ...res };
}
/**
* Sets the conversation context IDs.
* @param {Array} ids - The conversation context IDs.
*/
setIds(ids) {
this.conversationContext.ids = ids;
}
/**
* Resets the conversation context.
*/
resetConversation() {
this.conversationContext = undefined;
}
/**
* Extracts a value from HTML.
* @private
* @param {String} thing - The value to extract.
* @param {String} html - The HTML to extract the value from.
* @returns {String} - The extracted value.
*/
#extractFromHTML(thing, html) {
const regex = new RegExp(`"${thing}":"([^"]+)"`);
const match = regex.exec(html);
return match?.[1];
}
/**
* Gets the parameters for the Bard chatbot.
* @private
* @returns {Object} - The parameters for the Bard chatbot.
* @property {String} atValue - The at value for the Bard chatbot (session token or something).
* @property {String} blValue - The bl value for the Bard chatbot (bot version).
*/
async #getParams() {
const html = await this.#fetchHtml("https://bard.google.com/faq");
const atValue = this.#extractFromHTML("SNlM0e", html);
const blValue = this.#extractFromHTML("cfb2h", html);
return { atValue, blValue };
}
/**
* Logs a message.
* @private
* @param {...*} args - The arguments to log.
*/
#log(...args) {
if (this.logFunc) {
this.logFunc(...a);
}
}
/**
* Parses the response from the Bard chatbot.
* @private
* @param {String} resp - The response from the Bard chatbot.
* @returns {Object} - The parsed response from the Bard chatbot.
* @property {String} text - The response text.
* @property {Array} ids - The conversation context IDs.
* @property {Array} responses - The response objects.
* @property {Array} searches - The search objects.
* @property {Array} payload - The payload objects.
*/
#parseResponse(resp) {
const data = JSON.parse(resp.split("\n")[3]);
const payload = JSON.parse(data[0][2]);
if (!payload) {
throw new Error("Failed to access Bard");
}
this.#log("PAYLOAD", payload);
const text = payload[0][0];
return {
text,
ids: [...payload[1], payload[4][0][0]],
responses: [
{ ids: [...payload[1], payload[4][0][0]], text: payload[0][0] },
...payload[4].map((i) => ({
ids: [...payload[1], i[0]],
text: i[1][0],
})),
],
searches: payload[2].map((i) => i[0]),
payload,
};
}
/**
* Generates a request ID.
* @private
* @returns {Number} - The generated request ID.
*/
#generateReqId() {
return Math.floor(Math.random() * 900000) + 100000;
}
/**
* Fetches the HTML for a given URL.
* @private
* @param {String} url - The URL to fetch the HTML from.
* @returns {Promise<String>} - A promise that resolves to the HTML for the given URL.
*/
async #fetchHtml(url) {
return new Promise((resolve, reject) => {
this.xmlhttprequest({
method: "GET",
url: url,
onload: (response) => {
if (response.status === 200) {
resolve(response.responseText);
} else {
reject(
new Error(
`Failed to fetch HTML from ${url}. Status code: ${response.status}`
)
);
}
},
onerror: (error) => {
reject(
new Error(`Failed to fetch HTML from ${url}. Error: ${error}`)
);
},
});
});
}
}
function interceptXHR(callback, win) {
const originalXHR = win.XMLHttpRequest;
function newXHR() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
xhr.open = function (method, url) {
this.url = url;
this.method = method;
return originalOpen.apply(this, arguments);
};
xhr.addEventListener("readystatechange", function () {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
callback(xhr.responseText, xhr.url, xhr.method, xhr.requestBody);
}
});
const originalSend = xhr.send;
xhr.send = function (body) {
this.requestBody = body;
return originalSend.apply(this, arguments);
};
return xhr;
}
win.XMLHttpRequest = newXHR;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment