├── Extension ├── Pages │ └── .gitkeep ├── icons │ ├── 128x128.png │ ├── 16x16.png │ ├── 48x48.png │ └── 540x540.png ├── vendor │ ├── get_app-black-18dp.svg │ └── delete-black-18dp.svg ├── logReader.js ├── manifest.json ├── Iframe.js ├── popup.html ├── Presence.js └── background.js ├── babel.config.js ├── src ├── main.js └── App.vue ├── .gitignore ├── .editorconfig ├── package ├── pack.js └── updateVersion.js ├── README.md ├── icon.svg ├── overwrite └── compileChanged.ts ├── locale.js ├── .github └── workflows │ └── deploy.yml ├── webpack.config.js ├── package.json ├── presenceUpdater.ts └── LICENSE /Extension/Pages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Extension/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolamtisch/PreWrap/HEAD/Extension/icons/128x128.png -------------------------------------------------------------------------------- /Extension/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolamtisch/PreWrap/HEAD/Extension/icons/16x16.png -------------------------------------------------------------------------------- /Extension/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolamtisch/PreWrap/HEAD/Extension/icons/48x48.png -------------------------------------------------------------------------------- /Extension/icons/540x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolamtisch/PreWrap/HEAD/Extension/icons/540x540.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | el: "#app", 6 | render: (h) => h(App), 7 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | presenceUpdater.js 2 | Presences/ 3 | Localization/ 4 | node_modules/ 5 | Extension/Pages/ 6 | Extension/webpack/ 7 | webextension.zip 8 | -------------------------------------------------------------------------------- /Extension/vendor/get_app-black-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | charset = utf-8 3 | 4 | trim_trailing_whitespace = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.js] 13 | end_of_line = lf 14 | insert_final_newline = true 15 | indent_style = space 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /Extension/vendor/delete-black-18dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package/pack.js: -------------------------------------------------------------------------------- 1 | const archiver = require("archiver"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | 5 | const dist = path.join(__dirname, ".."); 6 | 7 | const output = fs.createWriteStream(path.join(dist, "/webextension.zip")); 8 | const archive = archiver("zip", { 9 | zlib: { level: 9 }, 10 | }); 11 | archive.pipe(output); 12 | archive.directory(path.join(dist, "Extension"), false); 13 | archive.finalize(); 14 | -------------------------------------------------------------------------------- /package/updateVersion.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | 5 | var manifest = path.join(__dirname, "..", "Extension", "manifest.json"); 6 | fs.readFile(manifest, "utf8", function (err, data) { 7 | if (err) { 8 | return console.log(err); 9 | } 10 | 11 | const ver = new Date().toISOString().replace(/T.*/, "").replace(/-0*/g, "."); 12 | 13 | const result = data.replace( 14 | /"version": "[\d|\.]+",/g, 15 | `"version": "${ver}",` 16 | ); 17 | 18 | fs.writeFile(manifest, result, "utf8", function (err) { 19 | if (err) return console.log(err); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /Extension/logReader.js: -------------------------------------------------------------------------------- 1 | const PreWrap_consoleLogger = document.createElement("script"); 2 | PreWrap_consoleLogger.type = "text/javascript"; 3 | PreWrap_consoleLogger.id = "PreWrap_consoleLogger"; 4 | PreWrap_consoleLogger.innerText = ` 5 | console.stdlog = console.log.bind(console); 6 | console.logs = []; 7 | console.log = function() { 8 | let inc = Array.from(arguments); 9 | inc.forEach(arg => { 10 | console.logs.push(arg); 11 | }); 12 | while (console.logs.length > 100) { 13 | console.logs.shift(); 14 | } 15 | console.stdlog.apply(console, arguments); 16 | }; 17 | `; 18 | document.head.appendChild(PreWrap_consoleLogger); 19 | -------------------------------------------------------------------------------- /Extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "PreWrap", 4 | "version": "0.0.1", 5 | "icons": { 6 | "16": "icons/16x16.png", 7 | "48": "icons/48x48.png", 8 | "128": "icons/128x128.png" 9 | }, 10 | "permissions": [ 11 | "storage", 12 | "activeTab", 13 | "webNavigation" 14 | ], 15 | "optional_permissions": [ 16 | "http://*/*", 17 | "https://*/*" 18 | ], 19 | "background": { 20 | "scripts": [ 21 | "Pages/pages.js", 22 | "Pages/locale.js", 23 | "background.js" 24 | ], 25 | "persistent": false 26 | }, 27 | "browser_action": { 28 | "default_popup": "popup.html" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PreWrap 2 | [PreMiD Presences](https://github.com/PreMiD/Presences) wrapper for the [Discord-RPC-Extension](https://github.com/lolamtisch/Discord-RPC-Extension) with focus on security and performance. 3 | The included presences are not created nor maintained by me. All credit goes to the [PreMiD](https://premid.app/) team and their [contributors](https://premid.app/contributors). 4 | 5 | ### Download 6 | Chrome 7 | Firefox 8 | 9 | ### What is the difference to PreMiD 10 | * No remote javascript execution or remote dependencies 11 | * Permission requested only for the active Presences 12 | * minimal content script and it only gets injected for the active Presence pages. 13 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /overwrite/compileChanged.ts: -------------------------------------------------------------------------------- 1 | import actions from "@actions/core"; 2 | import chalk from "chalk"; 3 | 4 | import PresenceCompiler from "../classes/PresenceCompiler.js"; 5 | 6 | import { basename, dirname } from "node:path"; 7 | import glob from "glob"; 8 | 9 | const compiler = new PresenceCompiler(), 10 | changedFolders = getDiff(); 11 | 12 | if (!changedFolders.length) 13 | actions.info(chalk.green("No Presences changed, exiting...")); 14 | else { 15 | const errors = await compiler.compilePresence(changedFolders); 16 | 17 | for (const error of errors.filter(error => !error.name.includes("TS"))) 18 | actions.error(error); 19 | 20 | //if (errors.length) 21 | //actions.setFailed("Some Presences failed to compile, exiting..."); 22 | } 23 | 24 | export function getDiff(): string[] { 25 | const changedPresenceFolders = glob.sync("websites/*/*/metadata.json", { absolute: true }) 26 | 27 | if (!changedPresenceFolders.length) return []; 28 | 29 | return [...new Set(changedPresenceFolders.map(f => basename(dirname(f))))]; 30 | } 31 | -------------------------------------------------------------------------------- /Extension/Iframe.js: -------------------------------------------------------------------------------- 1 | if (typeof oldLog !== 'undefined' && oldLog) { 2 | console.error('Content and Iframe executed at the same time'); 3 | console.log = oldLog; 4 | } 5 | 6 | console.log = (function () { 7 | return Function.prototype.bind.call( 8 | console.log, 9 | console, 10 | "%cIframe", 11 | "background-color: #312f40; color: white; padding: 2px 10px; border-radius: 3px;" 12 | ); 13 | })(); 14 | 15 | var activeIframePresence = null; 16 | 17 | class iFrame { 18 | constructor() { 19 | this._events = []; 20 | console.log('loaded', this.getUrl()) 21 | activeIframePresence = this; 22 | } 23 | 24 | getUrl() { 25 | return window.location.href; 26 | } 27 | 28 | on(eventName, callback) { 29 | this._events[eventName] = callback; 30 | } 31 | 32 | send(data) { 33 | console.log("send", data); 34 | chrome.runtime.sendMessage({ type: "iframeData", data: data }); 35 | } 36 | 37 | callEvent() { 38 | this._events["UpdateData"](); 39 | } 40 | } 41 | 42 | chrome.runtime.onMessage.addListener(function (info, sender, sendResponse) { 43 | if (info.type === 'presence') { 44 | console.log('Iframe data requested', info); 45 | activeIframePresence.callEvent(); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /locale.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const fs = require('fs') 3 | 4 | console.log('Generate locale'); 5 | 6 | main(); 7 | 8 | function main() { 9 | let = langRes = {}; 10 | 11 | getFolder('./Localization/src/Extension').forEach(el => { 12 | const tLang = readFile('./Localization/src/Extension/'+el); 13 | langRes = {...langRes, ...tLang}; 14 | }); 15 | 16 | getFolder('./Localization/src/Presence').forEach(el => { 17 | const tLang = readFile('./Localization/src/Presence/'+el); 18 | langRes = {...langRes, ...tLang}; 19 | }); 20 | 21 | writeObj(langRes); 22 | } 23 | 24 | function getFolder(path) { 25 | return fs.readdirSync(path); 26 | } 27 | 28 | function readFile(path) { 29 | const data = fs.readFileSync(path, 'utf8'); 30 | const json = JSON.parse(data); 31 | const res = {}; 32 | for (const el in json) { 33 | res[el] = json[el].message; 34 | } 35 | 36 | return res; 37 | } 38 | 39 | function writeObj(obj) { 40 | console.log(obj); 41 | const content = ` 42 | var language = ${JSON.stringify(obj)}; 43 | `; 44 | fs.writeFileSync('./Extension/Pages/locale.js', content); 45 | } 46 | 47 | async function getJson(url) { 48 | return new Promise((resolve, reject) => { 49 | request({url: url}, function (error, response, body) { 50 | if(error) { 51 | console.error('error:', error); 52 | reject(error); 53 | return; 54 | } 55 | if(response && response.statusCode === 200) { 56 | resolve(JSON.parse(body)); 57 | } 58 | }); 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deployment 2 | on: 3 | schedule: 4 | - cron: '0 0 * * 0' 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | Firefox: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: '16.x' 18 | - name: Build 19 | run: | 20 | npm ci 21 | npm run build 22 | env: 23 | APP_TARGET: firefox 24 | - name: Pack 25 | run: | 26 | npm run pack:webextension 27 | - uses: trmcnvn/firefox-addon@v1 28 | with: 29 | uuid: '{9474c3da-698d-46ca-a50f-e8ff1ebdfff1}' 30 | xpi: webextension.zip 31 | manifest: Extension/manifest.json 32 | api-key: ${{ secrets.FIREFOX_API_KEY }} 33 | api-secret: ${{ secrets.FIREFOX_API_SECRET }} 34 | 35 | Chrome: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions/setup-node@v1 40 | with: 41 | node-version: '16.x' 42 | - name: Build 43 | run: | 44 | npm ci 45 | npm run build 46 | env: 47 | APP_TARGET: chrome 48 | - name: Pack 49 | run: | 50 | npm run pack:webextension 51 | - name: Deploy 52 | uses: trmcnvn/chrome-addon@v2 53 | with: 54 | extension: calpcokkjmookodfpbmdbknfcjhekgaj 55 | zip: webextension.zip 56 | client-id: ${{ secrets.DEV_CHROME_CLIENT_ID }} 57 | client-secret: ${{ secrets.DEV_CHROME_CLIENT_SECRET }} 58 | refresh-token: ${{ secrets.DEV_CHROME_REFRESH_TOKEN }} 59 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | const VueLoaderPlugin = require("vue-loader/lib/plugin"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | 6 | module.exports = { 7 | entry: "./src/main.js", 8 | output: { 9 | path: path.resolve(__dirname, "./Extension/webpack"), 10 | publicPath: "/dist/", 11 | filename: "popup.js", 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | loader: "babel-loader", 18 | exclude: /node_modules/, 19 | }, 20 | { 21 | test: /\.less$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | "vue-style-loader", 25 | { loader: "to-string-loader" }, 26 | { loader: "css-loader" }, 27 | { loader: "less-loader" }, 28 | ], 29 | }, 30 | { 31 | test: /\.vue$/, 32 | exclude: /node_modules/, 33 | loader: "vue-loader", 34 | options: { 35 | shadowMode: true, 36 | }, 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | alias: { 42 | vue$: "vue/dist/vue.esm.js", 43 | }, 44 | extensions: ["*", ".js", ".vue", ".json"], 45 | }, 46 | devServer: { 47 | historyApiFallback: true, 48 | noInfo: true, 49 | overlay: true, 50 | }, 51 | performance: { 52 | hints: false, 53 | }, 54 | devtool: "source-map", 55 | plugins: [ 56 | new VueLoaderPlugin(), 57 | new webpack.optimize.LimitChunkCountPlugin({ 58 | maxChunks: 1, 59 | }), 60 | new TerserPlugin({ 61 | terserOptions: { 62 | output: { 63 | beautify: true, 64 | comments: false, 65 | }, 66 | mangle: false, 67 | compress: true, 68 | }, 69 | }), 70 | ], 71 | optimization: { 72 | minimize: false, 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "presences", 3 | "version": "3.0.0", 4 | "author": "Timeraa", 5 | "license": "MPL-2.0", 6 | "scripts": { 7 | "cleanup": "rm -rf Localization && rm -rf Presences && rm -rf Extension/Pages/* && touch Extension/Pages/.gitkeep", 8 | "cleanup:win": "(if exist .\\Localization ( rmdir /s/q .\\Localization ) || true) && (if exist .\\Presences ( rmdir /s/q .\\Presences ) || true) && (if exist .\\Extension\\Pages ( rmdir /s/q .\\Extension\\Pages ) || true) && mkdir .\\Extension\\Pages && type nul > .\\Extension\\Pages\\.gitkeep", 9 | "clone": "git clone https://github.com/PreMiD/Presences.git ./Presences && git clone https://github.com/PreMiD/Localization.git ./Localization", 10 | "pu": "node ./presenceUpdater.js", 11 | "pu:compile": "node ./node_modules/typescript/bin/tsc --module CommonJS --target ES2020 --esModuleInterop --removeComments --noUnusedParameters --noUnusedLocals --noFallthroughCasesInSwitch --newLine lf --inlineSourceMap ./presenceUpdater.ts", 12 | "pack:webextension": "node package/updateVersion.js && node package/pack.js", 13 | "pack": "npm run build && npm run pack:webextension", 14 | "build": "npm run cleanup && npm run build:build", 15 | "build:build": "npm run clone && npm run pu:compile && npm run pu && npm run build:webpack && npm run build:localisation", 16 | "build:webpack": "webpack", 17 | "build:localisation": "node locale.js", 18 | "build:win": "npm run cleanup:win && npm run build:build" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.15.5", 22 | "@babel/preset-env": "^7.15.6", 23 | "@types/babel__core": "^7.1.16", 24 | "@types/chrome": "^0.0.158", 25 | "@types/glob": "^7.1.4", 26 | "@types/mongodb": "4.0.7", 27 | "@types/node": "^16.10.2", 28 | "@types/prettier": "^2.4.1", 29 | "@types/semver": "^7.3.8", 30 | "@typescript-eslint/eslint-plugin": "^4.33.0", 31 | "@typescript-eslint/parser": "^4.33.0", 32 | "@vue/cli-plugin-babel": "^4.5.6", 33 | "archiver": "^5.0.2", 34 | "axios": "^0.22.0", 35 | "babel-loader": "^8.1.0", 36 | "chalk": "^4.1.2", 37 | "eslint": "^7.32.0", 38 | "execa": "^4.0.3", 39 | "glob": "^7.2.0", 40 | "jsonschema": "^1.4.0", 41 | "mongodb": "^3.6.9", 42 | "prettier": "^2.4.1", 43 | "semver": "^7.3.5", 44 | "source-map-support": "^0.5.19", 45 | "terser": "^5.9.0", 46 | "terser-webpack-plugin": "^4.2.2", 47 | "typescript": "^4.4.3", 48 | "vue": "^2.6.12", 49 | "vue-loader": "^15.9.3", 50 | "vue-template-compiler": "^2.6.12", 51 | "webpack": "^4.44.2", 52 | "webpack-cli": "^3.3.12" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 192 | 193 | 194 |
195 |
196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /presenceUpdater.ts: -------------------------------------------------------------------------------- 1 | const regExpReplacement = { 2 | 'Amazon': '([a-z0-9-]+[.])*amazon([.][a-z]+)+[/]', 3 | 'eggsy.codes': 'eggsy[.]xyz', 4 | 'IDLIX': '(((tv([0-9]?))?(vip)?[.])?id(f)?lix(official)?[.][a-z]{2,6})', 5 | 'Naver': '((section)[.])?([a-z]+)[.]naver[.]([a-z0-9]+)', 6 | } 7 | 8 | 9 | import "source-map-support/register"; 10 | 11 | import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs"; 12 | import { sync as glob } from "glob"; 13 | import { valid } from "semver"; 14 | import { execSync } from "child_process"; 15 | 16 | let exitCode = 0, 17 | appCode = 0; 18 | 19 | function isValidJSON(text: string): boolean { 20 | try { 21 | JSON.parse(text); 22 | return true; 23 | } catch { 24 | return false; 25 | } 26 | } 27 | 28 | const readFile = (path: string): string => 29 | readFileSync(path, { encoding: "utf8" }), 30 | writeJS = (path: string, code: string): void => 31 | writeFileSync(path, code, { encoding: "utf8", flag: "w" }), 32 | readJson = (jsonPath: string): T => JSON.parse(readFile(jsonPath)) as T, 33 | compile = () => { 34 | copyFileSync('./overwrite/compileChanged.ts', './Presences/tools/auto/compileChanged.ts'); 35 | execSync("npm install", { cwd: './Presences', stdio: 'inherit' }); 36 | execSync("npm run compile", { cwd: './Presences', stdio: 'inherit' }); 37 | }, 38 | main = async (): Promise => { 39 | if (!process.env.GITHUB_ACTIONS) 40 | console.log( 41 | "\nPlease note that this script is ONLY supposed to run on a CI environment" 42 | ); 43 | 44 | console.log("\nFETCHING...\n"); 45 | 46 | const presences: Array<[Metadata, string]> = glob("./Presences/{websites,programs}/*/*/") 47 | .filter((pF) => existsSync(`${pF}/metadata.json`)) 48 | .map((pF) => { 49 | const file = readFile(`${pF}/metadata.json`); 50 | if (isValidJSON(file)) { 51 | const data = JSON.parse(file); 52 | delete data["$schema"]; 53 | return [data, pF]; 54 | } else { 55 | console.error( 56 | `Error. Folder ${pF} does not include a valid metadata file, skipping...` 57 | ); 58 | exitCode = 1; 59 | return null; 60 | } 61 | }), 62 | dbDiff = presences; 63 | 64 | if (dbDiff.length > 0) console.log("\nCOMPILING...\n"); 65 | 66 | compile(); 67 | 68 | const compiledPresences = (await Promise.all( 69 | dbDiff.map(async (file) => { 70 | let metadata = file[0]; 71 | const path = file[1], 72 | metadataFile = readJson(`${path}metadata.json`); 73 | 74 | console.log('Getting', path); 75 | 76 | appCode = 0; 77 | 78 | if (!metadata && !metadataFile) { 79 | console.error( 80 | `Error. No metadata was found for ${path}, skipping...` 81 | ); 82 | appCode = 1; 83 | return null; 84 | } else if (!metadata && metadataFile) metadata = metadataFile; 85 | 86 | if (!path) return null; 87 | 88 | if ( 89 | !metadataFile || 90 | (metadataFile && valid(metadataFile.version) == null) 91 | ) { 92 | const meta = 93 | metadataFile && metadataFile.service 94 | ? metadataFile.service 95 | : metadata && metadata.service 96 | ? metadata.service 97 | : path; 98 | console.error( 99 | `Error. ${meta} does not include a valid metadata file/version, skipping...` 100 | ); 101 | appCode = 1; 102 | return null; 103 | } 104 | 105 | if (!existsSync(`${path}presence.js`)) { 106 | const meta = metadataFile.service ? metadataFile.service : path; 107 | console.error(`Error. ${meta} did not compile, skipping...`); 108 | appCode = 1; 109 | return null; 110 | } 111 | 112 | const resJson: DBdata = { 113 | name: metadata.service, 114 | url: `https://api.premid.app/v2/presences/${encodeURIComponent( 115 | metadata.service 116 | )}/`, 117 | metadata, 118 | presenceJs: readFileSync(`${path}presence.js`, "utf-8") 119 | }; 120 | 121 | if (metadata.iframe && existsSync(`${path}iframe.js`)) 122 | resJson.iframeJs = readFileSync(`${path}iframe.js`, "utf-8"); 123 | else if (metadata.iframe && !existsSync(`${path}iframe.js`)) { 124 | console.error( 125 | `Error. ${metadata.service} explicitly includes iframe but no such file was found, skipping...` 126 | ); 127 | appCode = 1; 128 | return null; 129 | } else if (!metadata.iframe && existsSync(`${path}iframe.js`)) { 130 | console.error( 131 | `Error. ${metadata.service} contains an iframe file but does not include it in the metadata, skipping...` 132 | ); 133 | appCode = 1; 134 | return null; 135 | } 136 | 137 | if (appCode === 1) { 138 | if (exitCode === 0) exitCode = 1; 139 | metadata.service && metadata.service.length > 0 140 | ? console.log(`❌ ${metadata.service}`) 141 | : console.log(`❌ ${path}`); 142 | } 143 | 144 | return resJson; 145 | }) 146 | )).filter((el) => el !== null); 147 | 148 | console.log("\nUPDATING...\n"); 149 | 150 | try { 151 | const metad: any[] = []; 152 | 153 | if(compiledPresences.length < 100) throw `Less than 100 Presences (${compiledPresences.length})`; 154 | 155 | compiledPresences.forEach((el) => { 156 | console.log(`./Extension/Pages/${el.name}/index.js`); 157 | 158 | if (!existsSync(`./Extension/Pages/${el.name}`)) { 159 | mkdirSync(`./Extension/Pages/${el.name}`); 160 | } 161 | 162 | var iframeMode = ''; 163 | if(el.metadata.iframe) iframeMode = 'var checkIframe = true;'; 164 | 165 | writeJS( 166 | `./Extension/Pages/${el.name}/index.js`, 167 | //@ts-ignore 168 | iframeMode+' var serviceNameWrap="'+el.name+'"; var mCategory = "' + el.metadata.category + '"; \n' + el.presenceJs 169 | ); 170 | if (el.iframeJs) writeJS(`./Extension/Pages/${el.name}/iframe.js`, el.iframeJs); 171 | 172 | //@ts-ignore 173 | delete el.metadata.description; 174 | delete el.metadata.version; 175 | 176 | //@ts-ignore 177 | if (el.metadata.regExp) { 178 | //@ts-ignore 179 | const reg = el.metadata.regExp; 180 | if ( 181 | reg.includes('(?=') || 182 | reg.includes('(?!') || 183 | reg.includes('(?<=') || 184 | reg.includes('(? { 209 | console.error(rejection); 210 | process.exit(1); 211 | }); 212 | 213 | process.on("uncaughtException", (err) => { 214 | console.error(err.stack || err); 215 | process.exit(1); 216 | }); 217 | 218 | interface Metadata { 219 | schema: string; 220 | service: string; 221 | version: string; 222 | iframe?: boolean; 223 | } 224 | 225 | interface DBdata { 226 | name: string; 227 | url: string; 228 | metadata: Metadata; 229 | presenceJs: string; 230 | iframeJs?: string; 231 | } 232 | -------------------------------------------------------------------------------- /Extension/Presence.js: -------------------------------------------------------------------------------- 1 | var oldLog = console.log; 2 | console.log = (function () { 3 | return Function.prototype.bind.call( 4 | console.log, 5 | console, 6 | "%cContent", 7 | "background-color: #694ba1; color: white; padding: 2px 10px; border-radius: 3px;" 8 | ); 9 | })(); 10 | 11 | var extensionId = "agnaejlkbiiggajjmnpmeheigkflbnoo"; //Chrome 12 | if (typeof browser !== 'undefined' && typeof chrome !== "undefined") { 13 | extensionId = "{57081fef-67b4-482f-bcb0-69296e63ec4f}"; //Firefox 14 | } 15 | 16 | var activePresence = null; 17 | 18 | const MIN_SLIDE_TIME = 5000; 19 | 20 | var reRegistered = 0; 21 | 22 | class Presence { 23 | constructor(presenceOptions) { 24 | reRegistered++; 25 | this._events = []; 26 | this.playback = true; 27 | this.internalPresence = {}; 28 | this.mode = "active"; 29 | 30 | 31 | console.log('######', presenceOptions, mCategory, reRegistered); 32 | this.clientId = presenceOptions.clientId; 33 | 34 | if(mCategory === 'music') { 35 | this.mode = 'passive'; 36 | } 37 | 38 | if(activePresence) { 39 | this._events = activePresence._events; 40 | this.internalPresence = activePresence.internalPresence; 41 | } 42 | 43 | activePresence = this; 44 | 45 | if(50 > reRegistered) this.register(); 46 | } 47 | 48 | register() { 49 | chrome.runtime.sendMessage(extensionId, { mode: this.mode }, function (response) { 50 | console.log('Presence registred', response) 51 | }); 52 | } 53 | 54 | on(eventName/*: "UpdateData" | "iFrameData"*/, callback) { 55 | this._events[eventName] = callback; 56 | } 57 | 58 | callEvent() { 59 | this._events["UpdateData"](); 60 | } 61 | 62 | callIframeEvent(data) { 63 | console.log('Iframe Data', data); 64 | this._events["iFrameData"](data); 65 | } 66 | 67 | setActivity(presenceData/*: presenceData = {}*/, playback/*: boolean = true*/) { 68 | if (presenceData instanceof Slideshow) presenceData = presenceData.currentSlide; 69 | if(presenceData && Object.keys(presenceData).length) { 70 | presenceData.largeImageText = serviceNameWrap; 71 | } 72 | console.log('presence', presenceData); 73 | this.internalPresence = presenceData; 74 | this.playback = playback; 75 | } 76 | 77 | getActivity() { 78 | return this.internalPresence; 79 | } 80 | 81 | getPresence(focus) { 82 | var activity = this.getActivity(); 83 | console.log("activity", activity); 84 | if (!activity || !Object.keys(activity).length) return {}; 85 | if ( 86 | !focus && 87 | this.mode === "passive" && 88 | !this.playback && 89 | !activity.smallImageKey.includes('play') 90 | ) 91 | return {}; 92 | return { 93 | clientId: this.clientId, 94 | presence: this.getActivity(), 95 | }; 96 | } 97 | 98 | clearActivity() { 99 | this.internalPresence = {}; 100 | } 101 | 102 | getStrings(strings, languageKey = 'en') { 103 | return new Promise(resolve => { 104 | chrome.runtime.sendMessage({ type: "getStrings", data: strings}, (response) => { 105 | console.log('Language', response); 106 | resolve(response); 107 | }); 108 | }); 109 | } 110 | 111 | getSetting(key) { 112 | return new Promise(resolve => { 113 | chrome.runtime.sendMessage( 114 | { 115 | type: "serviceSettings", 116 | data: { service: serviceNameWrap, mode: "get", key: key }, 117 | }, 118 | (res) => { 119 | console.log(key, res) 120 | resolve(res); 121 | } 122 | ); 123 | }) 124 | } 125 | 126 | hideSetting(key) { 127 | chrome.runtime.sendMessage({ 128 | type: "serviceSettings", 129 | data: { service: serviceNameWrap, mode: "hidden", key: key, value: true }, 130 | }); 131 | } 132 | 133 | showSetting(key) { 134 | chrome.runtime.sendMessage({ 135 | type: "serviceSettings", 136 | data: { service: serviceNameWrap, mode: "hidden", key: key, value: false }, 137 | }); 138 | } 139 | 140 | getPageletiable(letiable) { 141 | return new Promise(resolve => { 142 | let script = document.createElement("script"), 143 | _listener = (data) => { 144 | script.remove(); 145 | resolve(JSON.parse(data.detail)); 146 | 147 | window.removeEventListener("PreWrap_Pageletiable", _listener, true); 148 | }; 149 | 150 | window.addEventListener("PreWrap_Pageletiable", _listener); 151 | 152 | script.id = "PreWrap_Pageletiables"; 153 | script.appendChild( 154 | document.createTextNode(` 155 | var pmdPL = new CustomEvent( 156 | "PreWrap_Pageletiable", 157 | { 158 | detail: (typeof window["${letiable}"] === "string") 159 | ? window["${letiable}"] 160 | : JSON.stringify(window["${letiable}"]) 161 | } 162 | ); 163 | window.dispatchEvent(pmdPL); 164 | `) 165 | ); 166 | 167 | (document.body || document.head || document.documentElement).appendChild( 168 | script 169 | ); 170 | }); 171 | } 172 | 173 | setTrayTitle(trayTitle) {} 174 | 175 | getExtensionVersion(numeric = true) { 176 | const version = chrome.runtime.getManifest().version; 177 | if (onlyNumeric) return parseInt(version.replace(/\D/g, "")); 178 | return version; 179 | } 180 | 181 | async getLogs(regExp = false) { 182 | let logs = (await this.getPageletiable("console")).logs; 183 | if (regExp) { 184 | logs = logs.filter( 185 | log => typeof log === "string" && new RegExp(regExp).test(log) 186 | ); 187 | } 188 | if (logs == undefined) logs = []; 189 | return logs; 190 | } 191 | 192 | info(text) { 193 | console.log( 194 | `[PRESENCE] [INFO] ${text}`, 195 | ); 196 | } 197 | 198 | success(text) { 199 | console.log( 200 | `[PRESENCE] [SUCCESS] ${text}`, 201 | ); 202 | } 203 | 204 | error(text) { 205 | console.error( 206 | `[PRESENCE] [ERROR] ${text}`, 207 | ); 208 | } 209 | 210 | getTimestampsfromMedia(element) { 211 | return this.getTimestamps(element.currentTime, element.duration); 212 | } 213 | 214 | getTimestamps(elementTime, elementDuration) { 215 | const startTime = Date.now(); 216 | const endTime = Math.floor(startTime / 1000) - elementTime + elementDuration; 217 | return [Math.floor(startTime / 1000), endTime]; 218 | } 219 | 220 | timestampFromFormat(format) { 221 | return format 222 | .split(":") 223 | .map(time => { 224 | return parseInt(time); 225 | }) 226 | .reduce((prev, time) => 60 * prev + time); 227 | } 228 | 229 | createSlideshow() { 230 | return new Slideshow; 231 | } 232 | } 233 | 234 | class SlideshowSlide { 235 | constructor(id, data, interval) { 236 | this.id = id; 237 | this.data = data; 238 | this.interval = interval; 239 | } 240 | 241 | get interval() { 242 | return this._interval; 243 | } 244 | 245 | set interval(interval) { 246 | if (interval <= MIN_SLIDE_TIME) { 247 | interval = MIN_SLIDE_TIME; 248 | } 249 | this._interval = interval; 250 | } 251 | 252 | updateData(data = null) { 253 | this.data = data || this.data; 254 | } 255 | 256 | updateInterval(interval = null) { 257 | this.interval = interval || this.interval; 258 | } 259 | } 260 | 261 | class Slideshow { 262 | constructor() { 263 | this.index = 0; 264 | this.slides = []; 265 | this.currentSlide = {}; 266 | this.pollSlide(); 267 | } 268 | 269 | pollSlide() { 270 | if (this.index > this.slides.length - 1) this.index = 0; 271 | if (this.slides.length !== 0) { 272 | const slide = this.slides[this.index]; 273 | this.currentSlide = slide.data; 274 | this.index++; 275 | setTimeout(() => { 276 | this.pollSlide(); 277 | }, slide.interval); 278 | activePresence.register(); 279 | } else { 280 | this.currentSlide = {}; 281 | setTimeout(() => { 282 | this.pollSlide(); 283 | }, MIN_SLIDE_TIME); 284 | } 285 | } 286 | 287 | addSlide(id, data, interval) { 288 | if (this.hasSlide(id)) return this.updateSlide(id, data, interval); 289 | const slide = new SlideshowSlide(id, data, interval); 290 | this.slides.push(slide); 291 | return slide; 292 | } 293 | 294 | deleteSlide(id) { 295 | this.slides = this.slides.filter(slide => slide.id !== id); 296 | } 297 | 298 | deleteAllSlides() { 299 | this.slides = []; 300 | this.currentSlide = {}; 301 | } 302 | 303 | updateSlide(id, data = null, interval = null) { 304 | for (const slide of this.slides) { 305 | if (slide.id === id) { 306 | slide.updateData(data); 307 | slide.updateInterval(interval); 308 | return slide; 309 | } 310 | } 311 | } 312 | 313 | hasSlide(id) { 314 | return this.slides.some(slide => slide.id === id); 315 | } 316 | 317 | getSlides() { 318 | return this.slides; 319 | } 320 | } 321 | 322 | chrome.runtime.onMessage.addListener(function (info, sender, sendResponse) { 323 | if (info.type === 'presence') { 324 | try{ 325 | console.log("Presence requested", info); 326 | activePresence.callEvent(); 327 | setTimeout(() => { 328 | try { 329 | sendResponse(activePresence.getPresence(info.data.active)); 330 | } catch (e) { 331 | console.error("Activity error", e); 332 | sendResponse({}); 333 | } 334 | }, 500); 335 | } catch(e) { 336 | console.error('Presence error', e); 337 | sendResponse({}); 338 | } 339 | return true; 340 | } else if (info.type === "iframeDataEvent") { 341 | activePresence.callIframeEvent(info.data); 342 | activePresence.callEvent(); 343 | } 344 | }); 345 | 346 | setTimeout(() => { 347 | setInterval(() => { 348 | checkForIframes(); 349 | }, 3 * 60 * 1000); 350 | checkForIframes(); 351 | }, 10000); 352 | 353 | function checkForIframes() { 354 | if (typeof checkIframe !== 'undefined' && checkIframe) { 355 | frames = document.getElementsByTagName("iframe"); 356 | console.log('Frames found', frames.length); 357 | var urlsD = []; 358 | for (i = 0; i < frames.length; ++i) { 359 | let frame = frames[i]; 360 | if (frame.src) { 361 | urlsD.push(frame.src); 362 | } 363 | } 364 | console.log("domains", urlsD); 365 | chrome.runtime.sendMessage({ 366 | type: "iframeDomains", 367 | data: { 368 | domains: urlsD, 369 | }, 370 | }); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /Extension/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessageExternal.addListener(function (request, sender, sendResponse) { 2 | if (request.action == "presence") { 3 | chrome.tabs.sendMessage(request.tab, {type: 'presence', data: request.info}, function (response) { 4 | sendResponse(response); 5 | }); 6 | } 7 | return true; 8 | }); 9 | 10 | async function initWebNavigationListener() { 11 | conent(); 12 | ifr(); 13 | async function conent() { 14 | chrome.webNavigation.onCompleted.removeListener(navigationListener); 15 | var config = await getFilter(); 16 | console.log('Add navigation listener', config); 17 | if (!config.length) return; 18 | chrome.webNavigation.onCompleted.addListener(navigationListener, { url: config }); 19 | } 20 | 21 | async function ifr() { 22 | chrome.webNavigation.onCompleted.removeListener(iframeNavigationListener); 23 | var config = await getIframeFilter(); 24 | console.log("Add Iframe navigation listener", config); 25 | if (!config.length) return; 26 | chrome.webNavigation.onCompleted.addListener(iframeNavigationListener, { url: config }); 27 | } 28 | 29 | } 30 | 31 | var originCache = []; 32 | var activePages = []; 33 | chrome.storage.sync.get('activePages', (res) => { 34 | if (res.activePages && Object.values(res.activePages).length) { 35 | activePages = res.activePages; 36 | } 37 | initWebNavigationListener(); 38 | }); 39 | 40 | chrome.storage.onChanged.addListener(function (changes, namespace) { 41 | for (var key in changes) { 42 | var storageChange = changes[key]; 43 | console.log( 44 | 'Storage key "%s" in namespace "%s" changed. ' + 45 | 'Old value was "%s", new value is "%s".', 46 | key, 47 | namespace, 48 | storageChange.oldValue, 49 | storageChange.newValue 50 | ); 51 | if (namespace === 'sync' && (key === 'activePages')) { 52 | originCache = []; 53 | activePages = storageChange.newValue; 54 | } 55 | if (namespace === 'sync' && (key === 'activePages' || key === 'allowedIframes')) { 56 | initWebNavigationListener(); 57 | } 58 | if (key === "missingPermissions" || key === "missingIframes") { 59 | setBadge(); 60 | } 61 | } 62 | }); 63 | 64 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 65 | switch (request.type) { 66 | case "iframeDomains": 67 | checkIframeDomains(request.data.domains); 68 | sendResponse(); 69 | break; 70 | case "iframeData": 71 | chrome.tabs.sendMessage(sender.tab.id, { 72 | type: "iframeDataEvent", 73 | data: request.data, 74 | sender, 75 | }); 76 | break; 77 | case "requestPermissions": 78 | chrome.permissions.request(request.data.permissions, (accepted) => { 79 | console.log("Permissions accepted", accepted); 80 | if (accepted) { 81 | if (request.data.sync) chrome.storage.sync.set(request.data.sync); 82 | if (request.data.local) chrome.storage.local.set(request.data.local); 83 | if (request.data.permissions.origins) reloadTabsByOrigin(request.data.permissions.origins); 84 | } 85 | }); 86 | break; 87 | case "serviceSettings": 88 | serviceSettings(request.data).then(res => sendResponse(res)); 89 | return true; 90 | case "getStrings": 91 | const strings = request.data; 92 | for (var key in request.data) { 93 | strings[key] = language[strings[key]]; 94 | } 95 | sendResponse(strings); 96 | break; 97 | default: 98 | console.log(request); 99 | throw 'Unknown request' 100 | } 101 | }); 102 | 103 | function reloadTabsByOrigin(origins) { 104 | chrome.tabs.query({ url: origins.map(el => el+'*')}, (tabs) => { 105 | console.log('Reload tabs', tabs); 106 | tabs.forEach(tab => { 107 | chrome.tabs.reload(tab.id); 108 | }) 109 | }); 110 | } 111 | 112 | function getFilter() { 113 | return new Promise((resolve) => { 114 | if (activePages && activePages.length) { 115 | console.log('Active Pages', activePages); 116 | var filter = []; 117 | activePages.forEach(page => { 118 | const found = pages.find(p => p.service === page); 119 | if (found) { 120 | if (found.regExp) filter.push({urlMatches: found.regExp}); 121 | if (found.regExp && found.regExp.includes('[.]html')) filter.push({ originAndPathMatches: found.regExp }); 122 | if (Array.isArray(found.url)) { 123 | found.url.forEach(el => { 124 | filter.push({hostEquals: el}); 125 | }); 126 | 127 | } else { 128 | filter.push({hostEquals: found.url}); 129 | } 130 | } 131 | 132 | }) 133 | resolve(filter); 134 | return; 135 | } 136 | resolve([]); 137 | }); 138 | } 139 | 140 | function getIframeFilter() { 141 | return new Promise((resolve) => { 142 | chrome.storage.sync.get('allowedIframes', (res) => { 143 | if (res.allowedIframes && Object.values(res.allowedIframes).length) { 144 | console.log('AllowedIframes', res.allowedIframes); 145 | var filter = []; 146 | res.allowedIframes.forEach(ifr => { 147 | filter.push({ hostEquals: ifr.replace(/(^https?:|\/)/gi, '') }); 148 | }) 149 | resolve(filter); 150 | return; 151 | } 152 | resolve([]); 153 | }) 154 | }); 155 | } 156 | 157 | function iframeNavigationListener(data) { 158 | console.log('####Iframe####', data); 159 | chrome.tabs.get(data.tabId, async (tab) => { 160 | const iframeOrigin = new URL(tab.url).origin + "/"; 161 | console.log("Iframe root origin", iframeOrigin); 162 | const page = findPageWithOrigin(iframeOrigin); 163 | if (!page) { 164 | console.error('Iframe No Page found for', iframeOrigin); 165 | return 166 | } 167 | console.log("Inject Iframe", page.service); 168 | if (!data.frameId) { 169 | console.log('Do not inject iframe on root page'); 170 | return; 171 | } 172 | await loadScriptPromise( 173 | data.tabId, 174 | data.frameId, 175 | "Iframe.js" 176 | ); 177 | await loadScriptPromise( 178 | data.tabId, 179 | data.frameId, 180 | "Pages/" + page.service + "/iframe.js" 181 | ); 182 | }) 183 | } 184 | 185 | function navigationListener(data) { 186 | console.log('####Content####', data); 187 | const permConfig = { origins: [new URL(data.url).origin+'/'] }; 188 | chrome.permissions.contains(permConfig, async perm => { 189 | console.log('Permission', perm); 190 | if (!perm) { 191 | 192 | console.error("No Permission", permConfig); 193 | saveMissingPermission(permConfig.origins[0]); 194 | return; 195 | } 196 | 197 | var page = findPageWithOrigin(permConfig.origins[0]); 198 | if (!page) { 199 | page = findPageWithOrigin(data.url); 200 | } 201 | 202 | if (!page) { 203 | console.error('No Page found for', permConfig.origins[0]); 204 | return 205 | } 206 | console.log("Inject", page.service); 207 | 208 | if (data.frameId) { 209 | console.log('Do not inject root page script in iframes'); 210 | return; 211 | } 212 | 213 | await loadScriptPromise( 214 | data.tabId, 215 | data.frameId, 216 | "Presence.js" 217 | ); 218 | await loadScriptPromise( 219 | data.tabId, 220 | data.frameId, 221 | "Pages/" + page.service + "/index.js" 222 | ); 223 | 224 | if (page.readLogs) { 225 | console.log('Inject log reader'); 226 | await loadScriptPromise( 227 | data.tabId, 228 | data.frameId, 229 | "logReader.js" 230 | ); 231 | } 232 | }); 233 | } 234 | 235 | function saveMissingPermission(origin) { 236 | chrome.storage.local.get("missingPermissions", (res) => { 237 | var cur = []; 238 | if (res.missingPermissions && Object.values(res.missingPermissions).length) { 239 | cur = res.missingPermissions; 240 | } 241 | if (cur.find((el) => el === origin)) return; 242 | cur.push(origin); 243 | chrome.storage.local.set({"missingPermissions": cur}); 244 | }); 245 | } 246 | 247 | chrome.browserAction.onClicked.addListener(tab => { 248 | console.log('click'); 249 | }); 250 | 251 | function findPageWithOrigin(origin) { 252 | if (originCache[origin]) return origin; 253 | return pages.filter(page => activePages.includes(page.service)).find(page => checkIfDomain(page, origin)); 254 | } 255 | 256 | function checkIfDomain(meta, url) { 257 | if (typeof meta.regExp !== "undefined") { 258 | res = url.match(new RegExp(meta.regExp)); 259 | 260 | if (res === null) return false; 261 | return res.length > 0; 262 | } 263 | 264 | if (Array.isArray(meta.url)) { 265 | res = meta.url.filter(mUrl => new URL(url).hostname === mUrl).length > 0; 266 | } else { 267 | res = new URL(url).hostname === meta.url; 268 | } 269 | return res; 270 | } 271 | 272 | function checkIframeDomains(domains) { 273 | console.log(domains); 274 | if(!domains.length) return; 275 | chrome.storage.sync.get(["allowedIframes", "missingIframes", "blockedIframes"], (res) => { 276 | var allowed = []; 277 | var missing = []; 278 | var blocked = []; 279 | if (res.allowedIframes && Object.values(res.allowedIframes).length) { 280 | allowed = res.allowedIframes; 281 | } 282 | 283 | if (res.missingIframes && Object.values(res.missingIframes).length) { 284 | missing = res.missingIframes; 285 | } 286 | 287 | if (res.blockedIframes && Object.values(res.blockedIframes).length) { 288 | blocked = res.blockedIframes; 289 | } 290 | 291 | console.log(allowed, missing, blocked); 292 | 293 | domains.forEach(domain => { 294 | if (!domain) return; 295 | const origin = new URL(domain).origin + "/"; 296 | if (allowed.includes(origin)) return; 297 | if (missing.includes(origin)) return; 298 | if (blocked.includes(origin)) return; 299 | missing.push(origin); 300 | }) 301 | 302 | chrome.storage.sync.set({ missingIframes: missing }); 303 | }); 304 | } 305 | 306 | function serviceSettings(options) { 307 | return new Promise((resolve, reject) => { 308 | var serId = options.service; 309 | var meta = pages.filter((p) => p.service === serId); 310 | if (!meta) { 311 | resolve(); 312 | return; 313 | } 314 | meta = meta[0]; 315 | const sKey = "settings_" + serId 316 | chrome.storage.local.get(sKey, (res) => { 317 | if (res[sKey] && Object.values(res[sKey]).length) { 318 | meta.settings = meta.settings.map( el => { 319 | var e = res[sKey].find(set => set.id === el.id); 320 | if (e) return e; 321 | return el; 322 | }); 323 | } 324 | 325 | if (!meta.settings) { 326 | resolve(); 327 | return; 328 | }; 329 | 330 | switch (options.mode) { 331 | case "all": 332 | resolve(meta.settings); 333 | break; 334 | case "get": 335 | var r = meta.settings.find(s => s.id === options.key); 336 | if (r) { 337 | resolve(r.value); 338 | return; 339 | } 340 | resolve(); 341 | break; 342 | case "set": 343 | var r = meta.settings.find(s => s.id === options.key); 344 | if (r) { 345 | r.value = options.value; 346 | var set = {}; 347 | set[sKey] = meta.settings; 348 | chrome.storage.local.set(JSON.parse(JSON.stringify(set))); 349 | } 350 | resolve(); 351 | break; 352 | case "hidden": 353 | var r = meta.settings.find(s => s.id === options.key); 354 | if (r) { 355 | r.hidden = options.value; 356 | var set = {}; 357 | set[sKey] = meta.settings; 358 | chrome.storage.local.set(JSON.parse(JSON.stringify(set))); 359 | } 360 | resolve(); 361 | break; 362 | default: 363 | throw "Unknown mode"; 364 | } 365 | }); 366 | }) 367 | } 368 | 369 | function loadScriptPromise(tabId, frameId, file) { 370 | return new Promise((resolve, reject) => { 371 | chrome.tabs.executeScript( 372 | tabId, 373 | { 374 | file, 375 | frameId, 376 | }, 377 | (res) => { 378 | resolve(res); 379 | } 380 | ); 381 | }); 382 | } 383 | 384 | checkForMissingPermissions(); 385 | function checkForMissingPermissions() { 386 | iframePermCheck(); 387 | cleanUpPermissions(); 388 | function iframePermCheck() { 389 | chrome.storage.sync.get(["allowedIframes", "missingIframes"], (res) => { 390 | var allowed = []; 391 | var missing = []; 392 | if (res.allowedIframes && Object.values(res.allowedIframes).length) { 393 | allowed = res.allowedIframes; 394 | } 395 | if (res.missingIframes && Object.values(res.missingIframes).length) { 396 | missing = res.missingIframes; 397 | } 398 | 399 | Promise.all(allowed.map(al => { 400 | return new Promise((resolve) => { 401 | chrome.permissions.contains({ origins: [al] }, (perm) => { 402 | if (!perm && !missing.includes(al)) { 403 | missing.push(al); 404 | resolve(); 405 | } 406 | }); 407 | }) 408 | })).then(() => { 409 | chrome.storage.sync.set({ missingIframes: missing }); 410 | }) 411 | }); 412 | } 413 | function cleanUpPermissions() { 414 | chrome.storage.sync.get(["allowedIframes"], (res) => { 415 | var allowedIframes = []; 416 | if (res.allowedIframes && Object.values(res.allowedIframes).length) { 417 | allowedIframes = res.allowedIframes; 418 | } 419 | chrome.permissions.getAll((perms) => { 420 | console.log('Active Permissions', perms); 421 | if (perms.origins && perms.origins.length) { 422 | const removePerm = perms.origins.filter((origin) => { 423 | if (allowedIframes.includes(origin) || allowedIframes.includes(origin.replace('*', ''))) return false; 424 | if (findPageWithOrigin(origin)) return false; 425 | if (/^https?:\/\/\d+/.test(origin)) return false; 426 | return true; 427 | }); 428 | if (removePerm) { 429 | console.log('Unneeded permissions', removePerm); 430 | chrome.permissions.remove({origins: removePerm}); 431 | } 432 | } 433 | }); 434 | }); 435 | } 436 | } 437 | 438 | setBadge(); 439 | function setBadge() { 440 | let miss = false; 441 | chrome.storage.sync.get("missingIframes", (obj) => { 442 | if (obj.missingIframes && obj.missingIframes.length) miss = true; 443 | chrome.storage.local.get("missingPermissions", (obj) => { 444 | if (obj.missingPermissions && obj.missingPermissions.length) miss = true; 445 | if (miss){ 446 | chrome.browserAction.setBadgeText({ text: "❌" }); 447 | } else { 448 | chrome.browserAction.setBadgeText({ text: "" }); 449 | } 450 | }); 451 | }); 452 | } 453 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 148 | 149 | 450 | 451 | --------------------------------------------------------------------------------