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

View file

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

View file

@ -1,6 +1,6 @@
import { browser, WebRequest, Runtime } from "webextension-polyfill-ts"; import { browser, WebRequest, Runtime } from "webextension-polyfill-ts";
import content_script from "./gtranslate_content_script.ts?file"; 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"; import { newPromise } from "../utils";
interface TranslationRequest { interface TranslationRequest {
@ -12,42 +12,50 @@ interface TranslationRequest {
} }
export class GTranslateScraper { export class GTranslateScraper {
private conn?: Runtime.Port;
private queue: Array<TranslationRequest> = []; private queue: Array<TranslationRequest> = [];
private current?: TranslationRequest;
private iframe = <HTMLIFrameElement>document.createElement("iframe"); private iframe = <HTMLIFrameElement>document.createElement("iframe");
private current_lang: LanguagePair | undefined;
languages: Promise<Array<Language>>; languages: Promise<Array<Language>>;
lang: LanguagePair | undefined; msg: BackgroundMessenger;
constructor() { constructor() {
let languagesResolve: (value: Array<Language>) => void; let languagesResolve: (value: Array<Language>) => void;
[this.languages, languagesResolve] = newPromise<Array<Language>>(); [this.languages, languagesResolve] = newPromise<Array<Language>>();
browser.runtime.onConnect.addListener((p) => { this.msg = new BackgroundMessenger();
if (p.name != "gtranslate_scraper_conn") return; this.msg.addMessageListener("languageList", languagesResolve);
this.conn = p; this.msg.addMessageListener("translationFinished", (trans) =>
this.processFromQueue(p); this.onTranslationRecieved(trans)
p.onMessage.addListener((m: Message) => { );
switch (m.messageKind) {
case MessageKinds.languageList: //This allows us to create the google translate iframe by removing the x-frame-options header
languagesResolve(m.languages); browser.webRequest.onHeadersReceived.addListener(
return; allowIFrameAccess,
case MessageKinds.translationFinished: {
this.onTranslationRecieved(m.translation); urls: ["https://translate.google.com/?*"],
} types: ["sub_frame"],
}); },
}); ["blocking", "responseHeaders"]
this.loadIframe("de", "en"); //Sample languages so we can get the language list );
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)) if (!document.body.contains(this.iframe))
document.body.appendChild(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 //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 browser.contentScripts
.register({ .register({
matches: ["<all_urls>"], matches: ["https://translate.google.com/?*"],
allFrames: true, allFrames: true,
js: [ js: [
{ {
@ -57,39 +65,60 @@ export class GTranslateScraper {
runAt: "document_end", runAt: "document_end",
}) })
.then((r) => { .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.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 return connectedPromise;
browser.webRequest.onHeadersReceived.addListener(
allowIFrameAccess,
{
urls: ["<all_urls>"],
types: ["sub_frame"],
},
["blocking", "responseHeaders"]
);
} }
private onTranslationRecieved(trans: Translation) { private onTranslationRecieved(trans: Translation) {
const current = this.queue.pop(); if (this.current?.toTranslate !== trans.src)
if (current) current.resolveFn(trans); //I don't know how to code well so I gotta put in sanity checks like this to make debugging easier
this.processFromQueue(this.conn!); 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) { private async processFromQueue() {
const request = this.queue.slice(-1).pop(); if (this.current) return;
if (!request) return; this.current = this.queue.pop();
if (!this.current) return;
if (this.lang != request.lang) { this.processCurrent();
this.lang = request.lang;
this.loadIframe(this.lang.srcLang.code, this.lang.dstLang.code);
} else conn.postMessage(request.toTranslate);
} }
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, toTranslate: string,
langs: LanguagePair, langs: LanguagePair,
tabID?: number tabID?: number
@ -119,9 +148,14 @@ export class GTranslateScraper {
return true; 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; return promise;
} }
} }

View file

@ -1,7 +1,7 @@
import { Communicator, commandFunction, commandList } from "./communicator"; import { Communicator, commandFunction, commandList } from "./communicator";
import { browser } from "webextension-polyfill-ts"; import { browser } from "webextension-polyfill-ts";
export interface commands extends commandList { interface BackgroundCommands extends commandList {
setEnabled: setEnabled; setEnabled: setEnabled;
getEnabled: getEnabled; getEnabled: getEnabled;
translate: translate; translate: translate;
@ -12,6 +12,10 @@ export interface commands extends commandList {
setCurrentLanguages: setCurrentLanguages; setCurrentLanguages: setCurrentLanguages;
} }
interface FrontendCommands extends commandList {
setEnabled: setEnabled;
}
interface setEnabled extends commandFunction { interface setEnabled extends commandFunction {
args: boolean; args: boolean;
} }
@ -46,14 +50,17 @@ interface setCurrentLanguages extends commandFunction {
args: Partial<LanguagePair>; args: Partial<LanguagePair>;
} }
export class BackgroundMessenger extends Communicator<commands> { export class BackgroundMessenger extends Communicator<
BackgroundCommands,
FrontendCommands
> {
constructor() { constructor() {
super(); super();
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s)); browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
} }
runCommand<K extends keyof commands>( runCommand<K extends keyof BackgroundCommands>(
command: K, command: K,
args: commands[K]["args"], args: BackgroundCommands[K]["args"],
tabID: number tabID: number
) { ) {
const msg = this.getCommandMessage(command, args); 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() { constructor() {
super(); super();
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s)); browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
} }
runCommand<K extends keyof commands>( runCommand<K extends keyof BackgroundCommands>(
command: K, command: K,
args: commands[K]["args"] args: BackgroundCommands[K]["args"]
): commands[K]["return"] { ): BackgroundCommands[K]["return"] {
const message = this.getCommandMessage(command, args); const message = this.getCommandMessage(command, args);
return browser.runtime.sendMessage(message); 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> = ( declare type listener<T extends commandList, K extends keyof T> = (
args: T[K]["args"], args: T[K]["args"],
s: Runtime.MessageSender s?: Runtime.MessageSender
) => Promise<T[K]["return"]>; ) => Promise<T[K]["return"]> | T[K]["return"];
export abstract class Communicator<T extends commandList> { export abstract class Communicator<
private listeners = new Map<keyof T, listener<T, keyof T>>(); 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, command: K,
args: T[K]["args"], args: SendCommands[K]["args"],
...rest: any[] ...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); this.listeners.set(command, <any>callback);
} }
getCommandMessage<K extends keyof T>( protected getCommandMessage<K extends keyof RecvCommands>(
command: K, command: K,
...args: T[K]["args"] ...args: RecvCommands[K]["args"]
): commandMessage<T, K> { ): commandMessage<RecvCommands, K> {
return { name: command, args: args }; return { name: command, args: args };
} }
onMessage<K extends keyof T>( protected onMessage<K extends keyof RecvCommands>(
m: commandMessage<T, K>, m: commandMessage<RecvCommands, K>,
s: Runtime.MessageSender s?: Runtime.MessageSender
) { ) {
let listener = this.listeners.get(m.name); 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(); if (args[0] === undefined) args.pop();
args.push(s); args.push(s);
if (listener) return listener.apply(null, args); if (listener) return listener.apply(null, args);