Modernize gtranslate scraper
This commit is contained in:
parent
72568aa9b5
commit
67770a9c70
5 changed files with 139 additions and 94 deletions
|
@ -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);
|
||||||
|
|
|
@ -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,
|
|
||||||
translation: {
|
|
||||||
src: src.value,
|
src: src.value,
|
||||||
result: result.innerText.trim(),
|
result: result.innerText.trim(),
|
||||||
languages: {
|
languages: {
|
||||||
srcLang: srcLang,
|
srcLang: srcLang,
|
||||||
dstLang: dstLang,
|
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(),
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue