178 lines
4.8 KiB
TypeScript
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 };
|
|
};
|