Conkeror has changed the way I browse the web: it's basically Emacs + Firefox with javascript based configuration in .conkerrorc rather than elisp configuration in .emacs.
I've built up a huge library of .emacs customizations over the years by getting little bits and pieces from others. I'm just starting with Conkeror but the fact that it uses JS (far more widely known than Elisp) must mean that there's some amazing stuff out there.
Care to share your pieces? I'm particularly interested in stuff that interacts well with pages produced by Django (or other dynamic web pages).
For example, I'd love a Conkeror-based action recorder that let me browse a site and find bugs, and then immediately save & submit the sequence of actions as a bug report with a single keystroke. By including the JS actions required to replicate the error, it would be the ultimate testing harness -- even better than Selenium because it would be totally keyboard driven.
Here's mine:
// homepage = "http://www.google.com";
// set default webjump
read_url_handler_list = [read_url_make_default_webjump_handler("google")];
// possibly valid URL
function possibly_valid_url (str) {
return (/[\.\/:]/.test(str)) &&
!(/\S\s+\S/.test(str)) &&
!(/^\s*$/.test(str));
}
// page modes
require("page-modes/google-search-results.js"); // google search results
require("page-modes/wikipedia.js"); // wikipedia mode
// webjumps
define_webjump("gmail", "https://mail.google.com"); // gmail inbox
define_webjump("twitter", "http://twitter.com/#!/search/%s", $alternative = "https://twitter.com/"); // twitter
define_webjump("w3schools", "http://www.w3schools.com"); // w3schools site
define_webjump("w3search", "http://www.google.com/search?sitesearch=www.w3schools.com&as_q=%s"); // w3schools search
define_webjump("jquery", "http://docs.jquery.com/Special:Search?ns0=1&search=%s"); // jquery
define_webjump("archwiki", "https://wiki.archlinux.org/index.php?search=%s"); // arch wiki
define_webjump("stackoverflow", "http://stackoverflow.com/search?q=%s", $alternative = "http://stackoverflow.com/"); // stackoverflow
define_webjump("sor", "http://stackoverflow.com/search?q=[r]+%s", $alternative = "http://stackoverflow.com/questions/tagged/r"); // stackoverflow R section
define_webjump("stats", "http://stats.stackexchange.com/search?q=%s"); // stats
define_webjump("torrentz", "http://torrentz.eu/search?q=%s"); // torrentz
define_webjump("avaxsearch", "http://avaxsearch.com/avaxhome_search?q=%s&a=&c=&l=&sort_by=&commit=Search"); // avaxsearch
define_webjump("imdb", "http://www.imdb.com/find?s=all;q=%s"); // imdb
define_webjump("duckgo", "http://duckduckgo.com/?q=%s", $alternative = "http://duckduckgo.com"); // duckduckgo
define_webjump("blekko", "http://blekko.com/ws/%s", $alternative = "http://blekko.com/"); // blekko
define_webjump("youtube", "http://www.youtube.com/results?search_query=%s&aq=f", $alternative = "http://www.youtube.com"); // youtube
define_webjump("duckgossl", "https://duckduckgo.com/?q=%s"); // duckduckgo SSL
define_webjump("downforeveryoneorjustme", "http://www.downforeveryoneorjustme.com/%s"); // downforeveryoneorjustme
define_webjump("urbandictionary", "http://www.urbandictionary.com/define.php?term=%s"); // urban dictionary
define_webjump("rts", "http://rts.rs"); // RTS
define_webjump("facebook", "http://www.facebook.com"); // facebook homepage
// tab bar
require("new-tabs.js");
// clicks in new buffer
require("clicks-in-new-buffer.js");
// Set to either OPEN_NEW_BUFFER(_BACKGROUND)
clicks_in_new_buffer_target = OPEN_NEW_BUFFER_BACKGROUND; // Now buffers open in background.
// history webjump
define_browser_object_class(
"history-url", null,
function (I, prompt) {
check_buffer (I.buffer, content_buffer);
var result = yield I.buffer.window.minibuffer.read_url(
$prompt = prompt, $use_webjumps = false, $use_history = true, $use_bookmarks = false);
yield co_return (result);
});
interactive("find-url-from-history",
"Find a page from history in the current buffer",
"find-url",
$browser_object = browser_object_history_url);
interactive("find-url-from-history-new-buffer",
"Find a page from history in the current buffer",
"find-url-new-buffer",
$browser_object = browser_object_history_url);
define_key(content_buffer_normal_keymap, "h", "find-url-from-history-new-buffer");
define_key(content_buffer_normal_keymap, "H", "find-url-from-history");
// load session module
require("session.js");
session_auto_save_auto_load = true; // auto-load session
// don't open download buffer automatically
remove_hook("download_added_hook", open_download_buffer_automatically);
// don't show clock
remove_hook("mode_line_hook", mode_line_adder(clock_widget));
// add favicons
require("favicon");
add_hook("mode_line_hook", mode_line_adder(buffer_icon_widget), true);
read_buffer_show_icons = true;
// add content handlers
content_handlers.set("application/pdf", content_handler_save); // pdf
// torrent
// mp3
// ogg
function define_switch_buffer_key (key, buf_num) {
define_key(default_global_keymap, key,
function (I) {
switch_to_buffer(I.window,
I.window.buffers.get_buffer(buf_num));
});
}
for (let i = 0; i < 10; ++i) {
define_switch_buffer_key(String((i+1)%10), i);
}
function enable_scrollbars (buffer) {
buffer.top_frame.scrollbars.visible = true;
}
add_hook("create_buffer_late_hook", enable_scrollbars);
Since no one else seems interested in posting an actual reply, I will.
function my_title_format (window) {
return 'conkeror {'+get_current_profile()+'}'+window.buffers.current.description;
}
I find it helpful to have the profile name in the window title as I use multiple profiles. It doesn't help me so much now that I use StumpWM and don't look at the window list much, but it's very helpful in other window managers.
define_key(content_buffer_normal_keymap, "C-x C-c", "confirm-quit");
can_kill_last_buffer = false;
These two keep me from accidentally closing a conkeror window by closing the last buffer.
Well, I can't really help you here but I just wanted to say that although I'm an emacs guy, I dropped Conkeror for vimperator because IMHO the browsing experience is much better!
Conkeror is merely Firefox with Emacs keybindings. I don't think it is, and nor does it intend to be, a fully featured environment as Emacs is.
It facilitates your browsing experience by allowing you to use your well-worn Emacs muscle memory. And lets you keep your hands on the keyboard more, instead of jumping from keyboard to mouse all the time.
So I don't think it is really capable of doing some of the things you imagine.
Its development is also rather slow and is probably understaffed. You'll find, for example, that you can't use most common Firefox addons/extensions with it. Firebug, Greasemonkey, Gears, etc.
I do like it for some browsing tasks: it has special modes for reading Reddit, Google Reader, and others that make things a lot easier.
But I also agree with binOr that vimperator is much better, even if you're an emacs guy. Helps if you use VIM of course.
Related
I'm working on changing the UX of a proprietary, heavily obfuscated and tamper-proof web application, so I can't nip this in the bud at server level; I'm limited to the sandbox of my browser and bookmarklets.
A page loads up main code (<script preload>) and user config (<script defer>) in two separate queries. I would like for a setting in the user config to have a different value than what the server sends.
Doing it for myself, I could tamper with the config before it even arrives in the browser, but I have to make this work reliably for other users as well, without having them to install a whole bunch of stuff like Greasemonkey, Fiddler, whatever.
I've tried two equally inefficient approaches:
When the page starts loading, execute this and hope I win the race:
{
var loadedConf = document.getElementById("user").innerHTML;
if(typeof loadedConf === "undefined")
{
setTimeout(raceReplace, 5);
rantime += 5;
}
else
{
document.getElementById("user").innerHTML = loadedConf.replace(/arbitraryConfigField":false/,
'arbitraryConfigField":"arbitraryConfigValue"');
console.log("Raced to a finish in " + rantime + "ms")
}
}
raceReplace()
Because it's a race, it's wholly contingent upon how fast a user clicks the bookmarklet. With practice, I can do it almost 50% of the time! Heh.
Extend JSON.parse to make it return an object as it normally would, the way God and Douglas Crockford intended, except an arbitrary key/value:
JSON.parse = (text, fn) => {
const result = parse(text, fn)
if (result && result.arbitraryConfigField) {
result.arbiraryConfigField = 'arbitraryConfigValue'
}
return result
}
A better approach, but also prone to racing and could hinder regular JSON.parse performance.
(I'm wondering if performance-wise it wouldn't be better to just do a text.replace without any checking.)
Is there yet a third way to wait for the page to load, hook into it and on a subsequent reload (which, yes, necessarily re-renders everything) have the necessary value without the hassle and uncertainty of a race?
It would be grear to have this without the heavy artillery of designing a Chrome extension or hooking with Greasemonkey.
I recently needed to implement an automatic download of a CSV file in javascript. I didn't want to use any 3rd party libraries so instead I studies how the 3rd party libraries do it. I took a look at FileSaver npm package specifically at the function saveAs from here.
Eventually I changed the code to suit my needs into something like this:
class BlobDownload {
constructor(window, blob, fileName, contentType) {
this.window = window;
this.document = window.document;
this.blob = blob;
this.fileName = fileName;
this.contentType = contentType;
}
asyncCreateObjectURL = blob => {
return new Promise((resolve, reject) => {
const blobURL = this.window.URL.createObjectURL(blob);
blobURL ? resolve(blobURL) : reject();
});
}
click = (node) => {
try {
node.dispatchEvent(new MouseEvent('click'));
} catch (e) {
const evt = this.document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, this.window, 0, 0, 0, 80,
20, false, false, false, false, 0, null);
node.dispatchEvent(evt);
}
};
download = async () => {
const blob = new Blob([this.blob], { type: this.contentType });
const a = this.document.createElement('a');
a.download = this.fileName;
a.href = await this.asyncCreateObjectURL(blob);
setTimeout(() => { this.window.URL.revokeObjectURL(a.href) }, 60000) // 1min
setTimeout(() => { this.click(a) }, 0)
}
}
export default BlobDownload;
I don't understand a few things in the code though:
we're create an a node but it doesn't display anywhere on the page. Where does this node actually reside, only as an object in RAM?
the function click dispatched click event immediately, but if this doesn't work it creates a new event and then dispatches it. Why do we need to account for the case where simple dispatching doesn't work?
the whole procedure of creating an temporary a link and artificially clicking on it seems like a hack. Is this really a good pattern or better practices exist to download files?
we're create an a node but it doesn't display anywhere on the page. Where does this node actually reside, only as an object in RAM?
Yes, it will only reside in the memory but it's still possible to call an event on it as evident by the source code. Note: this will not work on iOS Safari. There you have to display the actual link and prompt the user to tap it - there's a note about it on FileSaver.js's README.md.
the function click dispatched click event immediately, but if this doesn't work it creates a new event and then dispatches it. Why do we need to account for the case where simple dispatching doesn't work?
Reading the MDN page for initMouseEvent tells us it deprecated. If the click function fails and the try-catch is triggered then we're on a really old browser where initMouseEvent should be available. If you go back in the commit history the code used to be far more complicated due to edge cases and ultimately not needed feature detection.
the whole procedure of creating an temporary a link and artificially clicking on it seems like a hack. Is this really a good pattern or better practices exist to download files?
It's a hack, there's no way around it. Best practices won't get you far if you need to have backwards compatibility. FileSaver.js strives to abstract away all of the gnarly hacking needed to handle programmatic file saving so that you don't have to.
In general I would advise against copying over library code for a custom implementation. FileSaver.js is over 9 years old now and still relevant. Having it as a package.json dependency or a copy-paste in a library folder would preserve it's attribution making maintenance easier on the next guy :).
In general it's a good set of questions! I'm also implementing a CSV download feature right now. I stumbled on it from googling based on this SO answer while trying to get file saving to work with Web Workers without the need to postMessage the contents of a file. Apparently passing only an URL to a blob is enough for saveAs to work!
// in web worker return from promise-worker/postMessage ->
URL.createObjectURL(new Blob([data], { type: 'text/csv;charset=utf-8;' }));
// on main thread
saveAs(objectURLCreatedAbove);
// done :)
Consider using Web Workers if your CSV's get big and there are considerable freezes when you prepare data for downloading. The sheetjs library is also quite helpful if you need to do anything with spreadsheet data.
Happy coding!
Complete - Edited Once
I am looking to create a Like Counter with persistent Memory!
Right now, my project is stored on a USB-Drive and I'm not thinking of uploading my semi-finished site to the Internet just yet. I'm carrying it around, plugging and working.
A feature of the site, is a Heart Counter and Like Counter, respective with their symbolic icons.
I have a little sideline JavaScript file that has a dozen functions to handle the click-events and such - such as the Number Count of the counters.
But, as the values of the counters are auto-assigned to Temporary Memory - if you were to reload the page - the counter number would reset to it's default, Zero. A huge headache...
Reading from .txt
I thought of using the experimental ReadFile() object to handle the problem - but I soon found that it needed a user-put file to operate (from my examinations).
Here's my attempt:
if (heartCount || likeCount >= 1) {
var reader = new FileReader();
var readerResults = reader.readAsText(heartsAndLikes.txt);
//return readerResults
alert(readerResults);
}
When loaded, the page runs through standard operations, except for the above.
This, in my opinion, would have been the ideal solution...
Reading from Cookies
Cookies now don't seem like an option as it resides on a per-computer basis.
They are stored on the computer's SSD, not in the JavaScript File... sad...
HTML5 Web Storage
Using the new Web Storage will be of big help, probably. But again, it is on a per-computer basis, no matter how beautiful the system is...
localStorage.heartCount = 0 //Originally...
function heartButtonClicked() {
if (localStorage.heartCount) {
localStorage.heartCount = Number(localStorage.heartCount) + 1
}
document.getElementById('heartCountDisplay').innerHTML = localStorage.heartCount
} //Function is tied to the heartCountButton directly via the 'onclick' method
However, I am questioning whether web storage can be carried over on a USB-Drive...
Summarised ideas
Currently, I am looking to Reading and Editing the files, as it's most ideal to my situation. But...
Which would you use? Would you introduce a new method of things?
Please, tell me about it! :)
if (typeof(Storage) !== "undefined") { //make sure local storage is available
if (!localStorage.heartCount) { //if heartCount is not set then set it to zero
localStorage.heartCount = 0;
}
} else {
alert('Local storage is not available');
}
function heartButtonClicked() {
if (localStorage.heartCount) { //if heartCount exists then increment it by one
localStorage.heartCount++;
}
//display the result
document.getElementById('heartCountDisplay').innerHTML = localStorage.heartCount
}
This will only work on a per computer basis and will not persist on your thumb drive. The only way I can think of to persist the data on your drive is to manually download a JSON or text file.
I want to load a shared worker with a user-script. The problem is the user-script is free, and has no business model for hosting a file - nor would I want to use a server, even a free one, to host one tiny file. Regardless, I tried it and I (of course) get a same origin policy error:
Uncaught SecurityError: Failed to construct 'SharedWorker': Script at
'https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js'
cannot be accessed from origin 'http://stackoverflow.com'.
There's another way to load a web worker by converting the worker function to a string and then into a Blob and loading that as the worker but I tried that too:
var sharedWorkers = {};
var startSharedWorker = function(workerFunc){
var funcString = workerFunc.toString();
var index = funcString.indexOf('{');
var funcStringClean = funcString.substring(index + 1, funcString.length - 1);
var blob = new Blob([funcStringClean], { type: "text/javascript" });
sharedWorkers.google = new SharedWorker(window.URL.createObjectURL(blob));
sharedWorkers.google.port.start();
};
And that doesn't work either. Why? Because shared workers are shared based on the location their worker file is loaded from. Since createObjectURL generates a unique file name for each use, the workers will never have the same URL and will therefore never be shared.
How can I solve this problem?
Note: I tried asking about specific solutions, but at this point I think
the best I can do is ask in a more broad manner for any
solution to the problem, since all of my attempted solutions seem
fundamentally impossible due to same origin policies or the way
URL.createObjectURL works (from the specs, it seems impossible to
alter the resulting file URL).
That being said, if my question can somehow be improved or clarified, please leave a comment.
You can use fetch(), response.blob() to create an Blob URL of type application/javascript from returned Blob; set SharedWorker() parameter to Blob URL created by URL.createObjectURL(); utilize window.open(), load event of newly opened window to define same SharedWorker previously defined at original window, attach message event to original SharedWorker at newly opened windows.
javascript was tried at console at How to clear the contents of an iFrame from another iFrame, where current Question URL should be loaded at new tab with message from opening window through worker.port.postMessage() event handler logged at console.
Opening window should also log message event when posted from newly opened window using worker.postMessage(/* message */), similarly at opening window
window.worker = void 0, window.so = void 0;
fetch("https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js")
.then(response => response.blob())
.then(script => {
console.log(script);
var url = URL.createObjectURL(script);
window.worker = new SharedWorker(url);
console.log(worker);
worker.port.addEventListener("message", (e) => console.log(e.data));
worker.port.start();
window.so = window.open("https://stackoverflow.com/questions/"
+ "38810002/"
+ "how-can-i-load-a-shared-web-worker-"
+ "with-a-user-script", "_blank");
so.addEventListener("load", () => {
so.worker = worker;
so.console.log(so.worker);
so.worker.port.addEventListener("message", (e) => so.console.log(e.data));
so.worker.port.start();
so.worker.port.postMessage("hi from " + so.location.href);
});
so.addEventListener("load", () => {
worker.port.postMessage("hello from " + location.href)
})
});
At console at either tab you can then use, e.g.; at How to clear the contents of an iFrame from another iFrame worker.postMessage("hello, again") at new window of current URL How can I load a shared web worker with a user-script?, worker.port.postMessage("hi, again"); where message events attached at each window, communication between the two windows can be achieved using original SharedWorker created at initial URL.
Precondition
As you've researched and as it has been mentioned in comments,
SharedWorker's URL is subject to the Same Origin Policy.
According to this question there's no CORS support for Worker's URL.
According to this issue GM_worker support is now a WONT_FIX, and
seems close enough to impossible to implement due to changes in Firefox.
There's also a note that sandboxed Worker (as opposed to
unsafeWindow.Worker) doesn't work either.
Design
What I suppose you want to achieve is a #include * userscript that will collect some statistics or create some global UI what will appear everywhere. And thus you want to have a worker to maintain some state or statistic aggregates in runtime (which will be easy to access from every instance of user-script), and/or you want to do some computation-heavy routine (because otherwise it will slow target sites down).
In the way of any solution
The solution I want to propose is to replace SharedWorker design with an alternative.
If you want just to maintain a state in the shared worker, just use Greasemonkey storage (GM_setValue and friends). It's shared among all userscript instances (SQLite behide the scenes).
If you want to do something computation-heavy task, to it in unsafeWindow.Worker and put result back in Greasemonkey storage.
If you want to do some background computation and it must be run only by single instance, there are number of "inter-window" synchronisation libraries (mostly they use localStorage but Greasemomkey's has the same API, so it shouldn't be hard to write an adapter to it). Thus you can acquire a lock in one userscript instance and run your routines in it. Like, IWC or ByTheWay (likely used here on Stack Exchange; post about it).
Other way
I'm not sure but there may be some ingenious response spoofing, made from ServiceWorker to make SharedWorker work as you would like to. Starting point is in this answer's edit.
I am pretty sure you want a different answer, but sadly this is what it boils down to.
Browsers implement same-origin-policies to protect internet users, and although your intentions are clean, no legit browser allows you to change the origin of a sharedWorker.
All browsing contexts in a sharedWorker must share the exact same origin
host
protocol
port
You cannot hack around this issue, I've trying using iframes in addition to your methods, but non will work.
Maybe you can put it your javascript file on github and use their raw. service to get the file, this way you can have it running without much efforts.
Update
I was reading chrome updates and I remembered you asking about this.
Cross-origin service workers arrived on chrome!
To do this, add the following to the install event for the SW:
self.addEventListener('install', event => {
event.registerForeignFetch({
scopes: [self.registration.scope], // or some sub-scope
origins: ['*'] // or ['https://example.com']
});
});
Some other considerations are needed aswell, check it out:
Full link: https://developers.google.com/web/updates/2016/09/foreign-fetch?hl=en?utm_campaign=devshow_series_crossoriginserviceworkers_092316&utm_source=gdev&utm_medium=yt-desc
Yes you can! (here's how):
I don't know if it's because something has changed in the four years since this question was asked, but it is entirely possible to do exactly what the question is asking for. It's not even particularly difficult. The trick is to initialize the shared worker from a data-url that contains its code directly, rather than from a createObjectURL(blob).
This is probably most easily demonstrated by example, so here's a little userscript for stackoverflow.com that uses a shared worker to assign each stackoverflow window a unique ID number, displayed in the tab title. Note that the shared-worker code is directly included as a template string (i.e. between backtick quotes):
// ==UserScript==
// #name stackoverflow userscript shared worker example
// #namespace stackoverflow test code
// #version 1.0
// #description Demonstrate the use of shared workers created in userscript
// #icon https://stackoverflow.com/favicon.ico
// #include http*://stackoverflow.com/*
// #run-at document-start
// ==/UserScript==
(function() {
"use strict";
var port = (new SharedWorker('data:text/javascript;base64,' + btoa(
// =======================================================================================================================
// ================================================= shared worker code: =================================================
// =======================================================================================================================
// This very simple shared worker merely provides each window with a unique ID number, to be displayed in the title
`
var lastID = 0;
onconnect = function(e)
{
var port = e.source;
port.onmessage = handleMessage;
port.postMessage(["setID",++lastID]);
}
function handleMessage(e) { console.log("Message Recieved by shared worker: ",e.data); }
`
// =======================================================================================================================
// =======================================================================================================================
))).port;
port.onmessage = function(e)
{
var data = e.data, msg = data[0];
switch (msg)
{
case "setID": document.title = "#"+data[1]+": "+document.title; break;
}
}
})();
I can confirm that this is working on FireFox v79 + Tampermonkey v4.11.6117.
There are a few minor caveats:
Firstly, it might be that the page your userscript is targeting is served with a Content-Security-Policy header that explicitly restricts the sources for scripts or worker scripts (script-src or worker-src policies). In that case, the data-url with your script's content will probably be blocked, and OTOH I can't think of a way around that, unless some future GM_ function gets added to allow a userscript to override a page's CSP or change its HTTP headers, or unless the user runs their browser with an extension or browser settings to disable CSP (see e.g. Disable same origin policy in Chrome).
Secondly, userscripts can be defined to run on multiple domains, e.g. you might run the same userscript on https://amazon.com and https://amazon.co.uk. But even when created by this single userscript, shared workers obey the same-origin policy, so there should be a different instance of the shared worker that gets created for all the .com windows vs for all the .co.uk windows. Be aware of this!
Finally, some browsers may impose a size limit on how long data-urls can be, restricting the maximum length of code for the shared worker. Even if not restricted, the conversion of all the code for long, complicated shared worker to base64 and back on every window load is quite inefficient. As is the indexing of shared workers by extremely long URLs (since you connect to an existing shared worker based on matching its exact URL). So what you can do is (a) start with an initially very minimal shared worker, then use eval() to add the real (potentially much longer) code to it, in response to something like an "InitWorkerRequired" message passed to the first window that opens the worker, and (b) For added efficiency, pre-calculate the base-64 string containing the initial minimal shared-worker bootstrap code.
Here's a modified version of the above example with these two wrinkles added in (also tested and confirmed to work), that runs on both stackoverflow.com and en.wikipedia.org (just so you can verify that the different domains do indeed use separate shared worker instances):
// ==UserScript==
// #name stackoverflow & wikipedia userscript shared worker example
// #namespace stackoverflow test code
// #version 2.0
// #description Demonstrate the use of shared workers created in userscript, with code injection after creation
// #icon https://stackoverflow.com/favicon.ico
// #include http*://stackoverflow.com/*
// #include http*://en.wikipedia.org/*
// #run-at document-end
// ==/UserScript==
(function() {
"use strict";
// Minimal bootstrap code used to first create a shared worker (commented out because we actually use a pre-encoded base64 string created from a minified version of this code):
/*
// ==================================================================================================================================
{
let x = [];
onconnect = function(e)
{
var p = e.source;
x.push(e);
p.postMessage(["InitWorkerRequired"]);
p.onmessage = function(e) // Expects only 1 kind of message: the init code. So we don't actually check for any other sort of message, and page script therefore mustn't send any other sort of message until init has been confirmed.
{
(0,eval)(e.data[1]); // (0,eval) is an indirect call to eval(), which therefore executes in global scope (rather than the scope of this function). See http://perfectionkills.com/global-eval-what-are-the-options/ or https://stackoverflow.com/questions/19357978/indirect-eval-call-in-strict-mode
while(e = x.shift()) onconnect(e); // This calls the NEW onconnect function, that the eval() above just (re-)defined. Note that unless windows are opened in very quick succession, x should only have one entry.
}
}
}
// ==================================================================================================================================
*/
// Actual code that we want the shared worker to execute. Can be as long as we like!
// Note that it must replace the onconnect handler defined by the minimal bootstrap worker code.
var workerCode =
// ==================================================================================================================================
`
"use strict"; // NOTE: because this code is evaluated by eval(), the presence of "use strict"; here will cause it to be evaluated in it's own scope just below the global scope, instead of in the global scope directly. Practically this shouldn't matter, though: it's rather like enclosing the whole code in (function(){...})();
var lastID = 0;
onconnect = function(e) // MUST set onconnect here; bootstrap method relies on this!
{
var port = e.source;
port.onmessage = handleMessage;
port.postMessage(["WorkerConnected",++lastID]); // As well as providing a page with it's ID, the "WorkerConnected" message indicates to a page that the worker has been initialized, so it may be posted messages other than "InitializeWorkerCode"
}
function handleMessage(e)
{
var data = e.data;
if (data[0]==="InitializeWorkerCode") return; // If two (or more) windows are opened very quickly, "InitWorkerRequired" may get posted to BOTH, and the second response will then arrive at an already-initialized worker, so must check for and ignore it here.
// ...
console.log("Message Received by shared worker: ",e.data); // For this simple example worker, there's actually nothing to do here
}
`;
// ==================================================================================================================================
// Use a base64 string encoding minified version of the minimal bootstrap code in the comments above, i.e.
// btoa('{let x=[];onconnect=function(e){var p=e.source;x.push(e);p.postMessage(["InitWorkerRequired"]);p.onmessage=function(e){(0,eval)(e.data[1]);while(e=x.shift()) onconnect(e);}}}');
// NOTE: If there's any chance the page might be using more than one shared worker based on this "bootstrap" method, insert a comment with some identification or name for the worker into the minified, base64 code, so that different shared workers get unique data-URLs (and hence don't incorrectly share worker instances).
var port = (new SharedWorker('data:text/javascript;base64,e2xldCB4PVtdO29uY29ubmVjdD1mdW5jdGlvbihlKXt2YXIgcD1lLnNvdXJjZTt4LnB1c2goZSk7cC5wb3N0TWVzc2FnZShbIkluaXRXb3JrZXJSZXF1aXJlZCJdKTtwLm9ubWVzc2FnZT1mdW5jdGlvbihlKXsoMCxldmFsKShlLmRhdGFbMV0pO3doaWxlKGU9eC5zaGlmdCgpKSBvbmNvbm5lY3QoZSk7fX19')).port;
port.onmessage = function(e)
{
var data = e.data, msg = data[0];
switch (msg)
{
case "WorkerConnected": document.title = "#"+data[1]+": "+document.title; break;
case "InitWorkerRequired": port.postMessage(["InitializeWorkerCode",workerCode]); break;
}
}
})();
I wrote a userscript that runs on the page for a game called TagPro that allows for voice recognition. It listens for key words followed by phrases, and depending on the word it puts the phrase into a different chat channel.
I'm running the script in Chrome's Tampermonkey extension. The speech recognition library I'm using is Annyang.
At the beginning of every game, Chrome confirms that you want to allow the site to use your microphone with a prompt like this:
The problem I'm having is that sometimes, in the middle of a game, the prompt will come up again. It doesn't happen in every game, but when it does happen it usually happens more than once. I haven't noticed any patterns that would make it reproducible. That makes it very hard to diagnose, let alone fix. Is there an error with my script that could be causing this?
// ==UserScript==
// #name TagPro Speech To Text
// #namespace http://www.reddit.com/u/undergroundmonorail
// #description Say a message out loud to say it into chat.
// #include http://tagpro-*.koalabeast.com:*
// #include http://tangent.jukejuice.com:*
// #include http://maptest.newcompte.fr:*
// #include http://justletme.be*
// #license MIT
// #author monorail
// #version 0.2
// ==/UserScript==
(function() {
// https://github.com/TalAter/annyang
// I couldn't figure out how to load it dynamically, so I just copypasted
// the minified version.
// #require works in Firefox, but not Chrome, and this is way easier than
// any alternative I found.
(function(a){"use strict";var b=this,c=b.SpeechRecognition||b.webkitSpeechRecognition||b.mozSpeechRecognition||b.msSpeechRecognition||b.oSpeechRecognition;if(!c)return b.annyang=null,a;var d,e,f=[],g={start:[],error:[],end:[],result:[],resultMatch:[],resultNoMatch:[],errorNetwork:[],errorPermissionBlocked:[],errorPermissionDenied:[]},h=0,i=!1,j="font-weight: bold; color: #00f;",k=/\s*\((.*?)\)\s*/g,l=/(\(\?:[^)]+\))\?/g,m=/(\(\?)?:\w+/g,n=/\*\w+/g,o=/[\-{}\[\]+?.,\\\^$|#]/g,p=function(a){return a=a.replace(o,"\\$&").replace(k,"(?:$1)?").replace(m,function(a,b){return b?a:"([^\\s]+)"}).replace(n,"(.*?)").replace(l,"\\s*$1?\\s*"),new RegExp("^"+a+"$","i")},q=function(a){a.forEach(function(a){a.callback.apply(a.context)})},r=function(){d===a&&b.annyang.init({},!1)};b.annyang={init:function(k,l){l=l===a?!0:!!l,d&&d.abort&&d.abort(),d=new c,d.maxAlternatives=5,d.continuous=!0,d.lang="en-US",d.onstart=function(){q(g.start)},d.onerror=function(a){switch(q(g.error),a.error){case"network":q(g.errorNetwork);break;case"not-allowed":case"service-not-allowed":e=!1,(new Date).getTime()-h<200?q(g.errorPermissionBlocked):q(g.errorPermissionDenied)}},d.onend=function(){if(q(g.end),e){var a=(new Date).getTime()-h;1e3>a?setTimeout(b.annyang.start,1e3-a):b.annyang.start()}},d.onresult=function(a){q(g.result);for(var c,d=a.results[a.resultIndex],e=0;e<d.length;e++){c=d[e].transcript.trim(),i&&b.console.log("Speech recognized: %c"+c,j);for(var h=0,k=f.length;k>h;h++){var l=f[h].command.exec(c);if(l){var m=l.slice(1);return i&&(b.console.log("command matched: %c"+f[h].originalPhrase,j),m.length&&b.console.log("with parameters",m)),f[h].callback.apply(this,m),q(g.resultMatch),!0}}}return q(g.resultNoMatch),!1},l&&(f=[]),k.length&&this.addCommands(k)},start:function(b){r(),b=b||{},e=b.autoRestart!==a?!!b.autoRestart:!0,h=(new Date).getTime(),d.start()},abort:function(){r(),e=!1,d.abort()},debug:function(a){i=arguments.length>0?!!a:!0},setLanguage:function(a){r(),d.lang=a},addCommands:function(a){var c,d;r();for(var e in a)if(a.hasOwnProperty(e)){if(c=b[a[e]]||a[e],"function"!=typeof c)continue;d=p(e),f.push({command:d,callback:c,originalPhrase:e})}i&&b.console.log("Commands successfully loaded: %c"+f.length,j)},removeCommands:function(a){a=Array.isArray(a)?a:[a],f=f.filter(function(b){for(var c=0;c<a.length;c++)if(a[c]===b.originalPhrase)return!1;return!0})},addCallback:function(c,d,e){if(g[c]!==a){var f=b[d]||d;"function"==typeof f&&g[c].push({callback:f,context:e||this})}}}}).call(this);
// The following code is the function for sending a chat message. This is
// how every userscript that touches chat does it. It's almost definitely
// not related to the problem.
var lastMessage = 0;
var chat = function(message, all) {
var limit = 500 + 10;
var now = new Date();
var timeDiff = now - lastMessage;
if (timeDiff > limit) {
tagpro.socket.emit("chat", {
message: message,
toAll: all
});
lastMessage = new Date();
} else if (timeDiff >= 0) {
setTimeout(chat, limit - timeDiff, chatMessage)
}
}
// Code that I wrote begins here.
var team = function(message) { chat(message, 0); };
var all = function(message) { chat(message, 1); };
var group = function(message) {
if (tagpro.group.socket) {tagpro.group.socket.emit('chat', message);}
};
commands = { 'say *message': all,
'team *message': team,
'group *message': group };
annyang.addCommands(commands);
annyang.start();
})();
Chrome's speech recognition implementation simply stops from time-to-time.
To handle this, annyang restarts it (unless it is stopped manually by you or the user). Since your page doesn't use HTTPS, the permission you gave Chrome to use speech recognition on that page doesn't persist, and it asks the user for permission again and again.
This is why it is recommended to use HTTPS whenever using speech recognition on the page.
This was a funny one.
So I unminified annyang.js to find the onend and onerror functions. In the onerror call I console logged the error to get:
Note error: "no-speech".
You see where this is going...
Looking up the W3 spec: https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html#dfn-sre.nospeech
It reads:
"no-speech"
No speech was detected.
Long story short, you're being too quiet.
As for PREVENTING it - dig into annyang.js, particularly the onerror event. Or sing the ABCs repeatedly while you need it on.
You are probably getting the extra prompts due to AJAX loading in iframes for various purposes.
Wrap your code in a frame check:
if (window.top == window.self) {
//-- Only run on the master (top) page...
(function() {
// https://github.com/TalAter/annyang
// I couldn't figure out how to load it dynamically, so I just copypasted
// the minified version.
// #require works in Firefox, but not Chrome, and this is way easier than
// any alternative I found.
(function(a){"use strict";var b=this,c=b.SpeechRecognition||b.webkitSpeechRecognition|| ...
// etc...
}
To eliminate the prompt completely, you have to use a full-blown extension. Then you can use techniques like in this other answer. (But then you also have to hassle with the Chrome store if you want to share the extension easily.)