From 41f0a7e01df7e94a38bd389770de038ad30605f5 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 4 Aug 2020 00:38:41 +0200 Subject: [PATCH] Add gtranslate script --- src/background/background.ts | 8 ++- src/background/gtranslate_content_script.ts | 46 ++++++++++++ src/background/gtranslate_scraper.ts | 77 +++++++++++++++++++++ src/background/gtranslate_types.ts | 5 ++ src/types.d.ts | 17 +++-- tsconfig.json | 1 - webpack.config.js | 13 +++- 7 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 src/background/gtranslate_content_script.ts create mode 100644 src/background/gtranslate_scraper.ts create mode 100644 src/background/gtranslate_types.ts diff --git a/src/background/background.ts b/src/background/background.ts index d65da08..39dcca3 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -2,6 +2,10 @@ import { browser, Runtime, Tabs } from "webextension-polyfill-ts"; import { GTranslateScraper } from "./gtranslate_scraper"; console.log("Background script loaded"); + +const scraper = new GTranslateScraper(); +console.log(scraper); + const getTab = async (s: Runtime.MessageSender): Promise => { if (s.tab) return s.tab; let tabs = await browser.tabs.query({ @@ -47,6 +51,8 @@ browser.runtime.onMessage.addListener( return setEnabledCommand(c.value, s); case commands.getEnabled: return getEnabledCommand(s); + case commands.translate: + return scraper.translate(c.toTranslate); } } ); @@ -58,5 +64,3 @@ browser.tabs.onUpdated.addListener(async (tabID, changeInfo) => { } } }); - -new GTranslateScraper(); diff --git a/src/background/gtranslate_content_script.ts b/src/background/gtranslate_content_script.ts new file mode 100644 index 0000000..ee5d5b1 --- /dev/null +++ b/src/background/gtranslate_content_script.ts @@ -0,0 +1,46 @@ +import { browser } from "webextension-polyfill-ts"; +import "./gtranslate_types"; +declare interface Window { + hasRun: boolean; +} + +(async () => { + if (window.location.host != "translate.google.com" || window.hasRun) return; + window.hasRun = true; + + const conn = browser.runtime.connect({ + name: "gtranslate_scraper_conn", + }); + + const src = document.querySelector("textarea#source"); + + const result = ( + document.querySelector("div.results-container") + ); + + 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") + ); + if (wasTranslating && !isTranslating()) { + sendTranslation(); + } + }); + observer.observe(result, { + attributes: true, + attributeOldValue: true, + attributeFilter: ["class"], + }); + + const sendTranslation = () => { + conn.postMessage({ src: src.value, result: result.innerText.trim() }); + }; + + conn.onMessage.addListener((to_translate: string) => { + if (src.value == to_translate && !isTranslating()) sendTranslation(); + else src.value = to_translate; + }); +})(); diff --git a/src/background/gtranslate_scraper.ts b/src/background/gtranslate_scraper.ts new file mode 100644 index 0000000..a884d6b --- /dev/null +++ b/src/background/gtranslate_scraper.ts @@ -0,0 +1,77 @@ +import "./gtranslate_types"; +import { browser, WebRequest, Runtime } from "webextension-polyfill-ts"; + +export class GTranslateScraper { + iframe: HTMLIFrameElement; + conn?: Runtime.Port; + resolveFn?: (value: TranslationResponse) => void; + rejectFn?: (reason: String) => void; + + constructor() { + browser.runtime.onConnect.addListener((p) => { + if (p.name != "gtranslate_scraper_conn") return; + this.conn = p; + p.onMessage.addListener((m) => this.onTranslationRecieved(m)); + }); + + //This allows us to create the google translate iframe by removing the x-frame-options header + browser.webRequest.onHeadersReceived.addListener( + allowIFrameAccess, + + { + urls: [""], + types: ["sub_frame"], + }, + ["blocking", "responseHeaders"] + ); + let iframe = document.createElement("iframe"); + + //TODO make the language customizable + iframe.src = "https://translate.google.com/?op=translate&sl=de&tl=en"; + this.iframe = iframe; + + //Registers a temp content script, because we cannot inject scripts into iframes created on the background html page, because they have no tabId + const js = { + file: "gtranslate_scraper.bundle.js", + }; + browser.contentScripts + .register({ + matches: [""], + allFrames: true, + js: [js], + runAt: "document_end", + }) + .then((r) => { + iframe.addEventListener("load", r.unregister); + document.body.appendChild(iframe); + }); + } + onTranslationRecieved(message: TranslationResponse) { + this.resolveFn(message); + this.resolveFn = null; + this.rejectFn = null; + } + + translate(to_translate: string): Promise { + if (this.rejectFn) this.rejectFn("Got a different translation request"); + if (!this.conn) return Promise.reject("The translator is not yet ready"); + + this.conn.postMessage(to_translate); + return new Promise((resolve, reject) => { + this.resolveFn = resolve; + this.rejectFn = reject; + }); + } +} + +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) => { + let h = header.name.toString().toLowerCase(); + h != "x-frame-options" && h != "frame-options"; + }); + return { responseHeaders: headers }; +}; diff --git a/src/background/gtranslate_types.ts b/src/background/gtranslate_types.ts new file mode 100644 index 0000000..99c565f --- /dev/null +++ b/src/background/gtranslate_types.ts @@ -0,0 +1,5 @@ +type TranslationRequest = string; +interface TranslationResponse { + src: string; + result: string; +} diff --git a/src/types.d.ts b/src/types.d.ts index 6608afa..19bafd7 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,11 +1,12 @@ -declare module "*?file" { - const file: string; - export = file; +declare module "*?raw" { + const contents: string; + export = contents; } declare const enum commands { - setEnabled = "toggleCommand", + setEnabled = "setEnabled", getEnabled = "getEnabled", + translate = "translate", } interface setEnabled { commandKind: commands.setEnabled; @@ -15,4 +16,10 @@ interface setEnabled { interface getEnabled { commandKind: commands.getEnabled; } -type command = setEnabled | getEnabled; + +interface translate { + commandKind: commands.translate; + toTranslate: string; +} + +type command = setEnabled | getEnabled | translate; diff --git a/tsconfig.json b/tsconfig.json index 6a2253f..3062f4b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ "noImplicitAny": true, "module": "CommonJS", "target": "es6", - "jsx": "react", "allowJs": true, "sourceMap": true, "esModuleInterop": true diff --git a/webpack.config.js b/webpack.config.js index 05be6c4..1e844ea 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,6 +16,12 @@ let options = { "content_script", "content_script.ts" ), + gtranslate_scraper: path.join( + __dirname, + "src", + "background", + "gtranslate_content_script.ts" + ), background: path.join(__dirname, "src", "background", "background.ts"), }, output: { @@ -39,7 +45,8 @@ let options = { oneOf: [ { resourceQuery: /file/, - use: ["ts-loader", "file-loader"], + use: ["file-loader", "ts-loader"], + type: "javascript/esm", }, { use: "ts-loader", @@ -51,8 +58,8 @@ let options = { test: /\.(c|s[ac])ss$/i, oneOf: [ { - resourceQuery: /file/, - use: ["file-loader", "css-loader", "sass-loader"], + resourceQuery: /raw/, + use: ["raw-loader", "sass-loader"], }, { use: ["style-loader", "css-loader", "sass-loader"],