├── .gitignore ├── CHANGELOG.md ├── Chrome ├── chrome │ ├── icon128.png │ ├── icon48.png │ ├── requests.js │ └── script.js ├── manifest.json └── template__ │ └── manifest.json ├── Firefox ├── bootstrap.js ├── defaults │ └── preferences │ │ └── prefs.js ├── harness-options.json ├── icon.png ├── install.rdf ├── locales.json ├── resources │ └── bttv4ffz │ │ └── lib │ │ └── main.js └── template__ │ ├── harness-options.json │ └── install.rdf ├── README.md ├── Safari └── BTTV4FFZ.safariextension │ ├── Icon.png │ ├── Info.plist │ ├── Settings.plist │ └── script.js ├── Userscript └── bttv4ffz_injector.user.js ├── inject.js └── pack.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Archives 2 | Firefox -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.7.3: 2 | - Bring back an actual Firefox Plugin 3 | - Ditching the GreaseMonkey plugin - no longer needed 4 | - Support for Pale Moon added 5 | 6 | ## 1.0.7.2: 7 | - Ditching Firefox, since they don't want to allow me the remote-script-loading 8 | - Adding GreaseMonkey support 9 | The file can be installed from [here](https://cdn.lordmau5.com/bttv4ffz.user.js). 10 | 11 | ## 1.0.7.1: 12 | - Initial release of remote-script-loading from my CDN :) 13 | -------------------------------------------------------------------------------- /Chrome/chrome/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lordmau5/BTTV4FFZ/3e877de4734524861c44cae7905fe768751d344c/Chrome/chrome/icon128.png -------------------------------------------------------------------------------- /Chrome/chrome/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lordmau5/BTTV4FFZ/3e877de4734524861c44cae7905fe768751d344c/Chrome/chrome/icon48.png -------------------------------------------------------------------------------- /Chrome/chrome/requests.js: -------------------------------------------------------------------------------- 1 | chrome.webRequest.onHeadersReceived.addListener(function(info) { 2 | return { 3 | responseHeaders: info.responseHeaders.concat([{ 4 | name: 'access-control-allow-origin', 5 | value: '*' 6 | }]) 7 | }; 8 | }, { 9 | urls: [ 10 | "*://cdn.betterttv.net/*" 11 | ] 12 | }, ["blocking", "responseHeaders"]); 13 | -------------------------------------------------------------------------------- /Chrome/chrome/script.js: -------------------------------------------------------------------------------- 1 | function inject() { 2 | var s = document.createElement('script'); 3 | s.src = "https://cdn.lordmau5.com/inject.js"; 4 | s.onload = function() { 5 | this.parentNode.removeChild(this); 6 | }; 7 | (document.head || document.documentElement).appendChild(s); 8 | } 9 | 10 | inject(); 11 | -------------------------------------------------------------------------------- /Chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "BetterTTV Emotes for FrankerFaceZ", 4 | "short_name": "BTTV4FFZ", 5 | "version": "1.1.2", 6 | "description": "Add the BTTV emotes on-top of FFZ!", 7 | 8 | "icons": { 9 | "48": "chrome/icon48.png", 10 | "128": "chrome/icon128.png" 11 | }, 12 | 13 | "background": { 14 | "scripts": ["chrome/requests.js"] 15 | }, 16 | 17 | "content_scripts": [ { 18 | "all_frames": true, 19 | "js": [ "chrome/script.js" ], 20 | "matches": [ "*://*.twitch.tv/*" ], 21 | "exclude_globs": [ "*://api.twitch.tv/*" ] 22 | } ], 23 | 24 | "permissions": [ 25 | "*://*.twitch.tv/*", 26 | "*://*.frankerfacez.com/*", 27 | "*://*.betterttv.net/*", 28 | "webRequest", 29 | "webRequestBlocking" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /Chrome/template__/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "BetterTTV Emotes for FrankerFaceZ", 4 | "short_name": "BTTV4FFZ", 5 | "version": "%%VERSION%%", 6 | "description": "Add the BTTV emotes on-top of FFZ!", 7 | 8 | "icons": { 9 | "48": "chrome/icon48.png", 10 | "128": "chrome/icon128.png" 11 | }, 12 | 13 | "background": { 14 | "scripts": ["chrome/requests.js"] 15 | }, 16 | 17 | "content_scripts": [ { 18 | "all_frames": true, 19 | "js": [ "chrome/script.js" ], 20 | "matches": [ "*://*.twitch.tv/*" ], 21 | "exclude_globs": [ "*://api.twitch.tv/*" ] 22 | } ], 23 | 24 | "permissions": [ 25 | "*://*.twitch.tv/*", 26 | "*://*.frankerfacez.com/*", 27 | "*://*.betterttv.net/*", 28 | "webRequest", 29 | "webRequestBlocking" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /Firefox/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // @see http://mxr.mozilla.org/mozilla-central/source/js/src/xpconnect/loader/mozJSComponentLoader.cpp 6 | 7 | 'use strict'; 8 | 9 | // IMPORTANT: Avoid adding any initialization tasks here, if you need to do 10 | // something before add-on is loaded consider addon/runner module instead! 11 | 12 | const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu, 13 | results: Cr, manager: Cm } = Components; 14 | const ioService = Cc['@mozilla.org/network/io-service;1']. 15 | getService(Ci.nsIIOService); 16 | const resourceHandler = ioService.getProtocolHandler('resource'). 17 | QueryInterface(Ci.nsIResProtocolHandler); 18 | const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')(); 19 | const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1']. 20 | getService(Ci.mozIJSSubScriptLoader); 21 | const prefService = Cc['@mozilla.org/preferences-service;1']. 22 | getService(Ci.nsIPrefService). 23 | QueryInterface(Ci.nsIPrefBranch); 24 | const appInfo = Cc["@mozilla.org/xre/app-info;1"]. 25 | getService(Ci.nsIXULAppInfo); 26 | const vc = Cc["@mozilla.org/xpcom/version-comparator;1"]. 27 | getService(Ci.nsIVersionComparator); 28 | 29 | 30 | const REASON = [ 'unknown', 'startup', 'shutdown', 'enable', 'disable', 31 | 'install', 'uninstall', 'upgrade', 'downgrade' ]; 32 | 33 | const bind = Function.call.bind(Function.bind); 34 | 35 | let loader = null; 36 | let unload = null; 37 | let cuddlefishSandbox = null; 38 | let nukeTimer = null; 39 | 40 | let resourceDomains = []; 41 | function setResourceSubstitution(domain, uri) { 42 | resourceDomains.push(domain); 43 | resourceHandler.setSubstitution(domain, uri); 44 | } 45 | 46 | // Utility function that synchronously reads local resource from the given 47 | // `uri` and returns content string. 48 | function readURI(uri) { 49 | let ioservice = Cc['@mozilla.org/network/io-service;1']. 50 | getService(Ci.nsIIOService); 51 | let channel = ioservice.newChannel(uri, 'UTF-8', null); 52 | let stream = channel.open(); 53 | 54 | let cstream = Cc['@mozilla.org/intl/converter-input-stream;1']. 55 | createInstance(Ci.nsIConverterInputStream); 56 | cstream.init(stream, 'UTF-8', 0, 0); 57 | 58 | let str = {}; 59 | let data = ''; 60 | let read = 0; 61 | do { 62 | read = cstream.readString(0xffffffff, str); 63 | data += str.value; 64 | } while (read != 0); 65 | 66 | cstream.close(); 67 | 68 | return data; 69 | } 70 | 71 | // We don't do anything on install & uninstall yet, but in a future 72 | // we should allow add-ons to cleanup after uninstall. 73 | function install(data, reason) {} 74 | function uninstall(data, reason) {} 75 | 76 | function startup(data, reasonCode) { 77 | try { 78 | let reason = REASON[reasonCode]; 79 | // URI for the root of the XPI file. 80 | // 'jar:' URI if the addon is packed, 'file:' URI otherwise. 81 | // (Used by l10n module in order to fetch `locale` folder) 82 | let rootURI = data.resourceURI.spec; 83 | 84 | // TODO: Maybe we should perform read harness-options.json asynchronously, 85 | // since we can't do anything until 'sessionstore-windows-restored' anyway. 86 | let options = JSON.parse(readURI(rootURI + './harness-options.json')); 87 | 88 | let id = options.jetpackID; 89 | let name = options.name; 90 | 91 | // Clean the metadata 92 | options.metadata[name]['permissions'] = options.metadata[name]['permissions'] || {}; 93 | 94 | // freeze the permissionss 95 | Object.freeze(options.metadata[name]['permissions']); 96 | // freeze the metadata 97 | Object.freeze(options.metadata[name]); 98 | 99 | // Register a new resource 'domain' for this addon which is mapping to 100 | // XPI's `resources` folder. 101 | // Generate the domain name by using jetpack ID, which is the extension ID 102 | // by stripping common characters that doesn't work as a domain name: 103 | let uuidRe = 104 | /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/; 105 | 106 | let domain = id. 107 | toLowerCase(). 108 | replace(/@/g, '-at-'). 109 | replace(/\./g, '-dot-'). 110 | replace(uuidRe, '$1'); 111 | 112 | let prefixURI = 'resource://' + domain + '/'; 113 | let resourcesURI = ioService.newURI(rootURI + '/resources/', null, null); 114 | setResourceSubstitution(domain, resourcesURI); 115 | 116 | // Create path to URLs mapping supported by loader. 117 | let paths = { 118 | // Relative modules resolve to add-on package lib 119 | './': prefixURI + name + '/lib/', 120 | './tests/': prefixURI + name + '/tests/', 121 | '': 'resource://gre/modules/commonjs/' 122 | }; 123 | 124 | // Maps addon lib and tests ressource folders for each package 125 | paths = Object.keys(options.metadata).reduce(function(result, name) { 126 | result[name + '/'] = prefixURI + name + '/lib/' 127 | result[name + '/tests/'] = prefixURI + name + '/tests/' 128 | return result; 129 | }, paths); 130 | 131 | // We need to map tests folder when we run sdk tests whose package name 132 | // is stripped 133 | if (name == 'addon-sdk') 134 | paths['tests/'] = prefixURI + name + '/tests/'; 135 | 136 | let useBundledSDK = options['force-use-bundled-sdk']; 137 | if (!useBundledSDK) { 138 | try { 139 | useBundledSDK = prefService.getBoolPref("extensions.addon-sdk.useBundledSDK"); 140 | } 141 | catch (e) { 142 | // Pref doesn't exist, allow using Firefox shipped SDK 143 | } 144 | } 145 | 146 | // Starting with Firefox 21.0a1, we start using modules shipped into firefox 147 | // Still allow using modules from the xpi if the manifest tell us to do so. 148 | // And only try to look for sdk modules in xpi if the xpi actually ship them 149 | if (options['is-sdk-bundled'] && 150 | (vc.compare(appInfo.version, '21.0a1') < 0 || useBundledSDK)) { 151 | // Maps sdk module folders to their resource folder 152 | paths[''] = prefixURI + 'addon-sdk/lib/'; 153 | // test.js is usually found in root commonjs or SDK_ROOT/lib/ folder, 154 | // so that it isn't shipped in the xpi. Keep a copy of it in sdk/ folder 155 | // until we no longer support SDK modules in XPI: 156 | paths['test'] = prefixURI + 'addon-sdk/lib/sdk/test.js'; 157 | } 158 | 159 | // Retrieve list of module folder overloads based on preferences in order to 160 | // eventually used a local modules instead of files shipped into Firefox. 161 | let branch = prefService.getBranch('extensions.modules.' + id + '.path'); 162 | paths = branch.getChildList('', {}).reduce(function (result, name) { 163 | // Allows overloading of any sub folder by replacing . by / in pref name 164 | let path = name.substr(1).split('.').join('/'); 165 | // Only accept overloading folder by ensuring always ending with `/` 166 | if (path) path += '/'; 167 | let fileURI = branch.getCharPref(name); 168 | 169 | // On mobile, file URI has to end with a `/` otherwise, setSubstitution 170 | // takes the parent folder instead. 171 | if (fileURI[fileURI.length-1] !== '/') 172 | fileURI += '/'; 173 | 174 | // Maps the given file:// URI to a resource:// in order to avoid various 175 | // failure that happens with file:// URI and be close to production env 176 | let resourcesURI = ioService.newURI(fileURI, null, null); 177 | let resName = 'extensions.modules.' + domain + '.commonjs.path' + name; 178 | setResourceSubstitution(resName, resourcesURI); 179 | 180 | result[path] = 'resource://' + resName + '/'; 181 | return result; 182 | }, paths); 183 | 184 | // Make version 2 of the manifest 185 | let manifest = options.manifest; 186 | 187 | // Import `cuddlefish.js` module using a Sandbox and bootstrap loader. 188 | let cuddlefishPath = 'loader/cuddlefish.js'; 189 | let cuddlefishURI = 'resource://gre/modules/commonjs/sdk/' + cuddlefishPath; 190 | if (paths['sdk/']) { // sdk folder has been overloaded 191 | // (from pref, or cuddlefish is still in the xpi) 192 | cuddlefishURI = paths['sdk/'] + cuddlefishPath; 193 | } 194 | else if (paths['']) { // root modules folder has been overloaded 195 | cuddlefishURI = paths[''] + 'sdk/' + cuddlefishPath; 196 | } 197 | 198 | cuddlefishSandbox = loadSandbox(cuddlefishURI); 199 | let cuddlefish = cuddlefishSandbox.exports; 200 | 201 | // Normalize `options.mainPath` so that it looks like one that will come 202 | // in a new version of linker. 203 | let main = options.mainPath; 204 | 205 | unload = cuddlefish.unload; 206 | loader = cuddlefish.Loader({ 207 | paths: paths, 208 | // modules manifest. 209 | manifest: manifest, 210 | 211 | // Add-on ID used by different APIs as a unique identifier. 212 | id: id, 213 | // Add-on name. 214 | name: name, 215 | // Add-on version. 216 | version: options.metadata[name].version, 217 | // Add-on package descriptor. 218 | metadata: options.metadata[name], 219 | // Add-on load reason. 220 | loadReason: reason, 221 | 222 | prefixURI: prefixURI, 223 | // Add-on URI. 224 | rootURI: rootURI, 225 | // options used by system module. 226 | // File to write 'OK' or 'FAIL' (exit code emulation). 227 | resultFile: options.resultFile, 228 | // Arguments passed as --static-args 229 | staticArgs: options.staticArgs, 230 | // Add-on preferences branch name 231 | preferencesBranch: options.preferencesBranch, 232 | 233 | // Arguments related to test runner. 234 | modules: { 235 | '@test/options': { 236 | allTestModules: options.allTestModules, 237 | iterations: options.iterations, 238 | filter: options.filter, 239 | profileMemory: options.profileMemory, 240 | stopOnError: options.stopOnError, 241 | verbose: options.verbose, 242 | parseable: options.parseable, 243 | checkMemory: options.check_memory, 244 | } 245 | } 246 | }); 247 | 248 | let module = cuddlefish.Module('sdk/loader/cuddlefish', cuddlefishURI); 249 | let require = cuddlefish.Require(loader, module); 250 | 251 | require('sdk/addon/runner').startup(reason, { 252 | loader: loader, 253 | main: main, 254 | prefsURI: rootURI + 'defaults/preferences/prefs.js' 255 | }); 256 | } catch (error) { 257 | dump('Bootstrap error: ' + 258 | (error.message ? error.message : String(error)) + '\n' + 259 | (error.stack || error.fileName + ': ' + error.lineNumber) + '\n'); 260 | throw error; 261 | } 262 | }; 263 | 264 | function loadSandbox(uri) { 265 | let proto = { 266 | sandboxPrototype: { 267 | loadSandbox: loadSandbox, 268 | ChromeWorker: ChromeWorker 269 | } 270 | }; 271 | let sandbox = Cu.Sandbox(systemPrincipal, proto); 272 | // Create a fake commonjs environnement just to enable loading loader.js 273 | // correctly 274 | sandbox.exports = {}; 275 | sandbox.module = { uri: uri, exports: sandbox.exports }; 276 | sandbox.require = function (id) { 277 | if (id !== "chrome") 278 | throw new Error("Bootstrap sandbox `require` method isn't implemented."); 279 | 280 | return Object.freeze({ Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm, 281 | CC: bind(CC, Components), components: Components, 282 | ChromeWorker: ChromeWorker }); 283 | }; 284 | scriptLoader.loadSubScript(uri, sandbox, 'UTF-8'); 285 | return sandbox; 286 | } 287 | 288 | function unloadSandbox(sandbox) { 289 | if ("nukeSandbox" in Cu) 290 | Cu.nukeSandbox(sandbox); 291 | } 292 | 293 | function setTimeout(callback, delay) { 294 | let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 295 | timer.initWithCallback({ notify: callback }, delay, 296 | Ci.nsITimer.TYPE_ONE_SHOT); 297 | return timer; 298 | } 299 | 300 | function shutdown(data, reasonCode) { 301 | let reason = REASON[reasonCode]; 302 | if (loader) { 303 | unload(loader, reason); 304 | unload = null; 305 | 306 | // Don't waste time cleaning up if the application is shutting down 307 | if (reason != "shutdown") { 308 | // Avoid leaking all modules when something goes wrong with one particular 309 | // module. Do not clean it up immediatly in order to allow executing some 310 | // actions on addon disabling. 311 | // We need to keep a reference to the timer, otherwise it is collected 312 | // and won't ever fire. 313 | nukeTimer = setTimeout(nukeModules, 1000); 314 | 315 | // Bug 944951 - bootstrap.js must remove the added resource: URIs on unload 316 | resourceDomains.forEach(domain => { 317 | resourceHandler.setSubstitution(domain, null); 318 | }) 319 | } 320 | } 321 | }; 322 | 323 | function nukeModules() { 324 | nukeTimer = null; 325 | // module objects store `exports` which comes from sandboxes 326 | // We should avoid keeping link to these object to avoid leaking sandboxes 327 | for (let key in loader.modules) { 328 | delete loader.modules[key]; 329 | } 330 | // Direct links to sandboxes should be removed too 331 | for (let key in loader.sandboxes) { 332 | let sandbox = loader.sandboxes[key]; 333 | delete loader.sandboxes[key]; 334 | // Bug 775067: From FF17 we can kill all CCW from a given sandbox 335 | unloadSandbox(sandbox); 336 | } 337 | loader = null; 338 | 339 | // both `toolkit/loader` and `system/xul-app` are loaded as JSM's via 340 | // `cuddlefish.js`, and needs to be unloaded to avoid memory leaks, when 341 | // the addon is unload. 342 | 343 | unloadSandbox(cuddlefishSandbox.loaderSandbox); 344 | unloadSandbox(cuddlefishSandbox.xulappSandbox); 345 | 346 | // Bug 764840: We need to unload cuddlefish otherwise it will stay alive 347 | // and keep a reference to this compartment. 348 | unloadSandbox(cuddlefishSandbox); 349 | cuddlefishSandbox = null; 350 | } 351 | -------------------------------------------------------------------------------- /Firefox/defaults/preferences/prefs.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lordmau5/BTTV4FFZ/3e877de4734524861c44cae7905fe768751d344c/Firefox/defaults/preferences/prefs.js -------------------------------------------------------------------------------- /Firefox/harness-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "abort_on_missing": false, 3 | "check_memory": false, 4 | "enable_e10s": false, 5 | "is-sdk-bundled": false, 6 | "jetpackID": "BTTV4FFZ@jetpack", 7 | "loader": "addon-sdk/lib/sdk/loader/cuddlefish.js", 8 | "main": "main", 9 | "mainPath": "bttv4ffz/main", 10 | "manifest": { 11 | "bttv4ffz/main": { 12 | "docsSHA256": null, 13 | "jsSHA256": "58e3acca682b421ca901e5896eeaf7a7d2ec82104faf908afc5a6ead34d34bcb", 14 | "moduleName": "main", 15 | "packageName": "bttv4ffz", 16 | "requirements": { 17 | "sdk/page-mod": "sdk/page-mod" 18 | }, 19 | "sectionName": "lib" 20 | } 21 | }, 22 | "metadata": { 23 | "addon-sdk": { 24 | "description": "Add-on development made easy.", 25 | "keywords": [ 26 | "javascript", 27 | "engine", 28 | "addon", 29 | "extension", 30 | "xulrunner", 31 | "firefox", 32 | "browser" 33 | ], 34 | "license": "MPL 2.0", 35 | "name": "addon-sdk" 36 | }, 37 | "bttv4ffz": { 38 | "author": "Lordmau5", 39 | "description": "Get those BetterTTV emotes in FFZ!", 40 | "main": "main", 41 | "name": "bttv4ffz", 42 | "permissions": { 43 | "private-browsing": true, 44 | "multiprocess": true 45 | }, 46 | "version": "1.1.2" 47 | } 48 | }, 49 | "name": "bttv4ffz", 50 | "parseable": false, 51 | "preferencesBranch": "BTTV4FFZ@jetpack", 52 | "sdkVersion": "1.17", 53 | "staticArgs": {}, 54 | "verbose": false 55 | } 56 | -------------------------------------------------------------------------------- /Firefox/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lordmau5/BTTV4FFZ/3e877de4734524861c44cae7905fe768751d344c/Firefox/icon.png -------------------------------------------------------------------------------- /Firefox/install.rdf: -------------------------------------------------------------------------------- 1 | 4 | 5 | @BTTV4FFZ 6 | 1.1.2 7 | 2 8 | true 9 | false 10 | https://cdn.lordmau5.com/update.rdf 11 | 12 | 13 | 14 | 15 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 16 | 26.0 17 | 39.0 18 | 19 | 20 | 21 | 22 | 23 | 24 | {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} 25 | 25.0 26 | 39.0 27 | 28 | 29 | 30 | 31 | BetterTTV for FrankerFaceZ 32 | Get those BetterTTV emotes in FFZ! 33 | Lordmau5 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Firefox/locales.json: -------------------------------------------------------------------------------- 1 | {"locales": []} 2 | -------------------------------------------------------------------------------- /Firefox/resources/bttv4ffz/lib/main.js: -------------------------------------------------------------------------------- 1 | var pageMod = require("sdk/page-mod"); 2 | 3 | pageMod.PageMod({ 4 | include: "*.twitch.tv", 5 | contentScript: 'var ce = document["createElement"].bind(document);' + 6 | 'var s = ce("script");' + 7 | //'s.src = "//cdn.lordmau5.com/inject.js";' + 8 | 's.src = "//lordmau5.com/nocache/inject.js";' + 9 | 's.onload = function() {' + 10 | 'this.parentNode.removeChild(this);' + 11 | '};' + 12 | '(document.head || document.documentElement).appendChild(s);' 13 | }); 14 | 15 | browser.webRequest.onHeadersReceived.addListener(function(info) { 16 | return { 17 | responseHeaders: info.responseHeaders.concat([{ 18 | name: 'access-control-allow-origin', 19 | value: '*' 20 | }]) 21 | }; 22 | }, { 23 | urls: [ 24 | "*://cdn.betterttv.net/*" 25 | ] 26 | }, ["blocking", "responseHeaders"]); 27 | -------------------------------------------------------------------------------- /Firefox/template__/harness-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "abort_on_missing": false, 3 | "check_memory": false, 4 | "enable_e10s": false, 5 | "is-sdk-bundled": false, 6 | "jetpackID": "BTTV4FFZ@jetpack", 7 | "loader": "addon-sdk/lib/sdk/loader/cuddlefish.js", 8 | "main": "main", 9 | "mainPath": "bttv4ffz/main", 10 | "manifest": { 11 | "bttv4ffz/main": { 12 | "docsSHA256": null, 13 | "jsSHA256": "58e3acca682b421ca901e5896eeaf7a7d2ec82104faf908afc5a6ead34d34bcb", 14 | "moduleName": "main", 15 | "packageName": "bttv4ffz", 16 | "requirements": { 17 | "sdk/page-mod": "sdk/page-mod" 18 | }, 19 | "sectionName": "lib" 20 | } 21 | }, 22 | "metadata": { 23 | "addon-sdk": { 24 | "description": "Add-on development made easy.", 25 | "keywords": [ 26 | "javascript", 27 | "engine", 28 | "addon", 29 | "extension", 30 | "xulrunner", 31 | "firefox", 32 | "browser" 33 | ], 34 | "license": "MPL 2.0", 35 | "name": "addon-sdk" 36 | }, 37 | "bttv4ffz": { 38 | "author": "Lordmau5", 39 | "description": "Get those BetterTTV emotes in FFZ!", 40 | "main": "main", 41 | "name": "bttv4ffz", 42 | "permissions": { 43 | "private-browsing": true, 44 | "multiprocess": true 45 | }, 46 | "version": "%%VERSION%%" 47 | } 48 | }, 49 | "name": "bttv4ffz", 50 | "parseable": false, 51 | "preferencesBranch": "BTTV4FFZ@jetpack", 52 | "sdkVersion": "1.17", 53 | "staticArgs": {}, 54 | "verbose": false 55 | } 56 | -------------------------------------------------------------------------------- /Firefox/template__/install.rdf: -------------------------------------------------------------------------------- 1 | 4 | 5 | @BTTV4FFZ 6 | %%VERSION%% 7 | 2 8 | true 9 | false 10 | https://cdn.lordmau5.com/update.rdf 11 | 12 | 13 | 14 | 15 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 16 | 26.0 17 | 39.0 18 | 19 | 20 | 21 | 22 | 23 | 24 | {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} 25 | 25.0 26 | 39.0 27 | 28 | 29 | 30 | 31 | BetterTTV for FrankerFaceZ 32 | Get those BetterTTV emotes in FFZ! 33 | Lordmau5 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important information! 2 | **BTTV4FFZ is deprecated.** 3 | 4 | **Please move over to the new repository located at:** 5 | 6 | **https://github.com/Lordmau5/The-FFZ-Addon-Pack** 7 | 8 | 9 | ___ 10 | 11 | # BTTV4FFZ 12 | Better Twitch TV emotes for FrankerFaceZ 13 | ___ 14 | 15 | _Ever wanted to have these slick Better Twitch TV emotes but you already have FrankerFaceZ installed?_ 16 | 17 | _Don't want to install Better Twitch TV because you prefer the design of FrankerFaceZ?_ 18 | 19 | _Can't get your favorite streamer to add per-channel-emotes via. FrankerFaceZ, because they prefer Better Twitch TV?_ 20 | 21 | 22 | **Well, LOOK NO FURTHER!** 23 | This is a extension for FrankerFaceZ that allows **EXACTLY THAT**! 24 | 25 | With this, you have access to the **Better Twitch TV's** arsenal of **global emotes**, as well as their **per-channel-emotes**. 26 | 27 | Never miss out on that RareParrot again ![RareParrot](http://cdn.betterttv.net/emote/55a24e1294dd94001ee86b39/1x) 28 | 29 | # _**FAQ:**_ 30 | **"GIF emotes aren't working! Are they broken?"** 31 | _**No**, they aren't. They are disabled by default._ 32 | You have to get into the **FrankerFaceZ advanced settings** tab and go to _**"BTTV4FFZ"**_, then enable them. 33 | 34 | **"I just installed this for the Better Twitch TV per-channel-emotes. Can I disable the global ones?"** 35 | _**Yes.**_ 36 | Check the question above on how to do that. There is another toggle-bar to disable them : 37 | 38 | ## Links 39 | Download: https://lordmau5.com/bttv4ffz/ 40 | 41 | ## IMPORTANT: 42 | FrankerFaceZ is **required** for this extension to work! 43 | If FrankerFaceZ is not found, the emotes will **not be available!** 44 | Also, don't use this together with BetterTTV. 45 | -------------------------------------------------------------------------------- /Safari/BTTV4FFZ.safariextension/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lordmau5/BTTV4FFZ/3e877de4734524861c44cae7905fe768751d344c/Safari/BTTV4FFZ.safariextension/Icon.png -------------------------------------------------------------------------------- /Safari/BTTV4FFZ.safariextension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | lordmau5.com/bttv4ffz/ 7 | Builder Version 8 | 11601.6.17 9 | CFBundleDisplayName 10 | BTTV4FFZ 11 | CFBundleIdentifier 12 | com.rennsport.bttv4ffz 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | Chrome 20 | 21 | Database Quota 22 | 0 23 | 24 | Companion Bundle Identifier 25 | 26 | Content 27 | 28 | Blacklist 29 | 30 | https://api.twitch.tv/* 31 | http://api.twitch.tv/* 32 | 33 | Scripts 34 | 35 | End 36 | 37 | script.js 38 | 39 | 40 | Whitelist 41 | 42 | https://www.twitch.tv/* 43 | http://www.twitch.tv/* 44 | 45 | 46 | Description 47 | Add the BTTV emotes on-top of FFZ! 48 | DeveloperIdentifier 49 | LBHS782YH3 50 | ExtensionInfoDictionaryVersion 51 | 1.0 52 | Permissions 53 | 54 | Website Access 55 | 56 | Include Secure Pages 57 | 58 | Level 59 | All 60 | 61 | 62 | Website 63 | https://lordmau5.com/bttv4ffz/ 64 | 65 | 66 | -------------------------------------------------------------------------------- /Safari/BTTV4FFZ.safariextension/Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Safari/BTTV4FFZ.safariextension/script.js: -------------------------------------------------------------------------------- 1 | function inject() { 2 | var s = document.createElement('script'); 3 | s.src = "//cdn.lordmau5.com/inject.js"; 4 | s.onload = function() { 5 | this.parentNode.removeChild(this); 6 | }; 7 | (document.head || document.documentElement).appendChild(s); 8 | } 9 | 10 | inject(); 11 | -------------------------------------------------------------------------------- /Userscript/bttv4ffz_injector.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @id bttv4ffz 3 | // @name BetterTTV Emotes for FrankerFaceZ 4 | // @namespace BTTV4FFZ 5 | // 6 | // @version 1.1.2 7 | // @updateURL https://cdn.lordmau5.com/script/bttv4ffz_injector.user.js 8 | // 9 | // @description Ever wanted to have these slick Better Twitch TV emotes but you already have FrankerFaceZ installed? BetterTwitchTV for FrankerFaceZ takes care of that problem. 10 | // @author Lordmau5 11 | // @homepage https://lordmau5.com/bttv4ffz/ 12 | // @icon https://cdn.lordmau5.com/script/icon32.png 13 | // @icon64 https://cdn.lordmau5.com/script/icon64.png 14 | // @icon128 https://cdn.lordmau5.com/script/icon128.png 15 | // 16 | // @include http://twitch.tv/* 17 | // @include https://twitch.tv/* 18 | // @include http://*.twitch.tv/* 19 | // @include https://*.twitch.tv/* 20 | // 21 | // @exclude http://api.twitch.tv/* 22 | // @exclude https://api.twitch.tv/* 23 | // 24 | // @grant none 25 | // @run-at document-end 26 | // ==/UserScript== 27 | 28 | function bttv4ffz_init() { 29 | var script = document.createElement('script'); 30 | 31 | script.id = 'bttv4ffz_script'; 32 | script.type = 'text/javascript'; 33 | script.src = 'https://cdn.lordmau5.com/inject.js'; 34 | 35 | document.head.appendChild(s); 36 | } 37 | 38 | bttv4ffz_script(); 39 | -------------------------------------------------------------------------------- /inject.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is being updated on my server (cdn.lordmau5.com) first before changes to the GitHub repo happen. 3 | It will not be embedded in the addon anymore. 4 | This makes it easier for me to do updates when necessary (sudden FFZ-API changes, for example) 5 | */ 6 | 7 | (function(){ 8 | 9 | // Global Storage / Settings 10 | 11 | var version = "1.3.10"; 12 | 13 | var _initialized, 14 | 15 | api, 16 | ffz, 17 | last_emote_set_id = 9000, 18 | channels = {}, 19 | enable_global_emotes, 20 | enable_gif_emotes, 21 | enable_override_emotes, 22 | enable_pro_emotes, 23 | 24 | socketClient; 25 | 26 | var global_emotes_loaded = false, 27 | gif_emotes_loaded = false; 28 | 29 | var override_emotes = [ ":'(", "D:" ]; 30 | var isOverrideEmote = function(emote_regex) { 31 | for(var i = 0; i < override_emotes.length; i++) { 32 | if(emote_regex === override_emotes[i]) 33 | return true; 34 | } 35 | return false; 36 | }; 37 | 38 | // Initialization 39 | 40 | var check_existance = function(attempts) { 41 | if (window.FrankerFaceZ !== undefined && window.jQuery !== undefined && window.App !== undefined) { 42 | // Register with FFZ. 43 | ffz = FrankerFaceZ.get(); 44 | 45 | // Check for BTTV 46 | if (ffz.has_bttv) { 47 | console.log("BTTV was found. BTTV4FFZ will not work in combination with it."); 48 | return; 49 | } 50 | api = ffz.api("BetterTTV", "https://cdn.betterttv.net/tags/developer.png", version); 51 | socketClient = new SocketClient(); 52 | 53 | api.log("Injected successfully."); 54 | 55 | // Create emote URLs. 56 | api.emote_url_generator = function(set_id, emote_id) { 57 | return "https://manage.betterttv.net/emotes/" + emote_id; 58 | }; 59 | 60 | doSettings(); 61 | setupAPIHooks(); 62 | 63 | implementBTTVBadges(); 64 | if (enable_global_emotes) 65 | implementBTTVGlobals(); 66 | 67 | if(enable_pro_emotes) { 68 | socketClient.connect(); 69 | } 70 | } 71 | else { 72 | attempts = (attempts || 0) + 1; 73 | if (attempts < 60) 74 | return setTimeout(check_existance.bind(this, attempts), 1000); 75 | 76 | console.log("Could not find FFZ. Injection unsuccessful."); 77 | } 78 | }; 79 | 80 | 81 | var doSettings = function() { 82 | FrankerFaceZ.settings_info.bttv_global_emotes = { 83 | type: "boolean", 84 | value: true, 85 | category: "BTTV4FFZ", 86 | name: "Global Emoticons", 87 | help: "Enable this to make the BTTV global emotes available.", 88 | on_update: function(enabled) { 89 | if (!global_emotes_loaded) { 90 | if (enabled) 91 | implementBTTVGlobals(); 92 | return; 93 | } 94 | 95 | if (enabled) { 96 | api.register_global_set(1); 97 | 98 | if(enable_gif_emotes) 99 | api.register_global_set(2); 100 | 101 | if(enable_override_emotes) 102 | api.register_global_set(3); 103 | } 104 | else { 105 | api.unregister_global_set(1); 106 | api.unregister_global_set(2); 107 | api.unregister_global_set(3); 108 | } 109 | 110 | enable_global_emotes = enabled; 111 | } 112 | }; 113 | 114 | enable_global_emotes = ffz.settings.get("bttv_global_emotes"); 115 | 116 | FrankerFaceZ.settings_info.bttv_allow_gif_emotes = { 117 | type: "boolean", 118 | value: false, 119 | category: "BTTV4FFZ", 120 | name: "GIF Emoticons", 121 | help: "Enable this to show GIF emotes.", 122 | on_update: function(enabled) { 123 | if (enabled) { 124 | if(enable_global_emotes) 125 | api.register_global_set(2); 126 | 127 | var i = channels.length; 128 | while(i--) { 129 | var name = channels[i]; 130 | api.register_room_set(name, channels[name]["gifemotes_setid"], channels[name]["gifemotes"]); 131 | } 132 | } 133 | else { 134 | api.unregister_global_set(2); 135 | 136 | var i = channels.length; 137 | while(i--) { 138 | var name = channels[i]; 139 | if(channels[name]["gifemotes_still"]) 140 | api.register_room_set(name, channels[name]["gifemotes_setid"], channels[name]["gifemotes_still"]); 141 | else 142 | api.unload_set(channels[name]["gifemotes_setid"]); 143 | } 144 | } 145 | 146 | enable_gif_emotes = enabled; 147 | } 148 | }; 149 | 150 | enable_gif_emotes = ffz.settings.get("bttv_allow_gif_emotes"); 151 | 152 | FrankerFaceZ.settings_info.bttv_enable_override_emotes = { 153 | type: "boolean", 154 | value: false, 155 | category: "BTTV4FFZ", 156 | name: "Enable Override Emotes", 157 | help: "Enable this to show override emotes (like D:).", 158 | on_update: function(enabled) { 159 | if (enabled) { 160 | if(enable_global_emotes) 161 | api.register_global_set(3); 162 | } 163 | else { 164 | api.unregister_global_set(3); 165 | } 166 | 167 | enable_override_emotes = enabled; 168 | } 169 | }; 170 | 171 | enable_override_emotes = ffz.settings.get("bttv_enable_override_emotes"); 172 | 173 | FrankerFaceZ.settings_info.bttv_enable_pro_emotes = { 174 | type: "boolean", 175 | value: true, 176 | category: "BTTV4FFZ", 177 | name: "Enable BTTV Pro Emotes", 178 | help: "Enable this to show Pro emotes from other users. (Requires refresh!)", 179 | on_update: function(enabled) { 180 | if(!enabled) { 181 | var i = bttv_pro_users.length; 182 | while(i--) { 183 | var user = bttv_pro_users[i]; 184 | api.unload_set(user._id_emotes); 185 | } 186 | } 187 | 188 | enable_pro_emotes = enabled; 189 | } 190 | }; 191 | 192 | enable_pro_emotes = ffz.settings.get("bttv_enable_pro_emotes"); 193 | 194 | FrankerFaceZ.settings_info.bttv_show_emotes_in_menu = { 195 | type: "boolean", 196 | value: true, 197 | category: "BTTV4FFZ", 198 | name: "Show emotes in Emoticon Menu", 199 | help: "Enable this to show the emotes in the Emoticon Menu (you can still enter the emotes manually when this is disabled)", 200 | on_update: function(enabled) { 201 | api.emote_sets[1].hidden = !enabled; 202 | api.emote_sets[2].hidden = !enabled; 203 | api.emote_sets[3].hidden = !enabled; 204 | 205 | for(var name in channels) { 206 | api.log(name); 207 | api.emote_sets[channels[name]["emotes"]].hidden = !enabled; 208 | api.emote_sets[channels[name]["gifemotes_setid"]].hidden = !enabled; 209 | } 210 | } 211 | }; 212 | 213 | show_emotes_in_menu = ffz.settings.get("bttv_show_emotes_in_menu"); 214 | }; 215 | 216 | 217 | var setupAPIHooks = function() { 218 | api.on('room-add', channelCallback); 219 | api.on('room-message', chatFilter); 220 | 221 | api.iterate_rooms(); 222 | }; 223 | 224 | var chatFilter = function(msg) { 225 | if(enable_pro_emotes && ffz.get_user() && msg.from === ffz.get_user().login) { 226 | socketClient.broadcastMe(msg.room); 227 | } 228 | }; 229 | 230 | var hatEmotes = []; 231 | 232 | var channelCallback = function(room_id, reg_function, attempts) { 233 | if(enable_pro_emotes) { 234 | socketClient.joinChannel(room_id); 235 | } 236 | 237 | $.getJSON("https://api.betterttv.net/2/channels/" + room_id) 238 | .done(function(data) { 239 | var channelBTTV = new Array(), 240 | channelBTTV_GIF = new Array(), 241 | emotes = data["emotes"]; 242 | 243 | var i = emotes.length; 244 | while(i--) { 245 | var req_spaces = /[^A-Za-z0-9]/.test(emotes[i]["code"]); 246 | 247 | var emote = emotes[i], 248 | id = emote["id"], 249 | 250 | xMote = { 251 | urls: { 252 | 1: "https://cdn.betterttv.net/emote/" + id + "/1x", 253 | 2: "https://cdn.betterttv.net/emote/" + id + "/2x", 254 | 4: "https://cdn.betterttv.net/emote/" + id + "/3x" 255 | }, 256 | id: id, 257 | name: emote["code"], 258 | width: 28, 259 | height: 28, 260 | owner: { 261 | display_name: emote["channel"] || room_id, 262 | name: emote["channel"] 263 | }, 264 | require_spaces: req_spaces 265 | }; 266 | 267 | if (emote["imageType"] === "png") 268 | channelBTTV.push(xMote); 269 | 270 | if (emote["imageType"] === "gif") 271 | channelBTTV_GIF.push(xMote); 272 | } 273 | 274 | if (!channelBTTV.length && !channelBTTV_GIF.length) 275 | return; 276 | 277 | channels[room_id] = { 278 | emotes: last_emote_set_id, 279 | gifemotes_setid: last_emote_set_id + 1 280 | }; 281 | last_emote_set_id += 2; 282 | 283 | var set = { 284 | emoticons: channelBTTV, 285 | title: "Emoticons" 286 | }; 287 | 288 | if(channelBTTV.length) { 289 | api.register_room_set(room_id, channels[room_id]["emotes"], set); // Load normal emotes 290 | api.emote_sets[channels[room_id]["emotes"]].hidden = !show_emotes_in_menu; 291 | } 292 | 293 | set = { 294 | emoticons: channelBTTV_GIF, 295 | title: "Emoticons (GIF)" 296 | }; 297 | 298 | channels[room_id]["gifemotes"] = jQuery.extend(true, {}, set); 299 | var tempStillEmotes = jQuery.extend(true, {}, set); 300 | 301 | var stillEmotes = tempStillEmotes["emoticons"]; 302 | 303 | i = stillEmotes.length; 304 | while(i--) { 305 | var element = stillEmotes[i]; 306 | var j = element["urls"].length; 307 | while(j--) { 308 | var key = element["urls"][i]; 309 | var img = new Image(); 310 | 311 | img.onload = (function(array_index, size) { 312 | var canvas = document.createElement("canvas"); 313 | var ctx = canvas.getContext("2d"); 314 | 315 | canvas.width = this.width; 316 | canvas.height = this.height; 317 | 318 | ctx.drawImage(this, 0, 0); 319 | 320 | if(!channels[room_id]["gifemotes_still"]) { 321 | channels[room_id]["gifemotes_still"] = tempStillEmotes; 322 | } 323 | 324 | stillEmotes[array_index]["urls"][size] = canvas.toDataURL(); 325 | 326 | api.register_room_set(room_id, channels[room_id]["gifemotes_setid"], channels[room_id]["gifemotes_still"]); // Load static GIF emotes 327 | api.emote_sets[channels[room_id]["gifemotes_setid"]].hidden = !show_emotes_in_menu; 328 | }).bind(img, i, key); 329 | img.onerror = function(errorMsg, url, lineNumber, column, errorObj) { 330 | console.log("Couldn't load."); 331 | }; 332 | img.crossOrigin = "anonymous"; 333 | img.src = element["urls"][key] + ".png"; 334 | } 335 | } 336 | 337 | api.register_room_set(room_id, channels[room_id]["gifemotes_setid"], set); // Load GIF emotes 338 | api.emote_sets[channels[room_id]["gifemotes_setid"]].hidden = !show_emotes_in_menu; 339 | 340 | if(!enable_gif_emotes) 341 | api.unload_set(channels[room_id]["gifemotes_setid"]); 342 | }).fail(function(data) { 343 | if (data["status"] === 404) { 344 | return; 345 | } 346 | 347 | attempts = (attempts || 0) + 1; 348 | if (attempts < 12) { 349 | api.log("Failed to fetch BTTV channel emotes. Trying again in 5 seconds."); 350 | return setTimeout(channelCallback.bind(this, room_id, reg_function, attempts), 5000); 351 | } 352 | }); 353 | }; 354 | 355 | var implementBTTVGlobals = function(attempts) { 356 | $.getJSON("https://api.betterttv.net/emotes") 357 | .done(function(data) { 358 | var globalBTTV = new Array(), 359 | globalBTTV_GIF = new Array(), 360 | overrideEmotes = new Array(), 361 | 362 | emotes = data["emotes"]; 363 | 364 | var i = emotes.length; 365 | while(i--) { 366 | var req_spaces = /[^A-Za-z0-9]/.test(emotes[i]["regex"]); 367 | 368 | var emote = emotes[i], 369 | match = /cdn.betterttv.net\/emote\/(\w+)/.exec(emote["url"]), 370 | id = match && match[1]; 371 | 372 | if (emote["channel"]) 373 | continue; 374 | 375 | var xMote = { 376 | urls: { 1: emote["url"] }, 377 | name: emote["regex"], 378 | width: emote["width"], 379 | height: emote["height"], 380 | require_spaces: req_spaces 381 | }; 382 | 383 | if (id) { 384 | xMote["id"] = id; 385 | xMote["urls"] = { 386 | 1: "https://cdn.betterttv.net/emote/" + id + "/1x", 387 | 2: "https://cdn.betterttv.net/emote/" + id + "/2x", 388 | 4: "https://cdn.betterttv.net/emote/" + id + "/3x" 389 | }; 390 | } 391 | 392 | // Hat emote check 393 | if (hatEmotes.indexOf(emote["regex"]) != -1) { 394 | xMote["margins"] = "0px 0px 8px 0px"; 395 | xMote["modifier"] = true; 396 | } 397 | 398 | if(isOverrideEmote(emote["regex"])) 399 | overrideEmotes.push(xMote); 400 | else { 401 | emote["imageType"] === "gif" ? globalBTTV_GIF.push(xMote) : globalBTTV.push(xMote); 402 | } 403 | } 404 | 405 | var set = { 406 | emoticons: globalBTTV 407 | }; 408 | api.register_global_set(1, set); 409 | if(!enable_global_emotes) 410 | api.unregister_global_set(1); 411 | api.emote_sets[1].hidden = !show_emotes_in_menu; 412 | 413 | set = { 414 | emoticons: globalBTTV_GIF, 415 | title: "Global Emoticons (GIF)" 416 | }; 417 | api.register_global_set(2, set); 418 | if(!enable_global_emotes || !enable_gif_emotes) 419 | api.unregister_global_set(2); 420 | api.emote_sets[2].hidden = !show_emotes_in_menu; 421 | 422 | set = { 423 | emoticons: overrideEmotes, 424 | title: "Global Emoticons (Override)" 425 | }; 426 | api.register_global_set(3, set); 427 | if(!enable_global_emotes || !enable_override_emotes) 428 | api.unregister_global_set(3); 429 | api.emote_sets[3].hidden = !show_emotes_in_menu; 430 | 431 | global_emotes_loaded = true; 432 | 433 | }).fail(function(data) { 434 | if (data["status"] === 404) 435 | return; 436 | 437 | attempts = (attempts || 0) + 1; 438 | if (attempts < 12) { 439 | api.log("Failed to fetch BTTV global emotes. Trying again in 5 seconds."); 440 | return setTimeout(implementBTTVGlobals.bind(this, attempts), 5000); 441 | } 442 | }); 443 | }; 444 | 445 | var implementBTTVBadges = function(attempts) { 446 | api.add_badge("bttv4ffz", { 447 | name: "bttv4ffz", 448 | title: "BTTV4FFZ Developer", 449 | image: "https://cdn.lordmau5.com/Mau5Badge.png", 450 | alpha_image: "https://cdn.lordmau5.com/Mau5Badge_Alpha.png", 451 | color: "#49acff" 452 | }); 453 | api.user_add_badge("lordmau5", 20, "bttv4ffz"); 454 | 455 | $.getJSON("https://api.betterttv.net/2/badges") 456 | .done(function(data) { 457 | var types = new Array(), 458 | badges = new Array(), 459 | 460 | _types = data["types"], 461 | _users = data["badges"]; 462 | 463 | var i = _types.length; 464 | while(i--) { 465 | var _type = _types[i]; 466 | 467 | var type = { 468 | name: _type.name, 469 | title: _type.description, 470 | image: _type.svg, 471 | no_invert: true 472 | }; 473 | 474 | types[type.name] = type; 475 | api.add_badge(type.name, type); 476 | } 477 | 478 | i = _users.length; 479 | while(i--) { 480 | var _user = _users[i]; 481 | 482 | if(types[_user.type] !== undefined) { 483 | api.log("Adding badge '" + _user.type + "' for user '" + _user.name + "'."); 484 | api.user_add_badge(_user.name, 21, _user.type); 485 | } 486 | } 487 | }).fail(function(data) { 488 | if (data["status"] === 404) 489 | return; 490 | 491 | attempts = (attempts || 0) + 1; 492 | if (attempts < 12) { 493 | api.log("Failed to fetch BTTV badges emotes. Trying again in 5 seconds."); 494 | return setTimeout(implementBTTVBadges.bind(this, attempts), 5000); 495 | } 496 | }); 497 | }; 498 | 499 | /* Attempt on hooking into the BTTV WebSocket servers for BTTV-Pro emotes */ 500 | var bttv_pro_users = {}; 501 | 502 | BTTVProUser = function(username, emotes_array) { 503 | this.username = username; 504 | this.emotes_array = emotes_array; 505 | 506 | this.initialize(); 507 | 508 | bttv_pro_users[this.username] = this; 509 | }; 510 | 511 | BTTVProUser.prototype.loadEmotes = function() { 512 | this.emotes_array.forEach(function(emote, index, array) { 513 | var xMote = { 514 | urls: { 515 | 1: "https://cdn.betterttv.net/emote/" + emote["id"] + "/1x", 516 | 2: "https://cdn.betterttv.net/emote/" + emote["id"] + "/2x", 517 | 4: "https://cdn.betterttv.net/emote/" + emote["id"] + "/3x" 518 | }, 519 | id: emote["id"], 520 | name: emote["code"], 521 | width: 28, 522 | height: 28, 523 | owner: { 524 | display_name: emote["channel"] || "", 525 | name: emote["channel"] 526 | }, 527 | require_spaces: true 528 | }; 529 | 530 | if(emote["imageType"] === "png") 531 | this.emotes.push(xMote); 532 | 533 | if(emote["imageType"] === "gif") 534 | this.gif_emotes.push(xMote); 535 | }, this); 536 | 537 | var set = { 538 | emoticons: this.emotes, 539 | title: "Personal Emotes" 540 | }; 541 | 542 | if(this.emotes.length) { 543 | api.load_set(this._id_emotes, set); 544 | api.user_add_set(this.username, this._id_emotes); 545 | } 546 | }; 547 | 548 | BTTVProUser.prototype.initialize = function() { 549 | this._id_emotes = this.username + "_images"; 550 | this.emotes = new Array(); 551 | 552 | this.loadEmotes(); 553 | }; 554 | 555 | var bttv_pro_events = {}; 556 | 557 | // BetterTTV Pro 558 | bttv_pro_events.lookup_user = function(subscription) { 559 | if (!subscription.pro || !enable_pro_emotes) return; 560 | 561 | if (subscription.pro && subscription.emotes) { 562 | if(subscription.name in bttv_pro_users) { 563 | bttv_pro_users[subscription.name].emotes_array = subscription.emotes; 564 | bttv_pro_users[subscription.name].loadEmotes(); 565 | } 566 | else { 567 | new BTTVProUser(subscription.name, subscription.emotes); 568 | } 569 | } 570 | }; 571 | 572 | SocketClient = function() { 573 | this.socket = false; 574 | this._lookedUpUsers = []; 575 | this._connected = false; 576 | this._connecting = false; 577 | this._connectAttempts = 1; 578 | this._joinedChannels = []; 579 | this._connectionBuffer = []; 580 | this._events = bttv_pro_events; 581 | } 582 | 583 | SocketClient.prototype.connect = function() { 584 | if (ffz.get_user() === undefined) return; 585 | if (this._connected || this._connecting) return; 586 | this._connecting = true; 587 | 588 | api.log('SocketClient: Connecting to Beta BetterTTV Socket Server'); 589 | 590 | var _self = this; 591 | this.socket = new WebSocket('wss://sockets.betterttv.net/ws'); 592 | 593 | this.socket.onopen = function() { 594 | api.log('SocketClient: Connected to Beta BetterTTV Socket Server'); 595 | 596 | _self._connected = true; 597 | _self._connectAttempts = 1; 598 | 599 | if(_self._connectionBuffer.length > 0) { 600 | var i = _self._connectionBuffer.length; 601 | while(i--) { 602 | var channel = _self._connectionBuffer[i]; 603 | _self.joinChannel(channel); 604 | _self.broadcastMe(channel); 605 | } 606 | _self._connectionBuffer = []; 607 | } 608 | }; 609 | 610 | this.socket.onerror = function() { 611 | api.log('SocketClient: Error from Beta BetterTTV Socket Server'); 612 | 613 | _self._connectAttempts++; 614 | _self.reconnect(); 615 | }; 616 | 617 | this.socket.onclose = function() { 618 | if (!_self._connected || !_self.socket) return; 619 | 620 | api.log('SocketClient: Disconnected from Beta BetterTTV Socket Server'); 621 | 622 | _self._connectAttempts++; 623 | _self.reconnect(); 624 | }; 625 | 626 | this.socket.onmessage = function(message) { 627 | var evt; 628 | 629 | try { 630 | evt = JSON.parse(message.data); 631 | } 632 | catch (e) { 633 | debug.log('SocketClient: Error Parsing Message', e); 634 | } 635 | 636 | if (!evt || !(evt.name in _self._events)) return; 637 | 638 | api.log('SocketClient: Received Event'); 639 | api.log(evt); 640 | 641 | _self._events[evt.name](evt.data); 642 | }; 643 | }; 644 | 645 | SocketClient.prototype.reconnect = function() { 646 | var _self = this; 647 | 648 | this.disconnect(); 649 | 650 | if (this._connecting === false) return; 651 | this._connecting = false; 652 | 653 | setTimeout(function() { 654 | _self.connect(); 655 | }, Math.random() * (Math.pow(2, this._connectAttempts) - 1) * 30000); 656 | }; 657 | 658 | SocketClient.prototype.disconnect = function() { 659 | var _self = this; 660 | 661 | if (this.socket) { 662 | try { 663 | this.socket.close(); 664 | } 665 | catch (e) {} 666 | } 667 | 668 | delete this.socket; 669 | 670 | this._connected = false; 671 | }; 672 | 673 | SocketClient.prototype.emit = function(evt, data) { 674 | if (!this._connected || !this.socket) return; 675 | 676 | this.socket.send(JSON.stringify({ 677 | name: evt, 678 | data: data 679 | })); 680 | }; 681 | 682 | // Introduce myself 683 | SocketClient.prototype.broadcastMe = function(channel) { 684 | if (!this._connected) return; 685 | if (!ffz.get_user()) return; 686 | 687 | this.emit('broadcast_me', { name: ffz.get_user().login, channel: channel }); 688 | }; 689 | 690 | SocketClient.prototype.joinChannel = function(channel) { 691 | if (!this._connected) { 692 | if (!this._connectionBuffer.includes(channel)) 693 | this._connectionBuffer.push(channel); 694 | return; 695 | } 696 | 697 | if (!channel.length) return; 698 | 699 | if (this._joinedChannels[channel]) { 700 | this.emit('part_channel', { name: channel }); 701 | } 702 | 703 | this.emit('join_channel', { name: channel }); 704 | this._joinedChannels[channel] = true; 705 | }; 706 | 707 | // Finally, load. 708 | check_existance(); 709 | })(); 710 | -------------------------------------------------------------------------------- /pack.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | puts "Please enter the version (Pattern: 1.0.4, 2.0.5, 3.3.3): " 4 | $version = gets.strip 5 | 6 | ########################################################## 7 | ## Firefox ## 8 | ########################################################## 9 | 10 | def ff_copy_template() 11 | FileUtils.cp("Firefox/template__/install.rdf", "Firefox/install.rdf") 12 | FileUtils.cp("Firefox/template__/harness-options.json", "Firefox/harness-options.json") 13 | end 14 | ff_copy_template 15 | 16 | def ff_replace_version() 17 | text = File.read("Firefox/install.rdf") 18 | new_content = text.gsub(/%%VERSION%%/, $version) 19 | 20 | File.open("Firefox/install.rdf", "w") { |file| file.puts new_content } 21 | 22 | ###################################################################### 23 | 24 | text = File.read("Firefox/harness-options.json") 25 | new_content = text.gsub(/%%VERSION%%/, $version) 26 | 27 | File.open("Firefox/harness-options.json", "w") { |file| file.puts new_content } 28 | end 29 | ff_replace_version 30 | 31 | def ff_archive_xpi() 32 | %x(files/7za.exe a archives/BTTV4FFZ-#{$version}.xpi .\\Firefox\\*) 33 | %x(files/7za.exe d archives/BTTV4FFZ-#{$version}.xpi template__) 34 | end 35 | ff_archive_xpi 36 | 37 | ########################################################## 38 | ## Chrome ## 39 | ########################################################## 40 | 41 | def ch_copy_template() 42 | FileUtils.cp("Chrome/template__/manifest.json", "Chrome/manifest.json") 43 | end 44 | ch_copy_template 45 | 46 | def ch_replace_version() 47 | text = File.read("Chrome/manifest.json") 48 | new_content = text.gsub(/%%VERSION%%/, $version) 49 | 50 | File.open("Chrome/manifest.json", "w") { |file| file.puts new_content } 51 | end 52 | ch_replace_version 53 | 54 | def ch_archive_zip() 55 | %x(files/7za.exe a archives/BTTV4FFZ-#{$version}.zip .\\Chrome\\*) 56 | %x(files/7za.exe d archives/BTTV4FFZ-#{$version}.zip template__) 57 | end 58 | ch_archive_zip 59 | --------------------------------------------------------------------------------