Saturday, December 29, 2012

Using Gazelle to write a Simple Greasemonkey/Tampermonkey Script


In this post I will demonstrate using Gazelle to write a Grease/Tampermonkey script to replace certain IP addresses in gmail's login history page with names, if the IP address is known to belong to a particular location.

I'm using Tampermonkey on Chrome, but I understand that the Greasemonkey script should be similar. Your mileage may vary.

Setting Up Gazelle

Gazelle is/aspires to be easy to set up.

You'll need GNU Emacs, which hosts the entire project.

You'll also need the Gazelle repository repository and shadchen-el.

you@home:~$ cd emacs-code # or wherever you put your emacs stuff
you@home:~/emacs-code$ git clone https://github.com/VincentToups/shadchen-el.git
you@home:~/emacs-code$ git clone  https://github.com/VincentToups/gazelle.git

Then, in your emacs configuration, either .emacs.d/init.el or .emacs add lines to the effect of:

(push "~/emacs-code/shadchen-el/" load-path)
(push "~/emacs-code/gazelle/" load-path)

If you want to use Gazelle you then must, at some point, (require 'gazelle).  It is very much worth byte-compiling all the Gazelle emacs lisp files, because they depend on a fair amount of macro magic and byte compiling should improve performance significantly.

What Do We Want to Do?

Gmail provides a handy feature that allows you to see what IP addresses are currently logged into your gmail account. It is the Details link at the very bottom right of one's inbox. It pops up a page that looks like this:



What we'd like to do is look at each of the td elements in the page, and if the element contains a description of an IP address we know, we want to replace the text of that element with a more descriptive name.  Here I have blacked out the IP addresses, but the lines are something to the effect of "* 000.00.00.000 (Geographic Location)".

This is a pretty simple task usinq something like jQuery, so the idea here is to show how it works in Gazelle.

Writing the Script

Create a file called name-ips-in-gmail.gazelle and put it someplace convenient. Tampermonkey scripts begin with a comment header that tells the system a few things about the script. Our script is going to use Jquery, and so the most notable line is the @require form. Comments are introduced in Gazelle via the comment form, which takes strings.

(comment "==UserScript=="
         "@name       Gazelle Example/IP Namer"
         "@namespace  http://use.i.E.your.homepage/"
         "@version    0.1"
         "@description  enter something useful"
         "@match      "
         "@require    https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"
         "@copyright  2012+, You"
         "==/UserScript==")

Now, it will be useful in this script to get the text of an html element but without the text of any sub-elements. The behavior of .text() in Jquery is the latter, rather than the former. So we define a function to do what we want:

(define (just-text element)
  (.. ($ element)
      (clone)
      (children)
      (remove)
      (end)
      (text)))

This function simply clones the element in question, removes all of its children, and then gets the text.
We will want to replace the contents of an element if it contains a particular IP address, so we need a predicate function to test for strings containing some substring.

(define (string-contains? s substring)
  (if (_> (.. s (index-of substring)) 0)
      true
    false))

In this case, we use the prim operation _> because we haven't included a function definition of >. If we want that we could write:

(define (> a b)
    (_> a b))

(See footnote 1)
When using Gazelle with modules, the hooves/operator-functions module contains function or macro definitions for the common operators (where appropriate). But here its not worth the trouble.
Now the meat of the script, a function which will be applied to each td element on the page, and which will test to see whether it should be replaced with a known location.

(define (each-fun index element)
  (var text (just-text element))
  (if (string-contains? text "000.00.000.000")
      (.. ($ element) (text "Location 1")))
  (if (string-contains? text "00.000.00.000")
      (.. ($ element) (text "Location 2")))
  (if (string-contains? text "00.00.000.000")
      (.. ($ element) (text "Location 3"))))

Obviously you will need to replace the IPs above with meaningful ones. You could also make an easier to extend implementation.

And finally we perform the action:

(.. 
 ($ "td") (each 
           each-fun))

Once we have all this in a file, we can say:

M-x gz:transcode-file

And we get this result in name-ips-in-gmail.js:
// ==UserScript==
// @name       Gazelle Example/IP Namer
// @namespace  http://use.i.E.your.homepage/
// @version    0.1
// @description  enter something useful
// @match      
// @require    https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js
// @copyright  2012+, You
// ==/UserScript==
;
var justText = function (element)  {
  return ($(element).clone().children().remove().end().text());
  };
var stringContainsPredicate = function (s, substring)  {
  return (((((s.indexOf(substring))>0))?(true) : (false)));
  };
var eachFun = function (index, element)  {
  var text = justText(element);
  ((stringContainsPredicate(text,
"***.**.***.***"))?($(element).text("Location 1")) : (undefined));
  ((stringContainsPredicate(text,
"**.***.**.***"))?($(element).text("Location 2")) : (undefined));
  return (((stringContainsPredicate(text,
"**.**.***.***"))?($(element).text("Location 3")) : (undefined)));
  };
$("td").each(eachFun);

Now simply create a new User Script in Grease or Tampermonkey and copy the .js file into it and you should be done. Congratulations, you've written your first bits of Gazelle code!

Footnote 1:

Since Gazelle is brand new, it bears discussing exactly what something like this:


(define (> a b)
    (_> a b))


really means.  This form introduces a function called `>` which, in its body, references the Javascript operator of the same name by using the symbol `_>`.   This is expanded to Javascript code like:

var greaterThan = function(a,b){ return a>b }

In Gazelle we can refer to the function `greaterThan` with the symbol `>`.  We could also restrict ourselves to uses of the operator `_>`, but the function is probably preferred, because we can map, reduce, filter, or apply with it.  Frankly, I'll never understand why modern programming languages keep to the idea that one should have these second class citizen "operators" floating around.

2 comments:

John Cowan said...

I'm saying this out of total laziness, because I seem to have no desire to test it myself. But it would be cool to know if Gazelle worked correctly under Guile 2.0's implementation of Elisp. There aren't so very many Elisp applications that don't depend on the availability of Emacs-specific data structures. You might give it a try if you have a few hours to play around with Guile; you can switch the REPL with ",language elisp" and back with ",L scheme".

J.V. Toups said...

Maybe I'll get around to it some time, but its a bit crazy, even for me. I'd rather just port it to Common Lisp and be done with it.