Hack Forums
[Userscript] How To Make Your Own Userscripts! - Printable Version

+- Hack Forums (https://hackforums.net)
+-- Forum: Tech (https://hackforums.net/forumdisplay.php?fid=444)
+--- Forum: Computing (https://hackforums.net/forumdisplay.php?fid=88)
+---- Forum: Computing Lounge (https://hackforums.net/forumdisplay.php?fid=8)
+---- Thread: [Userscript] How To Make Your Own Userscripts! (/showthread.php?tid=3636683)

Pages: Pages 1 2 3 4


How To Make Your Own Userscripts! - Emylbus - Jul 21, 2013

[Image: E3jJJ.png]

Contents
  1. Introduction
  2. Document Object Model
  3. jQuery
  4. Userscript Metadata Block
  5. Console Logging
  6. Adding CSS Styles
  7. Local Data Storage
  8. Sending Webpage Requests
  9. Easy Examples
  10. More Resources


Userscripts are scripts that increase the functionality of websites. Maybe you want to change the background color of your favorite site or perhaps you want a full blown revamp, userscripts are for you! Throughout this tutorial I will show you everything you need to know to get into making your own userscripts :D

Getting Greasemonkey
First thing's first, you should understand that userscripts are generally run through a script manager such as Greasemonkey. These will let you organize, install, edit, and update userscripts and are absolutely necessary to have if you want to get into this wonderful world full of customization. Click on one of the links below for the correct manager for your browser:
[Image: QhMS4Gh.png]
[Image: NgStvJA.png]

I recommend using Chrome's Tampermonkey, I find that Chrome's developer tools are simply more useful.

Installing Scripts
The largest source of userscripts is, without a doubt, userscripts.org. Once you select the script you wish to install and you go to the install page, your script manager will ask you if you wish to install it. Make sure you review the permissions you're giving the script and press ok.
[Image: vFZyhEK.png]

Starting a New Script
Greasemonkey
1) Select the Greasemonkey button and choose the "New User Script..." option.
[Image: dX1oOJr.png]
2) Enter the relevant information. The only important one right now is the Name, the rest you can easily change later. For a description of what each of these options are, jump to this section of the tutorial.
[Image: asYcraB.png]
3) Simply add your code in the editor that pops up :D
[Image: uwmJ7hT.png]

Tampermonkey
1) Open the Tampermonkey dashboard.
2) Click on the new script button.
[Image: pc55KdQ.png]
3) Simply add your code in the editor that pops up :D
[Image: Gcc5Yps.png]

Writing a New Script
Unfortunately I don't have the time nor space in this tutorial to teach you how to program. Luckily JavaScript is easy enough that if you just tinker with what's out there, you'll quickly learn the ropes. Find a simple userscript and change one thing and see how that makes the output different. What I will do is explain how userscripts work and show you the tools that are available so you can learn and create the next great program.

Good luck gentlemen and gentleladies. I'll see you on the other side!


* - Emylbus - Jul 21, 2013

Document Object Model

The Document Object Model (DOM) is the most commonly used convention for handling and interpreting objects in HTML documents. In DOM, a webpage is broken up into a family-tree where elements within other elements are considered the children of their upper level, or parent, elements:
[img]http://www.w3schools.com/htmldom/htmltree.gif[/img]
Image courtesy of w3schools.com

Here is a 3D representation of the DOM tree in website as complicated as Hack Forums:
[Image: TVmE4Bq.png]

As you can see from the blurry picture, the internet is a complicated place and the DOM convention lets us choose exactly what we are looking for.

Methods
Luckily there are methods built into JavaScript which let us traverse the DOM tree. Below I have listed some I frequently use, but if you want a full list please visit HTML DOM Document Objects | w3schools.
  • document.body
    • Returns the body element of the document.
  • document.getElementsByClassName(someClass)
    • Return the elements with the class name defined by the string someClass.
  • document.getElementById(someID)
  • document.getElementsByName(someName)
  • document.getElementsByTagName(someTag)
  • document.links
  • document.title
    • Returns or sets the title of a webpage.
    • PHP Code

      <?php 
      GM_log
      (document.title); // Prints the page title
      document.title = "A New Title"; // Sets the current page title 
  • document.URL
    • Returns the full URL of the document.

Despite having these tools at our disposal, using this method of DOM traversal is inefficient and often may break certain features of websites. I would strongly recommend, after understanding how DOM works, that you use the jQuery library to quickly and efficiently traverse and modify DOM elements. I will be covering jQuery in the next section.


* - Emylbus - Jul 21, 2013

jQuery

jQuery is an incredibly popular library designed to simplify JavaScript with numerous features. Features include the easy DOM traversal and modification, event handling, effects and animations, and various utilities. When writing userscipts, you'll find jQuery will simplify finding and manipulating the document.

You can include the jQuery into your script with the following @require line:
PHP Code

<?php 
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js 

Here is a list of other verisons of jQuery you may use by simply replacing the version number in the above @require line:
Code
2.0.3, 2.0.2, 2.0.1, 2.0.0, 1.10.2, 1.10.1, 1.10.0, 1.9.1, 1.9.0, 1.8.3, 1.8.2, 1.8.1, 1.8.0, 1.7.2, 1.7.1, 1.7.0, 1.6.4, 1.6.3, 1.6.2, 1.6.1, 1.6.0, 1.5.2, 1.5.1, 1.5.0, 1.4.4, 1.4.3, 1.4.2, 1.4.1, 1.4.0, 1.3.2, 1.3.1, 1.3.0, 1.2.6, 1.2.3

Filtering

Perhaps the most useful part of jQuery that we will be using frequently is the DOM filtering. Faster than doing it manually, you can specify your search by tags, class name, or id name and jQuery will return an array with everything matching your terms. You can then manipulate them however you wish.

Everything in jQuery starts with this syntax (where aString is the string containing your filter terms):
PHP Code

<?php 
$(aString); 

To filter for a certain tag (i.e. the <div> tag), you would simply make your search term "div". If you wished to search by class name, you would prefix your term with a period and get ".someclass". If you wished to sort by id name, you can do "#someid". Here are some examples of search terms and their results:
[Image: YGqAlnV.png]

As you can see, some search terms are more specific than others and you'll want to be as specific as possible when determining what you want to prevent accidentally causing unplanned and unwanted changes.

You can specify a child tags/class/id in addition to their parent, helping limit what results you get back. You do this by simply deliminating the parent and child filter with a space:
[Image: tBBFm9B.png]

There are also things called "selectors" which let you be even more specific with your filtering. However it is not within the scope of this tutorial to explain everything in jQuery, and I will redirect you to the official jQuery documentation for more information: Selectors | jQuery API Documentation

Manipulation

Once you have specified the elements you want to mess with, you can run built in functions to change their behavior. You can add or change attributes, you may change the text inside them, or do anything else you'd like. I can't possibly list all of jQuery's functions, but here are some of the ones I use most frequently:
  • .append(content)
    • Insert content in the form of a string, element, or jQuery object at the end of the set of matched elements defined by your filter.
    • jQuery API doc
  • .attr(attributeName, [value])
    • Set one or more attributes for the matched elements.
    • If you do not include the value, the function will return the attribute specified without changing anything.
    • jQuery API doc
  • .html([astring])
    • If the astring parameter is passed, this function will replace the html of the matched elements. If it is not, then this function will return the html in the matched elements.
    • jQuery API doc
  • .prepend(content)
    • Similar to .append(), however you insert content at the beginning of each matched element.
    • jQuery API doc
  • .remove([selector])
    • This will remove the matched elements. You may include an optional selector to help specify which elements to remove.
    • jQuery API doc
  • .text([astring])
    • Similar to .html(), however you may return or change the text within matched elements depending on whether or not you include the astring parameter.
    • jQuery API doc

You may find a list of all manipulation functions here: Manipulation | jQuery API Documentation.

There are many other functions that may help you learn how to use jQuery that I simply can't get into in this tutorial. I would strongly recommend you check out the full documentation and learn the amazingness that is called jQuery: jQuery API Documentation


* - Emylbus - Jul 21, 2013

Userscript Metadata Block

The Metadata Block is an essential and necessary part for every userscript. Located at the top of every script, this section has important information including the name of the script, the author, and other vital things.

Syntax

The syntax for the block is fairly straight forward: everything is placed into comments at the top of your script.
PHP Code

<?php 
// ==UserScript==
// @key value
// ==/UserScript== 

Imperatives
  • @description
    • A brief summary of what the script does. It will be shown to the user during installation of the script.
    • PHP Code

      <?php 
      // @description This is one awesome tutorial! 
  • @downloadURL
    • Define the URL to download updates if the userscript manager determines there is an update after checking the URL defined by @updateURL.
    • PHP Code

      <?php 
      //@downloadURL https://www.example.com/superscript.user.js 
  • @exclude
    • Define which URLs your script will not be activated on. The @exclude may be used to let you specify sections of a certain website you do not want the code to run on.
    • You may find detailed a more detailed list of rules and tricks for @exclude here: Include and exclude rules - GreaseSpot Wiki
    • PHP Code

      <?php 
      // @exclude https://www.example.com/secrets/* 
  • @grant
  • @icon
    • Define an icon to be displayed in the script management interface. Any image will work, but 32x32px will be displayed best.
    • PHP Code

      <?php 
      // @icon http://www.example.org/icon.png 
  • @include
    • Define what webpages you want your script to be activated on.
    • You may find a detailed list of rules and tricks for @include here: Include and exclude rules - GreaseSpot Wiki
    • PHP Code

      <?php 
      //@include http://www.example.com/* 
  • @match
    • A safer version of @include which sets more strict rules for the * character.
    • You may find a detailed list of rules and tricks for @match here: Match Patterns - Google Chrome.
    • PHP Code

      <?php 
      // @match http://*.example.com/* 
  • @name
    • The name of the script.
    • PHP Code

      <?php 
      // @name My First Script 
  • @namespace
    • @namespace, along with @name, create a unique identifier for the script. This will prevent different scripts with the same name from overwriting eachother. Usually the namespace is often a URL, however it can be any unique value.
    • PHP Code

      <?php 
      // @namespace http://www.example.com/ 
  • @require
    • This will import and download other userscript libraries.
    • PHP Code

      <?php 
      // @require http://www.example.com/example.js 
  • @resource
    • The named resources may be accessed through GM_getResourceText and GM_getResourceURL.
    • PHP Code

      <?php 
      // @resource resourceName http://www.example.com/example 
  • @run-at
    • You may force the script to run either before the page is loaded or after with either document-end or document-start. Default behavior is to run the script after the document has been loaded.
    • PHP Code

      <?php 
      // @run-at document-start 
  • @unwrap
    • Let your script run outside of the sandbox. Only use this for debugging purposes.
    • PHP Code

      <?php 
      // @unwrap 
  • @updateURL
    • Define where the userscript manager will check for updates from. The userscript manager will parse the metadata and determine if there is a newer version.
    • PHP Code

      <?php 
      // @updateURL http://www.example.org/example.meta.js 
  • @version
    • Define the version number of your program. This will be used when determining if there is an update.
    • PHP Code

      <?php 
      // @version 1.0 

Example

PHP Code

<?php 
// ==UserScript==
// @name  HF Enhancement Suite
// @author  Emylbus
// @namespace   http://www.sublyme.net
// @description This userscript defines general enhancement tweaks that apply to the entire HackForums site.
// @include  *hackforums.net/*
// @version  0.32
// @updateURL   http://sublyme.net/site_media/userscripts/HFES.user.js
// @grant  GM_addStyle
// @grant  GM_xmlhttpRequest
// @grant  GM_getValue
// @grant  GM_setValue
// @grant  GM_log
// @require  http://github.com/sizzlemctwizzle/GM_config/raw/master/gm_config.js
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js
// @require  http://sublyme.net/site_media/userscripts/GM_SuperValues.user.js
// ==/UserScript== 



* - Emylbus - Jul 21, 2013

Console Logging

Logging information about what is happening in your script is incredibly important, especially when debugging your program. You can print to your javascript console with GM_log():
PHP Code

<?php 
GM_log
(astring); 

Add CSS Styles

GM_addStyle() lets you insert new CSS into the document by creating a new <style> element in the <head> of the page. Here is an example of it being used:
PHP Code

<?php 
// Joining a list like this is a convenient way to make long strings:
var emyNotice = [
    
".emy_alert {",
    
"background: #33;",
    
"border-top: 1px solid #F4D639;",
    
"border-bottom: 1px solid #F4D639;",
    
"text-align: center;",
    
"padding: 5px 20px;",
    
"font-size: 11px;",
    
"margin-bottom: 15px;}",
    ].
join("");

GM_addStyle(emyNotice); 



* - Emylbus - Jul 21, 2013

Local Data Storage

The GreaseMonkey API provides convenient methods to store data locally on your computer.

Store Values

You can store data with a name->value mapping. The GM_setValue() method provides this functionality:
PHP Code

<?php 
GM_setValue
(name, value); 
Note that the name must be a String while the value may be either a String, Integer, or Boolean. GM_setValue() does not return anything.

Load Values

You can retrieve the data if you know the stored name with GM_getValue(). The syntax for getValue() is as follows:
PHP Code

<?php 
GM_getValue
(name, default); 
The default parameter is essentially the backup in case the name hasn't been stored yet, helping you prevent issues of trying to retrieve things that haven't been set. Note that the name must be a String and the default may be either a String, Integer, or Boolean. GM_getValue() returns the value associated with the name we set with GM_setValue().

List Values

You may retrieve the complete list of stored names with GM_listValues(). Syntax is as follows:
PHP Code

<?php 
GM_listValues
(); 
This function will return an array of strings containing all stored names.

Delete Values

You may delete a saved name->value pair completely with GM_deleteValue():
PHP Code

<?php 
GM_deleteValue
(name); 

Example

Here's a brief example of these features working together:
PHP Code

<?php 
GM_setValue
("owner", "Omniscient"); // Store "owner"->"Omniscient"
GM_setValue("AwesomePerson", "Emylbus"); // Store "AwesomePerson"->"Emylbus"
var aval = GM_getValue("owner", "unknown"); // Store either the value of "owner" or the default string "unknown"
GM_log("The owner is: "+aval); // Print the value stored in aval
GM_log("Here are all of the stored names: "+GM_listValues()); // This should print any stored names: ["owner", "AwesomePerson"]
GM_deleteValue("owner"); // This deletes the "owner" name->value pair.
GM_log("What names are left: "+GM_listValues()); // This should only print ["AwesomePerson"]. 

GM_SuperValue

The biggest issue with the previously stated methods of storing information is that you can only store Strings, Integers, and Booleans. What if you wanted to store an array or a JSON object? Fortunately a 3rd-party library called GM_SuperValue extends the storage capability to allow you to store any type of data.

You can use this by importing the library with the @require metadata:
PHP Code

<?php 
// @require http://userscripts.org/scripts/source/107941.user.js 
I am hosting a mirror which can be accessed with this:
PHP Code

<?php 
// @require http://www.sublyme.net/site_media/userscripts/GM_SuperValues.user.js 

The usage is identical to GM_setValue() and GM_getValue, except with a different name:
PHP Code

<?php 
GM_SuperValue
.set(name, value);
var 
x = GM_SuperValue.get(name, default); 



* - Emylbus - Jul 21, 2013

Sending Webpage Requests

To connect to another webpage we'll be using one of the GreaseMonkey APIs: GM_xmlhttpRequest(). This gmethod is asynchronous which means that it does not pause the execution of the rest of your program while it is working. Once the the script recieves the webpage, it will execute the function defined in the onload argument.

Usage

PHP Code

<?php 
GM_xmlhttpRequest
({
    
method: "GET",
    
url: "example.com",
    
onload: function(response){
  
// Put your code here that will be ran after the page is loaded.
  
GM_log(response.responseText); // Print the returned HTML
    
},
}); 

Arguments
  • binary
    • Boolean parameter and Optional
    • If set to true, your request to the server is sent in binary.
  • context
    • Object parameter and Optional
  • data
    • String parameter and Optional
    • Sent in the body of the request. Used for POST requests.
  • headers
    • Object parameter and Optional
    • Define the headers you wish to send to the server, this may include any header, such as User-Agent (if not specified, the useragent will be whatever your browser uses).
  • method
    • String parameter and Required
    • Define what request method you want to make (i.e. "GET" or "POST")
  • onabort
    • Function parameter and Optional
    • The function defined will be called if the request is aborted. The function will be passed the Response Object as an argument.
  • onerror
    • Function parameter and Optional
    • The function defined will be called if an error occurs while processing the request. The function will be passed the Response Object as an argument.
  • onload
    • Function parameter and Optional
    • The function defined will be called once the request has been successfully completed. The function will be passed the Response Object as an argument.
  • onprogress
    • Function parameter and Optional
    • The function defined will be called when the request progress changes. The function will be passed the Response Object as an argument.
  • onreadystatechange
    • Function parameter and Optional
    • The function defined will be called repeatedly while the request is in progress. The function will be passed the Response Object as an argument.
  • ontimeout
    • Function parameter and Optional
    • The function defined will be called if the request timesout. The function will be passed the Response Object as an argument.
  • overrideMimeType
    • String parameter and Optional
    • Lets you specify the MIME type with the request (i.e. "text/html; charset=ISO-8859-1")
  • password
    • String parameter and Optional
    • Send a password used for authentication purposes.
  • synchronous
    • Boolean parameter and Optional
    • If true, the request becomes synchronous which means your browser will freeze until the request has been completed.
  • timeout
    • Number parameter and Optional
    • Defines the number of milliseconds to wait before terminating the request. Default (or 0) has no timeout.
  • upload
    • Object parameter and Optional
    • Pass an object with optional callback functions to monitor the upload of data. Each is passed the Response Object.
  • url
    • String parameter and Required
    • Defines the URL you send the request to. The URL may be relative or absolute.
  • user
    • String parameter and Optional
    • Used for authentication purposes.

Response Object
All callback functions (onabort, onerror, onload, onprogress, onreadystatechange, and ontimeout) are passed the Response Object which contains information recieved from the server. Depending on when the function is called, the response object will have more data. The following is the different information this object has:
  • context
  • finalUrl
  • readyState
  • responseHeaders
  • responseText
  • status
  • statusText

Progress callbacks also have:
  • lengthComputable
  • loaded
  • total

Examples
These are examples provided from the Grease Monkey API documentation:

Bare Minimum
PHP Code

<?php 
GM_xmlhttpRequest
({
  
method: "GET",
  
url: "http://www.example.com/",
  
onload: function(response) {
    
alert(response.responseText);
  }
}); 

GET request
PHP Code

<?php 
GM_xmlhttpRequest
({
  
method: "GET",
  
url: "http://www.example.net/",
  
headers: {
    
"User-Agent": "Mozilla/5.0",    // If not specified, navigator.userAgent will be used.
    
"Accept": "text/xml"    // If not specified, browser defaults will be used.
  
},
  
onload: function(response) {
    var 
responseXML = null;
    
// Inject responseXML into existing Object (only appropriate for XML content).
    
if (!response.responseXML) {
  
responseXML = new DOMParser()
  .
parseFromString(response.responseText, "text/xml");
    }

    
GM_log([
  
response.status,
  
response.statusText,
  
response.readyState,
  
response.responseHeaders,
  
response.responseText,
  
response.finalUrl,
  
responseXML
    
].join("\n"));
  }
}); 

POST request
PHP Code

<?php 
GM_xmlhttpRequest
({
  
method: "POST",
  
url: "http://www.example.net/login",
  
data: "username=johndoe&password=xyz123",
  
headers: {
    
"Content-Type": "application/x-www-form-urlencoded"
  
},
  
onload: function(response) {
    if (
response.responseText.indexOf("Logged in as") > -1) {
  
location.href = "http://www.example.net/dashboard";
    }
  }
}); 

HEAD request
PHP Code

<?php 
GM_xmlhttpRequest
({
  
url: "http://www.example.com",
  
method: "HEAD",
  
onload: function(response) {
    
GM_log(response.responseHeaders);
  }
}); 



* - Emylbus - Jul 21, 2013

Easy Examples

Example 1 - Replace HF logo

The first thing we'll do is something pretty fun and quick to make: replacing the HF logo with any image of your choice. First things first, we need to figure out exactly where in the code the image is. Open up your developer tools and click on the logo to take us to that part of the HTML:
[Image: jLCEsAy.png]
As always we want to be as specific as possible when defining the jQuery filter, and the first thing that jumps out to me is the "logo" class. So that is what we should set our initial jQuery filter to be:
PHP Code

<?php 
$(".logo"); 
However that's not specific enough for us! If we can also throw another term into that filter to narrow down exactly what we're looking for! Remember that class names are preceeded by a period whereas tag names are not preceeded by anything:
PHP Code

<?php 
$(".logo img"); 
This will limit us to just that the bit of code we're looking for. Now that we have the code we need, we'll need to change the image source. We can do this with the attr() jQuery method:
PHP Code

<?php 
$(".logo img").attr("src","your-pic.jpg"); 

Let's try this out! Don't forget to import the jQuery code with @require!
[Image: tL9oNMn.png]

Let's see what it looks like!
[Image: Xh3ji42.png]

Alright, well the picture doesn't fit very well, but you can always choose your own to use. Lastly let's make sure this code doesn't run on hackforums.net because there is no logo. We can do by either changing the @include parameters in the beginning of the script to:
PHP Code

<?php 
// @include  hackforums.net/*
// @include  hackforums.net/* 
which will only let the script run on those two sites, or we can check the URL in script with a simple conditional which checks whether or not "nsfw." exists in the page's URL:
PHP Code

<?php 
if(document.URL.indexOf('nsfw.') != -1){
    
// 'nsfw.' is not in the URL
    
$(".logo img").attr("src","http://www.mainecoonsofminnesota.com/images/cat_banner.jpg");
} 


Example 2 - Add Custom Links

Next let's get some custom links to our favorite pages. In this case, let's add links to the right of where the link to our Private Messages is. As always we begin by looking at the source code to see what's already being done:
[Image: KlD8gS9.png]
As we can see each link is simply listed and if we wanted to add more we could just add anything we want to the end of that <div>. Note that the <div> in question has a convenient id "panel". Since you're only supposed to have one thing associated with an id, we can specify that in our jQuery filter without having to specify more like we did in the previous example:
PHP Code

<?php 
$("#panel"); 
We can append pretty much anything we want to the end of this <div> with the jQuery .append() method:
PHP Code

<?php 
$("#panel").append("<b>Hello World</b>"); 
If we run this code in a userscript, we'll get this result:
[Image: AO5GZzj.png]
Excellent! Now all we need to do is define our links and append those instead and we'll be in great shape. We'll store the links in variables this time:
PHP Code

<?php 
var myrep, lounge;
myrep = "<a href='https://hackforums.net/reputation.php?uid=[Your UID]'>My Rep</a>";
lounge = "<a href='https://hackforums.net/forumdisplay.php?fid=25'>Lounge</a>"; 
Just append those variables (deliminating them with "|" for consistency's sake), the final code will look like this:
[Image: AGLlIGi.png]
which will look like this in the end:
[Image: GFX3pXF.png]


Example 3 - Replace All Photos

All the way back in example 1, we found out how easy it was to replace the logo with a picture of a cat, let's change EVERY picture >:D

We start off trying to think of what we're trying to do: change every image source to our own and that requires us to first filter for all image tags.
PHP Code

<?php 
$('img'); 
We can see how many elements were tagged by using the GM_log() method we learned about earlier:
PHP Code

<?php 
GM_log
($("img")); 
Sheesh, we can see that there are a TON of images!
[Image: AN2rt90.png]
We can use the attr() method again to change the src attribute of all image tags at once!

PHP Code

<?php 
$("img").attr("src","my-pic.jpg"); 
Here's what my final code looks like:
[Image: DuWn1sq.png]

And my final result is (drum roll please):
[Image: G389XNj.png]
Beautiful.


* - Emylbus - Jul 21, 2013

More Resources
  • Official JavaScript Documentation
    • Don't let this scare you off, the JS docs are one of the best ways to quickly get information about anything with the language.
  • jQuery Documentation
    • An incredibly easy to use and understand resource for all jQuery features.
  • Greasemonkey API manual
    • A full list of API tools built into your userscript manager. Take advantage of them, they are incredibly useful.
  • The Ultimate Compilation of Hack Forum Userscripts by xadamxk
    • The most comprehensive list of Hack Forums specific userscripts.
    • Find some scripts you like from here and tinker with them. Learning through practicing and exploring is one of the best ways to learn how to program.



* - Emylbus - Jul 24, 2013

Sheesh that was a ton of information to throw you at once. Remember this isn't a time for resting your laurels, your adventure is just beginning! I hope to see great things from you all, good luck!


* - 'Snorlax - Jul 25, 2013

Incredibly detailed guide, Emylbus! Good job making this for this community, because a lot of people have been wondering how to make these userscripts!
Very detailed, very nice layout and a good read! Thanks for making and sharing this!


* - vitafortis - Jul 25, 2013

Great thread mate, looks like you put a lot of work into it. 5 Star from me.


* - Sleazy P. Martini - Jul 25, 2013

Looks good, Will read it all when i'm not busy.

Good job.


* - JustCraig - Jul 25, 2013

Thanks for this man! Awesome tutorial!


* - +Awesome - Jul 25, 2013

Very detailed and superb stuff! Thanks for posting. I saw this building! ;)