Modernize gtranslate scraper

This commit is contained in:
a 2020-08-17 22:25:26 +02:00
parent 72568aa9b5
commit 67770a9c70
5 changed files with 139 additions and 94 deletions

View file

@ -8,7 +8,7 @@ let con = new BackgroundMessenger();
const scraper = new GTranslateScraper();
con.addMessageListener("translate", async (toTranslate, sender) =>
scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id)
scraper.translate(toTranslate, await getCurrentLanguages(), sender!.tab?.id)
);
con.addMessageListener("getLanguages", () => scraper.languages);
@ -62,8 +62,8 @@ const injectScript = async (tabID: number, enabled?: boolean) => {
con.addMessageListener(
"setEnabled",
async (v: boolean, s: Runtime.MessageSender) => {
const tab = await getTabID(s);
async (v: boolean, s?: Runtime.MessageSender) => {
const tab = await getTabID(s!);
if ((await isEnabledSession(tab)) == v) return;
injectScript(tab);

View file

@ -1,4 +1,4 @@
import { browser } from "webextension-polyfill-ts";
import { ContentScriptMessenger } from "./gtranslate_commands";
export enum MessageKinds {
translationFinished = "translation",
@ -18,11 +18,7 @@ export type Message = TranslationMessage | LanguageListMessage;
if (window.location.host != "translate.google.com" || window.hasRun) return;
window.hasRun = true;
const conn = browser.runtime.connect(<any>{
name: "gtranslate_scraper_conn",
});
const sendMessage = (m: Message) => conn.postMessage(m);
const conn = new ContentScriptMessenger();
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
const result = <HTMLDivElement>(
@ -39,7 +35,6 @@ export type Message = TranslationMessage | LanguageListMessage;
const isTranslating = () => result.classList.contains("translating");
const observer = new MutationObserver((mutations) => {
console.log(mutations);
const wasTranslating = mutations.some((m) =>
m.oldValue!.split(" ").some((c) => c == "translating")
);
@ -54,22 +49,19 @@ export type Message = TranslationMessage | LanguageListMessage;
});
const sendTranslation = () => {
sendMessage({
messageKind: MessageKinds.translationFinished,
translation: {
src: src.value,
result: result.innerText.trim(),
languages: {
srcLang: srcLang,
dstLang: dstLang,
},
conn.runCommand("translationFinished", {
src: src.value,
result: result.innerText.trim(),
languages: {
srcLang: srcLang,
dstLang: dstLang,
},
});
};
conn.onMessage.addListener((to_translate: string) => {
if (src.value == to_translate && !isTranslating()) sendTranslation();
else src.value = to_translate;
conn.addMessageListener("translate", (toTranslate) => {
if (src.value == toTranslate && !isTranslating()) sendTranslation();
else src.value = toTranslate;
});
const getLanguages = () => {
@ -83,8 +75,5 @@ export type Message = TranslationMessage | LanguageListMessage;
return { code: code!, name: name! };
});
};
sendMessage({
messageKind: MessageKinds.languageList,
languages: getLanguages(),
});
conn.runCommand("languageList", getLanguages());
})();

View file

@ -1,6 +1,6 @@
import { browser, WebRequest, Runtime } from "webextension-polyfill-ts";
import content_script from "./gtranslate_content_script.ts?file";
import { Message, MessageKinds } from "./gtranslate_content_script";
import { BackgroundMessenger } from "./gtranslate_commands";
import { newPromise } from "../utils";
interface TranslationRequest {
@ -12,42 +12,50 @@ interface TranslationRequest {
}
export class GTranslateScraper {
private conn?: Runtime.Port;
private queue: Array<TranslationRequest> = [];
private current?: TranslationRequest;
private iframe = <HTMLIFrameElement>document.createElement("iframe");
private current_lang: LanguagePair | undefined;
languages: Promise<Array<Language>>;
lang: LanguagePair | undefined;
msg: BackgroundMessenger;
constructor() {
let languagesResolve: (value: Array<Language>) => void;
[this.languages, languagesResolve] = newPromise<Array<Language>>();
browser.runtime.onConnect.addListener((p) => {
if (p.name != "gtranslate_scraper_conn") return;
this.conn = p;
this.processFromQueue(p);
p.onMessage.addListener((m: Message) => {
switch (m.messageKind) {
case MessageKinds.languageList:
languagesResolve(m.languages);
return;
case MessageKinds.translationFinished:
this.onTranslationRecieved(m.translation);
}
});
});
this.loadIframe("de", "en"); //Sample languages so we can get the language list
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 loadIframe(srcLangCode: string, dstLangCode: string) {
private async ChangeLanguage(
srcLangCode: string,
dstLangCode: string
): Promise<void> {
if (!document.body.contains(this.iframe))
document.body.appendChild(this.iframe);
this.conn = undefined;
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: ["<all_urls>"],
matches: ["https://translate.google.com/?*"],
allFrames: true,
js: [
{
@ -57,39 +65,60 @@ export class GTranslateScraper {
runAt: "document_end",
})
.then((r) => {
this.iframe.addEventListener("load", r.unregister, { once: true });
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}`;
});
//This allows us to create the google translate iframe by removing the x-frame-options header
browser.webRequest.onHeadersReceived.addListener(
allowIFrameAccess,
{
urls: ["<all_urls>"],
types: ["sub_frame"],
},
["blocking", "responseHeaders"]
);
return connectedPromise;
}
private onTranslationRecieved(trans: Translation) {
const current = this.queue.pop();
if (current) current.resolveFn(trans);
this.processFromQueue(this.conn!);
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 processFromQueue(conn: Runtime.Port) {
const request = this.queue.slice(-1).pop();
if (!request) return;
private async processFromQueue() {
if (this.current) return;
this.current = this.queue.pop();
if (!this.current) return;
if (this.lang != request.lang) {
this.lang = request.lang;
this.loadIframe(this.lang.srcLang.code, this.lang.dstLang.code);
} else conn.postMessage(request.toTranslate);
this.processCurrent();
}
translate(
private async processCurrent() {
if (!this.current)
throw "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 "The current request changed while processCurrent was running. This should never happen";
}
async translate(
toTranslate: string,
langs: LanguagePair,
tabID?: number
@ -119,9 +148,14 @@ export class GTranslateScraper {
return true;
}
});
this.queue.push(req);
if (this.queue.length == 1 && this.conn) this.processFromQueue(this.conn);
this.queue.push(req);
if (this.queue.length == 1 && !this.current) {
this.current = this.queue.pop();
await this.msg.conn;
this.processCurrent();
}
return promise;
}
}

View file

@ -1,7 +1,7 @@
import { Communicator, commandFunction, commandList } from "./communicator";
import { browser } from "webextension-polyfill-ts";
export interface commands extends commandList {
interface BackgroundCommands extends commandList {
setEnabled: setEnabled;
getEnabled: getEnabled;
translate: translate;
@ -12,6 +12,10 @@ export interface commands extends commandList {
setCurrentLanguages: setCurrentLanguages;
}
interface FrontendCommands extends commandList {
setEnabled: setEnabled;
}
interface setEnabled extends commandFunction {
args: boolean;
}
@ -46,14 +50,17 @@ interface setCurrentLanguages extends commandFunction {
args: Partial<LanguagePair>;
}
export class BackgroundMessenger extends Communicator<commands> {
export class BackgroundMessenger extends Communicator<
BackgroundCommands,
FrontendCommands
> {
constructor() {
super();
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
}
runCommand<K extends keyof commands>(
runCommand<K extends keyof BackgroundCommands>(
command: K,
args: commands[K]["args"],
args: BackgroundCommands[K]["args"],
tabID: number
) {
const msg = this.getCommandMessage(command, args);
@ -61,16 +68,19 @@ export class BackgroundMessenger extends Communicator<commands> {
}
}
export class FrontendMessenger extends Communicator<commands> {
export class FrontendMessenger extends Communicator<
FrontendCommands,
BackgroundCommands
> {
constructor() {
super();
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
}
runCommand<K extends keyof commands>(
runCommand<K extends keyof BackgroundCommands>(
command: K,
args: commands[K]["args"]
): commands[K]["return"] {
args: BackgroundCommands[K]["args"]
): BackgroundCommands[K]["return"] {
const message = this.getCommandMessage(command, args);
return browser.runtime.sendMessage(message);
}

View file

@ -16,35 +16,47 @@ interface commandMessage<T extends commandList, K extends keyof T> {
declare type listener<T extends commandList, K extends keyof T> = (
args: T[K]["args"],
s: Runtime.MessageSender
) => Promise<T[K]["return"]>;
s?: Runtime.MessageSender
) => Promise<T[K]["return"]> | T[K]["return"];
export abstract class Communicator<T extends commandList> {
private listeners = new Map<keyof T, listener<T, keyof T>>();
export abstract class Communicator<
RecvCommands extends commandList,
SendCommands extends commandList
> {
private listeners = new Map<
keyof RecvCommands,
listener<RecvCommands, keyof RecvCommands>
>();
abstract runCommand<K extends keyof T>(
abstract async runCommand<K extends keyof SendCommands>(
command: K,
args: T[K]["args"],
args: SendCommands[K]["args"],
...rest: any[]
): Promise<T[K]["return"]>;
): Promise<SendCommands[K]["return"]>;
addMessageListener<K extends keyof T>(command: K, callback: listener<T, K>) {
addMessageListener<K extends keyof RecvCommands>(
command: K,
callback: listener<RecvCommands, K>
) {
this.listeners.set(command, <any>callback);
}
getCommandMessage<K extends keyof T>(
protected getCommandMessage<K extends keyof RecvCommands>(
command: K,
...args: T[K]["args"]
): commandMessage<T, K> {
...args: RecvCommands[K]["args"]
): commandMessage<RecvCommands, K> {
return { name: command, args: args };
}
onMessage<K extends keyof T>(
m: commandMessage<T, K>,
s: Runtime.MessageSender
protected onMessage<K extends keyof RecvCommands>(
m: commandMessage<RecvCommands, K>,
s?: Runtime.MessageSender
) {
let listener = this.listeners.get(m.name);
let args: [T[keyof T]["args"], Runtime.MessageSender] = m.args;
let args: [
RecvCommands[keyof RecvCommands]["args"],
Runtime.MessageSender
] = m.args;
if (args[0] === undefined) args.pop();
args.push(s);
if (listener) return listener.apply(null, args);