flashlang/src/background/gtranslate_scraper.ts

178 lines
4.8 KiB
TypeScript

import { browser, WebRequest } from "webextension-polyfill-ts";
import content_script from "./gtranslate_content_script.ts?file";
import { BackgroundMessenger } from "./gtranslate_commands";
import { newPromise } from "../utils";
interface TranslationRequest {
tabID?: number;
resolveFn: (value: Translation) => void;
rejectFn: (reason: string) => void;
toTranslate: string;
lang: LanguagePair;
}
export class GTranslateScraper {
private queue: Array<TranslationRequest> = [];
private current?: TranslationRequest;
private iframe = <HTMLIFrameElement>document.createElement("iframe");
private current_lang: LanguagePair | undefined;
languages: Promise<Array<Language>>;
msg: BackgroundMessenger;
constructor() {
let languagesResolve: (value: Array<Language>) => void;
[this.languages, languagesResolve] = newPromise<Array<Language>>();
this.msg = new BackgroundMessenger();
this.msg.addMessageListener("languageList", languagesResolve);
this.msg.addMessageListener("translationFinished", (trans) =>
this.onTranslationRecieved(trans)
);
//This allows us to create the google translate iframe by removing the x-frame-options header
browser.webRequest.onHeadersReceived.addListener(
allowIFrameAccess,
{
urls: ["https://translate.google.com/?*"],
types: ["sub_frame"],
},
["blocking", "responseHeaders"]
);
this.ChangeLanguage("de", "en"); //Sample languages so we can get the language list
}
private async ChangeLanguage(
srcLangCode: string,
dstLangCode: string
): Promise<void> {
if (!document.body.contains(this.iframe))
document.body.appendChild(this.iframe);
const [connectedPromise, onConnected] = newPromise<void>();
//Registers a temp content script, because we cannot inject scripts into iframes created on the background html page, because they have no tabId
browser.contentScripts
.register({
matches: ["https://translate.google.com/?*"],
allFrames: true,
js: [
{
file: content_script,
},
],
runAt: "document_end",
})
.then((r) => {
this.iframe.addEventListener(
"load",
() => {
r.unregister;
this.msg.conn.then(() => onConnected());
},
{ once: true }
);
this.iframe.src = `https://translate.google.com/?op=translate&sl=${srcLangCode}&tl=${dstLangCode}`;
});
return connectedPromise;
}
private onTranslationRecieved(trans: Translation) {
if (this.current?.toTranslate !== trans.src)
//I don't know how to code well so I gotta put in sanity checks like this to make debugging easier
console.error(
"Current request changed before OnTranslationRecieved was called. This should never happen"
);
if (this.current) this.current.resolveFn(trans);
this.current = undefined;
this.processFromQueue();
}
private async processFromQueue() {
if (this.current) return;
this.current = this.queue.pop();
if (!this.current) return;
this.processCurrent();
}
private async processCurrent() {
if (!this.current)
throw new Error(
"ProcessCurrent called while current translation request is undefined"
);
const cur = this.current;
let promise = Promise.resolve();
if (this.current_lang != this.current.lang) {
this.current_lang = cur.lang;
promise = this.ChangeLanguage(
this.current_lang.srcLang.code,
this.current_lang.dstLang.code
);
}
await promise;
if (cur == this.current) this.msg.runCommand("translate", cur.toTranslate);
else
throw new Error(
"The current request changed while processCurrent was running. This should never happen"
);
}
async translate(
toTranslate: string,
langs: LanguagePair,
tabID?: number
): Promise<Translation> {
toTranslate = toTranslate.trim();
if (toTranslate == "")
return Promise.resolve({
src: "",
result: "",
languages: langs,
});
const [promise, resolve, reject] = newPromise<Translation>();
const req = {
toTranslate: toTranslate,
resolveFn: resolve,
rejectFn: reject,
tabID: tabID,
lang: langs,
};
//Remove the requests from the same tab
if (tabID)
this.queue = this.queue.filter((r) => {
if (r.tabID !== tabID) {
r.rejectFn("Got another request from the same tab");
return true;
}
});
this.queue.push(req);
if (this.queue.length == 1 && !this.current) {
this.current = this.queue.pop();
await this.msg.conn;
this.processCurrent();
}
return promise;
}
}
const allowIFrameAccess = (info: WebRequest.OnHeadersReceivedDetailsType) => {
//Make sure that only the background page can access this iframe
if (info.documentUrl != document.URL) return;
let headers = info.responseHeaders;
headers = headers?.filter((header) => {
const h = header.name.toString().toLowerCase();
h != "x-frame-options" && h != "frame-options";
});
return { responseHeaders: headers };
};