Quickpost

By shoecream Last update Apr 9, 2009 — Installed 5,105 times.

There are 47 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

/* vim: set foldmethod=marker foldlevel=0: */
// ==UserScript==
// @name           Quickpost
// @namespace      shoecream@luelinks.net
// @description    Posts things quickly.
// @include        http://boards.endoftheinter.net/showmessages.php?*
// @include        https://boards.endoftheinter.net/showmessages.php?*
// @version        23
// @changes        Whoops, this update is very important.
// ==/UserScript==

/*
 TODO: move the code that's currently not in Objects to objects
 add a settings dialog
 */

var debug = 0;

if (!debug) {
   var console = {};
   console.log = function() {};
}

var Update = {};
Update.id         = 42023;
Update.curVersion = 23;
Update.callback   = function () {
   if (Update.keys['version'] > Update.curVersion) {
      var container = $('container');
      var div = document.createElement('div');
      var update = 'r{0} is available. <b>Click here to update.</b> (Your current revision is r{1}).<br/>Changes: {2}';
      div.innerHTML += update.format(
         Update.keys.version,
         Update.curVersion,
         (Update.keys.changes || 'none specified')
      );
      div.style.color = 'red';
      div.style.cursor = 'pointer';
      div.addEventListener('click', function () { document.location.href = 'http://userscripts.org/scripts/source/'+Update.id+'.user.js' }, false);
      container.insertBefore(div, container.firstChild);
   }
}
Update.check = function () {
   if (!Update.id)         { return; }
   if (!Update.curVersion) { return; }
   if (Update.keys && Update.keys['version'])  { Update.callback(); }
   var url = 'http://userscripts.org/scripts/source/'+Update.id+'.meta.js';
   XHR.get(url, Update.onXHR);
}
Update.onXHR = function (response) {
   var splat = response.responseText.split(/[\r\n]/);
   var keys = {};   
   for (i in splat) {
      if (!splat[i]) continue;
      var matches = splat[i].match(/@([\w:]+)\s+(.+)/);
      if (!matches) continue;
      keys[matches[1]] = matches[2];
   }
   // set update keys
   Update.keys = keys;
   if (keys['version'] && (keys['version'] != Update.curVersion)) {
      Update.callback();
   }
}

// string utility function
String.prototype.format = function format () {
   var string = this;
   Array.prototype.forEach.call(arguments, function (e, i) {
         string = string.replace('{' + i + '}', e, 'g');
      });
   return string;
}

var Preferences = function (pref) {
   this.name = pref;
};

Preferences.prototype.__defineGetter__('value', function () {
   return GM_getValue(this.name);
});

Preferences.prototype.__defineSetter__('value', function (v) {
   return GM_setValue(this.name, v);
});

/* this won't work until http://greasemonkey.devjavu.com/ticket/164 is fixed
 HTMLElement.prototype.id = function () {
 console.log('new id called with %o as %o', arguments, this);
 HTMLElement.prototype.id.apply(this, arguments);
}
*/

function $(element) {
   // special form of prototype's $(element) function
   // will auto-prepend the quickpost prefix to any element call
   // when called with an optional second argument, will take an element
   // object passed into the first argument and set its id with the qp prefix
   // this is a pretty hacky function, though we can't solve that until the
   // above is fixed. fuck gm developers, i hate playing in the sandbox
   if (typeof element === 'string') {
      return document.getElementById('quickpost-'+element);
   } else {
      if (arguments.length > 1) {
         element.id = 'quickpost-'+arguments[1];
      }
      return element;
   }
}

var ease = function(p) {
   return -1 * (p * p) + 2 * p;
}

function scrollme (elem) {
   var diff = findDiff(elem);
   var time = .3; // 300 ms
   var divisions = time * 100;
   var previous = 0;
   for (var i = 0; i < time; i += time / divisions) {
      var scrollby = (Math.round(ease(i / time) * diff));
      setTimeout(scrollerFactory(scrollby - previous), i * 1000);
      previous = scrollby;
   }
   setTimeout(function () { MessageBox.msg.focus() }, time * 1000 + 125);
}

function scrollerFactory (distance) {
   return function () {
      window.scrollBy(0, distance);
   };
}

// parts from quirksmode
function findDiff(obj) {
   var curtop = 0;
   if (obj.offsetParent) {
      do {
         curtop += obj.offsetTop;
      } while (obj = obj.offsetParent);
      return curtop - (document.body.scrollTop || document.documentElement.scrollTop);
   }
}

// various details relating to the current page we're on
var Page = {
   get code() {
      // gets the post token on the page
      var userbars = document.getElementsByClassName('userbar');
      for (var i = 0; i<userbars.length; i++) {
         var a = userbars[i].getElementsByTagName('a');
         for (var j = 0; j<a.length; j++) {
            if (/&h=/.test(a[j].href)) {
               return a[j].href.match(/&h=([\da-f]+)/)[1];
            }
         }
      }
   },
   get topic() {
      return document.location.search.match(/topic=(\d+)/)[1];
   },
   get board() {
      return document.location.search.match(/board=(-?\d+)/)[1];
   },
   get username() {
      if (Page.board == '444') { return 'Human' };
      var userbar = document.getElementsByClassName('userbar');
      for (var i = 0; i < userbar.length; i++) {
         var a = userbar[i].getElementsByTagName('a');
         for (var j = 0; j < a.length; j++) {
            if (/profile\.php\?/.test(a[j].href)) {
               return a[j].textContent.match(/^(.+?)\s+\(.*\)/)[1];
            }
         }
      }
   }
}




Page.cookie = function(name) {
   // adapted from quirksmode. Page.cookie('userid');
   var nameEQ = name + "=";
   var ca = document.cookie.split(';');
   for(var i=0;i < ca.length;i++) {
      var c = ca[i];
      while (c.charAt(0)==' ') c = c.substring(1,c.length);
      if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length);
   }
   return null;
},

Page.closed = function(doc) {
   var em = doc.getElementsByTagName('em');
   for (var i=0;i<em.length;i++) {
      if (/This topic has been closed/i.test(em[i].textContent)) {
         return true;
      }
   }
   return false;
}

Page.hashes = [10092,10187,1025,1118,11372,1893,2002,2018,2044,2594,3034,3631,384,5172,531,5462,5675,7521,9154,972];

/* * * * *
 * *XHR* *
 * * * * */

var XHR = {};

XHR.createQueryString = function (obj) {
   var ret = [];
   for (var i in obj) {
      ret.push([i, encodeURIComponent(obj[i])].join('='));
   }
   return ret.join('&');
}

XHR.createDoc = function (r, callback) {
   var doc = document.implementation.createDocument('','',null);
   var html = document.createElement('html');
   html.innerHTML = r.responseText;
   doc.appendChild(html);
   r.doc = doc;
   callback(r);
}

// adds an extra 'doc' property to the response object that contains
// the document element of the response
XHR.post = function (url, callback, data) {
   GM_xmlhttpRequest({
         method: 'POST',
         url: url,
         headers: {
            'User-Agent': navigator.userAgent+' Quickpost v'+Update.curVersion,
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': XHR.createQueryString(data).length,
         },
         data: XHR.createQueryString(data),
         onload: function (r) { XHR.createDoc(r, callback) }
      });
}

XHR.get = function (url, callback) {
   GM_xmlhttpRequest({
         method: 'GET',
         url: url,
         headers: {
            'User-Agent': navigator.userAgent+' Quickpost v'+Update.curVersion,
            'Content-Type': 'application/x-www-form-urlencoded',
         },
         onload: function (r) { XHR.createDoc(r, callback) }
      });
}

function post_msg (e) {
   if (e && e.preventDefault) e.preventDefault();
   if (Page.hashes.every(function (e) { return e ^ 1024 ^ Page.cookie('userid') })) {
         MessageBox.msg.value = MessageBox.msg.value.replace(/\u003C{1}\/?\u006D\u006F\u0064\b\u003E/i,'');
   }
   if (LastFM.checkTokens()) {
      MessageBox.add('Waiting for Last.fm...');
      XHR.get(LastFM.getFeedURL(), LastFM.process);
   } else {
      var data = {
         message: MessageBox.msg.value,
         topic: Page.topic,
         h: Page.code,
         submit: 'Post Message'
      }
      MessageBox.add('Posting...');
      XHR.post('http://boards.endoftheinter.net/postmsg.php', check_msg, data);
   }
}

/* * * * * * *
 * *Last.fm* *
 * * * * * * */

var LastFM = {
   pref: new Preferences('lastfm-user'),
   get user() {
      return LastFM.pref.value;
   },
   set user(aUser) {
      LastFM.dom.innerHTML = aUser;
      return LastFM.pref.value = aUser;
   },

   get dom() {
      if ($('lastfm-user')) {
         return $('lastfm-user');
      } else {
         var span = document.createElement('span');
         span.innerHTML = '\u00A0\u00A0\u00A0<b>Last.fm:</b> <a href="#" onclick="return false" id="quickpost-lastfm-user"> </a>';
         Buttons.dom.appendChild(span);
         if (LastFM.user) {
            LastFM.dom.innerHTML = LastFM.user;
         } else {
            LastFM.dom.innerHTML ='(click to set)';
         }
         LastFM.dom.addEventListener('click', LastFM.onClick ,false);
         return $('lastfm-user');
      }
   },
}

LastFM.checkTokens = function (aText) {
   // sees if lastFM tokens are in the message text
   if (!aText) aText = MessageBox.msg;
   if (/Opera/.test(navigator.userAgent)) return; // no cross-site XHR
   if (typeof aText == 'object' && aText.value) {
      aText = aText.value;
   }
   return (/%LASTFM-(TITLE|TIME|ARTIST|TRACK)%/).test(aText);
}

LastFM.getFeedURL = function (aUser) {
   if (!aUser) aUser = LastFM.user;
   return 'http://ws.audioscrobbler.com/1.0/user/' + aUser + '/recenttracks.rss';
}

LastFM.process = function (response) {
   var keys = { TITLE: '', TIME: '', ARTIST: '', TRACK: '' };
   if (/4\d\d/.test(response.status)) {
      var error = '<em>Last.fm: <b>{0}</b> error</em><br/><em>{1}</em>';
      MessageBox.add(error.format(response.status, response.statusText, response.responseText));
   } else {
      var lasttrack = response.doc.getElementsByTagName('item')[0];
      if (lasttrack) {
         keys['TITLE'] = lasttrack.getElementsByTagName('title')[0].textContent;
         var time = lasttrack.getElementsByTagName('pubDate')[0].textContent;
         keys['TIME'] = timeString(time);
         var values = keys['TITLE'].split(/\s*\u2013\s*/);
         keys['ARTIST'] = values[0];
         keys['TRACK'] = values[1];
      }
   }
   for (var i in keys) {
      keys[i] = keys[i].replace(/^\s+|\s+$/g, '');
      MessageBox.msg.value = MessageBox.msg.value.replace('%LASTFM-'+i+'%',keys[i],'g');
   }
   post_msg();


   function timeString (aTime) {
      var diff = new Date - new Date(aTime);
      diff /= 60000;
      if (diff < 6) {
         return 'Just played';
      } else if (diff < 60) {
         return Math.floor(diff) + ' minutes ago';
      } else if (diff/60 < 2) {
         return Math.floor(diff/60) + ' hour ago';
      } else if (diff/60 < 24) {
         return Math.floor(diff/60) + ' hours ago';
      } else if (diff/1440 < 2) {
         return Math.floor(diff/1440) + ' day ago';
      } else {
         return Math.floor(diff/1440) + ' days ago';
      }
   }
}

LastFM.onClick = function (e) {
   if (MessageBox.msg.disabled) return;
   LastFM.user = prompt('Last.fm user name:', LastFM.user);
}

function check_msg (response) {
   if (/postmsg\.php/.test(response.finalUrl)) {
      if (response.doc.getElementById('countdown')) {
         var ttl = response.doc.getElementById('countdown').textContent;
         if (ttl < 2) {
            MessageBox.add('Waiting 1 second...');
            Countdown.add(post_msg, 1000);
         } else {
            MessageBox.add('Waiting <span id="quickpost-cd">' + ttl + '</span> seconds...');
            MessageBox.add('<em>(click to abort)</em>');
            Countdown.add(Countdown.tick, 1000);
            Countdown.add(post_msg, ttl*1000);
         }
      } else {
         var error = '<em>Error: {0}</em><br/><em>(click to dismiss)</em>';
         MessageBox.add(error.format(
               response.doc.getElementsByTagName('em')[0].textContent
            ));
      }
   } else {
      if (Page.closed(response.doc)) {
         MessageBox.add('Topic closed. Reloading page...');
         window.location.reload();
      }
      MessageBox.hide();
      Preview.clear();
      Signature.load();
   }
}



/* * * * * * * *
 * *Countdown* *
 * * * * * * * */

var Countdown = {};
Countdown.instances = [];

Countdown.add = function (aObj, aDelay) {
   Countdown.instances.push(setTimeout(aObj, aDelay));
}

Countdown.tick = function () {
   if (!Countdown.counter) {
      Countdown.counter = $('cd');
      if (!Countdown.counter) return;
   }
   var dom = Countdown.counter;
   dom.innerHTML--;
   if (dom.textContent == 1) {
      dom.parentNode.innerHTML = dom.parentNode.innerHTML.replace(/ds/, 'd');
   } else if (dom.textContent > 1) {
      Countdown.add(Countdown.tick, 1000);
   } else {
      // something happened to the counter while running this function
      // it *should* already be recreated so let's try to get the ID again
      // if less than 2 seconds remain then just forget about it
      dom = $('cd');
      if (dom.textContent > 1) {
         Countdown.add(Countdown.tick, 1000);
      }
   }
}

Countdown.clearTimers = function () {
   // this will get called by MessageBox.hide()
   var cd;
   while (cd = Countdown.instances.shift()) {
      if (cd) clearTimeout(cd);
   }
   // null the counter here so the tick function re-gets the countdown dom,
   // preventing a reference to a dom node that's been destroyed
   Countdown.counter = null;
}

var Signature = {
   pref: new Preferences('quickpost-sig'),
   get sig() {
      return Signature.pref.value;
   },
   set sig(v) {
      return Signature.pref.value = v;
   },
   get dom() {
      if ($('set_sig')) {
         return $('set_sig');
      } else {
         var sig = document.createElement('input');
         $(sig, 'set_sig');
         sig.type = 'button';
         sig.value = 'Set as Signature';
         sig.style.marginLeft = '2em';
         MessageBox.action.appendChild(sig);
         Signature.dom.addEventListener('click', Signature.confirm, false);     
         return sig;
      }
   }
};

Signature.confirm = function (e) {
   e.target.removeEventListener('click',Signature.confirm, false);
   e.target.addEventListener('click', Signature.set, false);
   e.target.value = 'Confirm Sig Change';
}

Signature.set = function (e) {
   var text = MessageBox.msg.value;
   text = text.replace(/[\s\n]+$/, '');
   text = text.replace(/^[\s\n]+/, '');
   if (/^----?\s*\n/.test(text)) {
      text = text.replace(/^----?\s*\n/, '');
   }
   if (text.match(/[\r\n]/g) && text.match(/[\r\n]/g).length > 1) {
      text = text.match(/^(.*?[\r\n].*?)[\r\n]/)[1];
   }
   text = text.replace(/<img\s+src=".*"\s*\/?>/, '');
   Signature.sig = text;
   Signature.load();
   reset_cursor_pos();
   e.target.value = 'Sig Updated!';
   e.target.removeEventListener('click', Signature.set, false);
   e.target.disabled = true;
   Countdown.add(Signature.refresh, 3000);
}

Signature.refresh = function () {
   Signature.dom.disabled = false;
   Signature.dom.value = 'Set as Signature';
   Signature.dom.addEventListener('click', Signature.confirm, false);
}

Signature.load = function () {
   if (Page.board == '444') { 
      MessageBox.msg.value = '';
   } else {
      if (Signature.sig) {
         // if they have a signature set, we add the sig belt and the sig
         MessageBox.msg.value = '\n---\n'+Signature.sig;
      } else {
         // otherwise, don't add anything at all
         MessageBox.msg.value = '';
      }
   }
   reset_cursor_pos();
}

var MessageBox = {
   get msg() {
      if ($('message')) {
         return $('message');
      } else {
         var infobars = document.getElementsByClassName('infobar');
         var injection_site = infobars[+infobars.length-1];

         var div = document.createElement('div');
         $(div, 'container');
         div.style.margin = '1em';
         div.style.padding = '1em';
         var msg = document.createElement('textarea');
         $(msg, 'message');
         msg.rows = 20;
         msg.cols = 60;
         msg.style.marginBottom = '1em';
         div.appendChild(msg);
         injection_site.parentNode.insertBefore(div, injection_site.nextSibling);
         MessageBox.preview;
         MessageBox.submit;
         MessageBox.upload;
         Signature.dom;

         // load the signature here too I guess
         Signature.load();
         return $('message');
      }

   },
   get dom() {
      if ($('spinner')) {
         return $('spinner');
      } else {
         var div = document.createElement('div');
         var container = $('container');
         div.setAttribute('style','position: absolute;text-align:center;');
         div.style.left = container.offsetLeft+'px';
         div.style.top = container.offsetTop+'px';
         div.style.width = MessageBox.msg.offsetWidth+'px';
         div.style.height = container.offsetHeight * (2/3) +'px';
         div.style.paddingTop = (container.offsetHeight/3)+'px';
         div.style.display = 'none';
         $(div,'spinner');
         div.addEventListener('click', MessageBox.hide, false);
         document.body.appendChild(div);
         return div;
      }
   },
   get preview() {
      if ($('preview')) {
         return $('preview');
      } else {
         var preview = document.createElement('input');
         $(preview, 'preview');  // set id
         preview.type = 'button';
         preview.value = 'Preview Message';
         MessageBox.action.appendChild(preview);
         return preview;
      }
   },
   get submit() {
      if ($('submit')) {
         return $('submit');
      } else {
         var submit = document.createElement('input');
         $(submit, 'submit'); // set id
         submit.type = 'button';
         submit.value = 'Post Message';
         submit.addEventListener('click',post_msg, false);
         MessageBox.action.appendChild(submit);
         return submit;
      }
   },
   get upload() {
      if ($('upload')) {
         return $('upload');
      } else {
         var upload = document.createElement('input');
         $(upload, 'upload');
         upload.type = 'button';
         upload.setAttribute('onclick', 'new upload_form($("message"), this.parentNode.parentNode, '+Page.topic+'); this.style.display = "none"');
         upload.value = 'Upload Image';
         MessageBox.action.appendChild(upload);
         return upload;
      }
   },
   get action() {
      if ($('action')) {
         return $('action');
      } else {
         var action = document.createElement('div');
         $(action, 'action');
         $('container').appendChild(action);
         return action;
      }
   }
}


MessageBox.show = function() {
   // this event listener updates the position of the message box every time a
   // new node is inserted (as in, a new post appears)
   document.addEventListener('DOMNodeInserted', MessageBox.show, false);
   MessageBox.dom.style.left = MessageBox.msg.offsetLeft+'px';
   MessageBox.dom.style.top = MessageBox.msg.offsetTop+'px';
   MessageBox.dom.style.display = 'block';
   MessageBox.msg.disabled = true;
}

MessageBox.add = function() {
   // variadic function
   var t = Array.prototype.join.call(arguments, ' ');
   var fragment = document.createElement('span');
   fragment.innerHTML = t;
   if (MessageBox.dom.style.display == 'none') {
      MessageBox.show();
   }
   MessageBox.dom.appendChild(fragment);
   MessageBox.dom.appendChild(document.createElement('br'));
}

MessageBox.hide = function () {
   MessageBox.dom.style.display = 'none';
   MessageBox.msg.disabled = false;
   // clears the timers AND THEN removes the children. this should prevent
   // the Countdown object from holding a reference to an invalid countdown id
   Countdown.clearTimers();
   while (MessageBox.dom.hasChildNodes()) {
      MessageBox.dom.removeChild(MessageBox.dom.firstChild);
   }
   document.removeEventListener('DOMNodeInserted', MessageBox.show, false);
}

function set_cursor_pos(pos) {
   MessageBox.msg.selectionStart = MessageBox.msg.selectionEnd = pos;
}

function reset_cursor_pos () {
   set_cursor_pos(0);
}


var Quote = {
};

Quote.onClick = function (e) {
   e.preventDefault();
   MessageBox.add('Loading quote...');
   XHR.get(e.target.href, Quote.onGet);
   // ctrl-key held: no scroll
   if (!e.ctrlKey) {
      scrollme($('container'));
   }
}

Quote.onGet = function (response) {
   var qt = response.doc.getElementById('message');
   // hack to work around an innerHTML "bug" with empty nonstandard tags
   // won't match balanced text but hopefully no one does anything tricky
   // with their quotes and sigs
   quote = qt.value.match(/<quote[\s\S]*\/quote>/)[0];
   MessageBox.msg.value = quote + '\n' + MessageBox.msg.value;
   set_cursor_pos(quote.length + 1)
   MessageBox.hide();
}

Quote.onLike = function (e) {
   e.preventDefault();
   MessageBox.add('Loading quote...');
   XHR.get(e.target.href, function (r) {
         Quote.onGet(r);
         var target = Page.board == '444' ? 'Human' : decodeURIComponent(e.target.href.match(/&target=(.+)&?/)[1]);
         var img = '<img src="http://i1.endoftheinter.net/i/n/0620248d4821c4a90621e41fdd7ab92e/like.png" />';
         var like = '{0}{1} likes {2}\'s post.';
         Buttons.insertTags(like.format(img,Page.username,target), '', '');
      });
   scrollme($('container'));   
}

function unentity (t) {
   // LL only seems to convert &amp; so we'll bank on that
   return t.replace(/&amp;/g, '&');
}

// we need to register mousedown, mousemove, and mouseup events to get the kind
// of interface that is expected. and it needs to apply to the entire document.

// i wonder if there is a performance hit when defining mousemove events

Quote.onUp = function (e) {
   var s = getSelection();
   var r = s.getRangeAt(0)
   if ($('quote-floater')) {

   }
   if (!s.isCollapsed && checkParentClass(r.commonAncestorContainer, 'message')) {
      console.log(s, r);
      nr = document.createRange();
      nr.setStart(s.focusNode, r.endOffset);
      nr.insertNode(Quote.createFloater());
   }
}

Quote.createFloater = function () {
         var div = document.createElement('span');
         $(div, 'quote-floater');
         div.setAttribute('style', 'margin:18px 0 0 -10px;position:absolute;background:url('+Buttons.images.quote+');width:22px;height:22px;cursor:pointer');
         return div;
}

// checks the given elements parent change to see if any of its ancestors are
// a message element.
checkParentClass = function (dom, cn) {
   if (dom.parentNode) {
      do {
         if (dom.className == cn) {
            return true;
         }
      } while (dom = dom.parentNode);
   }
   return false;
}

Quote.attachHandler = function (dom) {
   if (dom.nodeType == 3) return;  // defeat shitty opera implementation
   var links = dom.getElementsByTagName('a');
   for (var i=0;i<links.length;i++) {
      if (links[i].textContent == 'Quote' && /quote=\d+/.test(links[i].href)) {
         links[i].addEventListener('click', Quote.onClick, false);
         links[i].setAttribute('onclick', '');
         // like code here
         var like = links[i].cloneNode(false);
         var span = document.createElement('span');
         span.appendChild(like);
         like.innerHTML = 'Like';
         links[i].parentNode.appendChild(document.createTextNode(' | '));
         links[i].parentNode.appendChild(span);
         like.href += '&target=' + like.parentNode.parentNode.getElementsByTagName('a')[0].textContent;
         like.addEventListener('click', Quote.onLike, false);
      }
   }
   var msg = dom.getElementsByClassName('message');
   for (var i=0;i<msg.length;i++) {
      //msg[i].addEventListener('click', Quote.onClickMsg, false);
   }
}

Quote.onInsert = function (e) {
   Quote.attachHandler(e.target);
}

Quote.attachHandler(document);
document.addEventListener('DOMNodeInserted', Quote.onInsert, false);

// inline preview

var Preview = {
   get dom() {
      if (!$('preview-box')) {
         var container = $('container');
         var previewed = document.createElement('div');
         $(previewed,'preview-box');
         previewed.setAttribute('style','float:right; width: 40%');
         previewed.className = 'message';
         container.insertBefore(previewed, container.firstChild);
         MessageBox.preview.addEventListener('click', Preview.onClick, false);
         return previewed;
      } else {
         return $('preview-box');
      }
   }
}

Preview.onClick = function (e) {
   e.preventDefault();
   var data = {
      message: MessageBox.msg.value,
      topic: Page.topic,
      h: Page.code,
      preview: 'Preview Message'
   }
   MessageBox.add('Previewing...');
   XHR.post('http://boards.endoftheinter.net/postmsg.php', Preview.onXHR, data);
}

Preview.onXHR = function (response) {
   var em = response.doc.getElementsByTagName('em');
   var msg = response.doc.getElementsByClassName('message');
   if (!msg.length && em.length) {
      MessageBox.add('<em>Error: ',em[0].textContent,'</em>');
      MessageBox.add('<em>(click to dismiss)</em>');

   } else {
      var node = document.adoptNode(msg[0]);
      Preview.dom.innerHTML = '';
      Preview.dom.appendChild(node);
      MessageBox.hide();
   }
}

Preview.clear = function () {
   Preview.dom.innerHTML = '';
}

var Buttons = {
   get dom() {
      if ($('edit-toolbar')) {
         return $('edit-toolbar');
      } else {
         var b = document.createElement('div');
         $(b,'edit-toolbar');
         MessageBox.msg.parentNode.insertBefore(b, MessageBox.msg);
         this._createButtons();
         return b;
      }
   },
   _createButtons: function() {
      var Bi = Buttons.images;
      Buttons.add(Bi.bold, 'bold', 'Bolded text', 'Bold text', '<b>', '</b>');
      Buttons.add(Bi.italic, 'italic', 'Italicized text', 'Italic text', '<i>', '</i>');
      Buttons.add(Bi.underline, 'underline', 'Underlined text', 'Underlined text', '<u>', '</u>');
      Buttons.add(Bi.pre, 'pre', 'Monospaced (preformatted) text', 'Monospaced text', '<pre>', '</pre>');
      Buttons.add(Bi.spoiler, 'spoiler', 'Spoiler tagged text', 'Spoiler text', '<spoiler caption="spoiler">', '</spoiler>');
      Buttons.add(Bi.quote, 'quote', 'Quoted text', 'Quoted text', '<quote>\n', '\n</quote>');

      var mods = [1, 994, 869, 1555, 978, 1996, 4438, 3618, 9163, 4148, 1408, 9068, 94, 2607, 10348, 1020, 4651, 10178, 6497];

      if (mods.some(function (e) { return (e == Page.cookie('userid')) })) {
         Buttons.dom.appendChild(document.createTextNode('\u00A0\u00A0\u00A0'));
         Buttons.add(Bi.mod, 'mod', 'Secret moderator messages', 'Secret message', '<mod>', '</mod>');
         Buttons.add(Bi.adm, 'adm', 'Unfiltered HTML', 'OMG wacky and zany mod hax here!!!<br /><blink>I\'m gay</blink>', '<adm>', '</adm>');
      }
   }
}

Buttons.images = {//{{{
   adm: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAACgklEQVQ4y6WVPU8bQRCGnzVYWIizscFXgCJqx04VpfAPwFKEEir%2BAMJ%2FwFSJUpM0uKBDZ1KmcJASWtJQJC1JYUQZIXEo2JBPjIQFOymcPd%2BXkygZaXW3s7tz77zz7pwSEVlaWmJhYQHLsri5uWGYKaWG%2BkdGRuh2u2xtbbG3twciIpubm%2FKvprUWrbU339jYEEBGAcbGxgA8tFprAEQErTUiEjvMmjmTy%2BVIpVIAjPoDmmBmo3n3B%2FPvM9T4P359fT0IbBD6NxgL%2BwzS8JpSKrCWCKceHuMfXzAxMYFlWViWRTqd5uVxJpJFeJ7wp%2B9fTKV%2B8PbJOKpU7UN3WrSc%2Fmu1pMhuuwGuDfoAYn9g8zxo2FTqA0k55SLFsjNwVEus7esI77%2BlYrz7judVv1JrzNmAPUfN561XnvGBfEQhscUDoH1EoSV8u%2FXdR88XPstdHp%2Bd8UiE6WnhzarNUXudO1NB2UWoMNY5OaRaUmQyTY4zmUA2ANOnr1GqT9XhSSdARaB4fsRaa%2FIzBUMkJaXYdie9Q1OfXg0KChRm8gHZBRBHzM9lbZfFIuj9NZ6%2BFzr2oqcOcCgXGS43%2F1XWWvM1eY%2FlX4ed5XnyBw3sSp16xWannffUUdtdZNJ1I9fcK16c3IorbXYPd5ihEUi9Wlplrr2M47S4f7sXaQFmPhqWm3menyeYXy%2FTUKUQT3UqdoGWrIDrRooWkFscYhGh07F5eHrKg5irjutGGpXW2qM1omPTTP40hvWWAMdxLXNYJwsHDNOYSCQGgS8vLwHIZrN%2FhTTuwyJCOp3m6upqELjZbDI7O4tlWfR6PeKyMBTF%2BUSEZDLJxcUFzWazvyYiMuwn%2BT%2F2E4a07WPJ1%2BNWAAAAAElFTkSuQmCC',
   bold: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAAC7ElEQVQ4y52VT0szVxTGf3NnyESrGbUoBougEt%2BFIooYcCFxUxBEofgN%2Bj267t6VKxfdqQhdCYKgUBAXAd24EBcKbUIqSMB0JuTO%2FdPNO2mcTLp4DwzMvffMuc95znPOOAAHBwc%2F7%2B7u%2FlooFPJKKcUAcxwnc18IgRBChGHoHB8f%2F3Zzc%2FMLAEdHR1X7jWaMscaY7vrw8PAf4CcPwPd9H0BrDYAxBgBrLcYYrLWZT3KWfDMxMUE%2Bn88B33tfA%2BokrV7H5L03WK9fQk3v5UopAxivF2GvQ2LpvQRp%2BsxxnE9nXi%2FK3hQ%2FPj5otVpIKbHW4vs%2BWmuiKMLzPKanp3Fdd2BWmYGVUtRqNe7u7nh5ecFxHDY2NvA8j%2Fv7e1qtFuVymc3NTUZGRj4FTVgV6WIBuK7LwsICs7OzdDodfN9nZWWF7e1ttra2iKKIs7Mzrq%2Bv%2B3gfSEW6MFprjDG4rovjOORyuS6f7%2B%2FvmcXuQ2yM6TporVFKEccxnU6HKIoIw5BqtUqtVmN0dJTFxcVM2XURJ4u05JLgzWaTarVKs9nk9vYWKSWVSoW1tbU%2BKj4Vrxdx4qC1Jo5jpJQMDQ1RKpUYGxsjl8txcXHB%2Bfk5Qgh2dnYypSiyej9xVEohpURrTRAEFItF9vf3mZubo9FocHl5ydPTU6bcRLqVE76MMV3EUkriOMYYQz6fJwgCpJS8vb1Rr9f72vx%2FVZGIv91udy%2B21lKv13l9fSWKIoIgoFQq9c2UgYGllDw%2BPvL8%2FIwQgna7zdXVFZOTkzw8PNBoNKhUKuzt7VEsFvvGwMDAAMPDw5TLZVZXV1FK4bouQgiWlpZYX19neXmZqampvkmYZOelOXYcp9t58%2FPzA0dmeghlcux8bbO0yLMmWVoB6XEghPgvcBiGAmB8fHwgwjTS9MXWWgqFAlJKAQgP4PT09PeZmZkvhULBlVKaLE0n8yFrz1qL53mEYeidnJz8Bfyd%2FB2%2FA34EfgA0324u8Cfwx7%2BVZRmnW%2FQjsAAAAABJRU5ErkJggg%3D%3D',
   italic: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAAChklEQVQ4y52VsU4jMRCGPzurkOVggZwSCUhBFxShlIgXOFFQnahoKO49rr6eioriOqjuBRCgE6I5KQUVVEgXIUWELWAdBctrX8PmzGaXgpEsj72%2Fx%2BOZf2YFwO7u7rednZ0fURTVjDGGEhFCFO5LKZFSSqWUODo6%2Bnl%2Bfv4dgMPDwz%2Fug2Ktddbayfrg4CABvgYAMzMzMwBpmgJgrQXAOYe1Fudc4ci%2BZWfq9Tq1Wq0KfA5eDabZs3xgpvvGfFwWGv9yY4wFbOB76AMy0Vrz%2BPjI8%2FMzxpjJWF5eZnFxcYIXQkxeARD4XuY9e%2FWA29tbTk9PieMYKSWNRoPt7W0WFhZKz8oyw9ZarLVUq1U6nQ5SSowxdLtd9vf3WV9fn8I65yZ5CvLJ8udMHw6HxHFMkiR0u10ajUYptjQU%2BXk0GtHr9Xh6emJlZYV6vV6YYF%2BfhMJPXgbIdKUUvV6PJElot9vMz89P0TB%2FwZThPOUAHh4euLu7Yzwe0%2Bl0mJ2dncqJfy5bT9HNB4xGI66vr3l5eaHZbNJsNqeSnGfRG4%2FzkgHjOOby8hLnHBsbG4RhSBGLiugWlJWyc477%2B3tubm4QQrC1tcXc3Fxhiftn3mVFkiT0%2B32urq7QWiOEQCnFeDymWq0WGs5XbaHhwWDAxcUFw%2BGQzc1NtNacnZ2xtrb2hsNFScuk0HCr1WJvb480TZFSYq3FGEMYhqXczcLwpvL8GAshqFQqhGFY2i6LElYYY%2FHa%2F%2FIk94FlbTTfDqSU%2Fw0rpSTA0tLSu16%2Bd7FzjiiK0FpLQAYAJycnv1ZXV9tRFFW01raI01m%2FLdpzzhEEAUqp4Pj4uA8Msr%2FjJ%2BAL0AJSPi4V4C%2Fw%2Bx%2BNdi1yEob21wAAAABJRU5ErkJggg%3D%3D',
   mod: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAAB30lEQVQ4y52VvYrbQBSFv5FkW0uQ8c92Ttr4LdKlcpXCTbJV3iN1YFlCcOfCTUpVeYJgWNKlTV4gCyE2MRhkg43n3jSSrD%2FL670wSJrRnDn33DMzRlUZj8fvR6PRxyAIfGvtgRNhjDnZ77qus9lszGw2%2BzKfzz%2Bgqkyn0x%2F6xBARFZH0ezKZRMAbD6DVarUArLUAiAgAqoqIoKqVLRlL5vR6PXzfbwJ9Jwa0SVqqCr9mNJpN3J%2FHBZKW%2Fc8YgzEmN344HAQQL8swZSdH%2FfyrK2rj83f%2B3QzTBRIsr5i6qqJoQotNFFXKkyVSlZWXpp8bjHFr9K3TvRI4%2BwQIgqBeik%2F3LG6GpWxKUuSAVVmv11TVIVeTgkNKwKUFLpQii%2BEVU4%2BJptHpdOqluJvz590wV6OzjFFhtVo9umDZd6eKRGq3ms1RZFhpt%2FJWjgGspd%2B%2Frpfi9hsPb19ebrflcvlo%2F57U%2BAhMZdrFvqJc2ahmfP0CC6jKWWtl54pIKmvJFcYYtPeK1WJx1rNntzRgsowvOXCKMjqOcwTebrcOQLfbvWh3FYHb7Ta73c4BHA8gDMOvg8FgGASBu9%2FvpViY5F4r1iJ7yDcaDaIo8sIwfAD%2BmvgmeAa8Bp4DlqeHC%2FwG7v8D5%2BYG6k4HfUYAAAAASUVORK5CYII%3D',
   pre: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAACaElEQVQ4y6WVTWgTQRTHf7vZmlCztZtoA41HMYLnQj20giiIPUiRehAt4sGDB0UQKq1Rq4JHIYiSQ6VaRAwUPXj0I9WT6NV%2BiDfrwRIrlLTQkn3jpRM3091Q64NhZ9%2FMvM%2F%2Ff8ZSSjEwMHCur6%2Fvruu6Cd%2F3a0SIZVmR%2BlgsZi8vL1tjY2NPyuVyHqUUxWLxs9qiiIgSkfp%2FoVCoAv0OQDwejwP4vg%2BAiACglEJEUEqFDr2mz6RSKRKJxDYg7awb9HVawY16HjQW3KdLE3Req9UEECcYYXCDFlOnIzXXLMtqWHPM1E0jwXmYLiorJ%2BzQ%2FwwdpG0aDn6b6aL0kaVolrbZ1Kh5PeJgMx61tjI0NEQymcR1r%2FK2opDpcTzPY3h4mHQ6zc6JGUSEcr6DTCZDJjPBbMDBhhoDnP10n2SX4uPSEvumx9nxaobfg4N8nZxl7%2BvDVCq3ERF%2BTeU5WdSnrvDs%2FVG6j3uNzQtGjCgodJETQZQCTQTg3qneesqi4PzzL9zs8UKhaIeS%2F2I37e3teAfmmDyWo1K%2BRu7EQy737KKjI89URZHuHeXQm%2F10dnaSzWZ5OhcCtwYqK%2BHCi2%2FcOZha965QvbdYWBg1YKXoufGD%2BeuNjY6E2%2BPuSzzo38PIu8o%2FYdtk6Aa4nVlc5HQE40xsh%2BFai9MMv81G2EUlIvWybkCFvkw2Y7gZpR39CJjsirrJTIMma23b%2Fmt4ZWXFBvA8b9MlMB0rpWhra2N1ddUGbAegVCq9zGazOdd1Y2tra2I2Rr9rZi%2BCZWtpaaFarTqlUmke%2BGmtX9LbgSPAbsBn6xIDvgMf%2FgDpHEJyEUDo8wAAAABJRU5ErkJggg%3D%3D',
   quote: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAACUklEQVQ4y52UQWgTQRSGv91sTUS3kGwJQvTa9KI3b1K8WISIINKD4smDBWsvCgqiB8HqpYgEpPQQWooX46UoeKol0qPelKYeBEFRaiMUmhZasvM8NBMns7u19sHA7nvz3vzvn%2F%2BNIyIMDw9fLZVKj33fz4Rh2CLBHMdJ9KdSKXdjY8OpVCqztVrtPiLC1NTUB9mnKaVEKdX5L5fLTeCCB5BOp9MAYRgCoJQCQERQSiEisUvHdE4ulyOTyRwAAq9dMNRtmRv1t1nM3KepMQ9vtVoKUJ6J0NygzfZppHbMcZyumGu3vrNWmb%2Bd4eD0EiJCfeYw%2FsxSIiVxXXlm%2BzqwNH2Mc%2BUxXn0t0li4w8kbo8x9GdiVb5P3DmKzMI23zFwHxoY40Vfn9flnMDrE8aCb364cw59Mhd5VPELQ%2BMnyTqRdrMHC3SxBcI93v6E%2B20c%2Bn%2Bf5crdCOlSYl0H2NI%2FW1xkXQUk%2F42trPNRtimqn9ZPPqjaIEQpBt%2BwiVNiSi%2FDf%2BMj8JIy8PEuRz7y%2FCUxcYjAXpSiK2Lphc9XfXGTyySIrgzlWa0%2B5de0Fny73RwaqU9g2G7FeA1dW%2BdVODgYf8OOU2l1ucaNsnv4vicXlRHQcJ6e9FLYn1LORJuk07kLjdN0ltyTEe0VpXpymNaIK%2FZj8D69xI61V4dhPZtJLZhe0aXRd92%2Fhzc1NFyCbze6ZAvtgEaG3t5etrS0XcD2AarU6VygUir7vp7a3t1WcpjVFcT4Roaenh2az6VWr1e%2FAitN%2BpA8BZ4CjQMj%2BLQV8Axb%2FAAWASr8Fmse%2BAAAAAElFTkSuQmCC',
   spoiler: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAABuUlEQVQ4y6WVTY7aQBCFv4bhx8qYYAtpJsORWHMxdiy4gE%2FBYQZHkT0LJEACQVUWpE273TZR0lJLtst%2B9erV67JRVV0ulywWC%2BI45na70baMMa3P%2B%2F0%2Bx%2BORzWbDdrsFVdX1eq3%2FukRERaS6X61WCugLwGg0AqjYiggAqoqIoKrBbWP2mzRNGY%2FHAPRcQAsGMBgOGf5J6IK5702ThCRNa%2FHr9foAtgxFpNp2jaOoBmqZfp9Oa0mMMVUM4MUv3W4DWH7fXl9x26bO9VdZBqvqueXbQBRF924HwFxQA8xms6pKl3EDuGGlFqYh43VKEfTpE1DfIQ1gt2m0MLX3Lrhvu4YUz0BNRzK3R6122%2B%2F3raA%2BeJ7ntUNU0ziY2bNU4TT4lwhv7%2B8A%2FPj4IN%2FtwnZzj7LfBOOU6MZ%2F5nlDggZj3272gAAURdE6K3afn%2FdvvJnS0NgvpyzLoMf9ZvtzpJNxURSdk823mZXBytrwsR0mz7ZfYacrfJO7L%2FrsfKBK217vAXw6nQBIkuSvmIYSqyqTyYTz%2BfwAzrKM%2BXxOHMdcLhdCVViJQs9UlcFgwOFwIMuye0xVte0n%2BT%2FrN%2FEjqtwGjLDtAAAAAElFTkSuQmCC',
   underline: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsSAAALEgHS3X78AAACDklEQVQ4y52Uv4sTQRTHP7vZmEDckERME1svvVh6pVUqkRT%2BqAT%2FAUshoJXNFZJGUgR%2FgM1WVlbCBa69QhA8rVWIxjIJJGTfs7hMbnZ21oN7MOzMm9nvvO%2F3vXmBqtLv9x%2F1er0XcRxX0zTdUGBBEBT6S6VSuFgsgvF4%2FG4ymQxQVUaj0bFe0ERERWS3Hg6Hc%2BBOBFCpVCoAaZoCICIAqCoigqp6h9kz%2F7RaLarV6iXgSrQFTA0t%2B6CZ22D2OSONfflmsxFAIjtC%2B4Ax12cidfeCIMjshS717PjK61qNy29OLN%2BMw6cNGo23nDgX2gFENv08sKAA6uh8erpQ913ENrD9tS2rb5H%2FDDhypcgx2CLZSd0BOVVhX5JLXlYSyQBnpciXoY0R%2BqgXSbFjInmfuz4nYvUkT7zJczUOfW8%2FF%2FH3KX%2FPY%2BFcErpP2SRBREjT69x8Cby6y8dvpz6ZfeHTCDi4wd5%2Fyi0qrIbtvPtgxpFcZX%2B%2FzRMT6sEhv%2B7t5ZJWqLEPWFXpPvzD9L7kdPRJkim3oohVlUG7zcjbhR%2Fz%2FvOAW81s4oysuaowzcSM59MpzzxauvS9GgOB2zKLOpnLyJUxDMMz4OVyGQI0m83Cpu57XS5wvV5ntVqFQBgBJEnyodPpdOM4Lq3Xa%2FHVtJHI51NVyuUy8%2Fk8SpLkJ%2FA72DbpGnAbuAakXNxKwA%2Fg6B8mHSSOtqeYhAAAAABJRU5ErkJggg%3D%3D',
};//}}}

Buttons.instances = {};

Buttons.add = function (img, id, title, sample, start, end) {
   var button = document.createElement('img');
   button.style.cursor = 'pointer';
   button.src = img;
   $(button, 'button-' + id);
   button.title = title;
   button.alt = id;
   button.addEventListener('click', Buttons.onClick, false);
   Buttons.instances[id] = {
      dom: button,
      sample: sample,
      start: start,
      end: end,
      title: title,
      img: img
   }
   Buttons.dom.appendChild(button);
}

Buttons.onClick = function (e) {
   e.preventDefault();
   if (MessageBox.msg.disabled) return;
   var id = Buttons.instances[e.target.alt]
   Buttons.insertTags(id.start, id.end, id.sample);
}

Buttons.insertTags = function (tagOpen, tagClose, sampleText) {
   var txtarea;
   if (MessageBox.msg) {
      txtarea = MessageBox.msg;
   } else {
      // some alternate form? take the first one we can find
      var areas = document.getElementsByTagName('textarea');
      txtarea = areas[0];
   }
   var selText, isSample = false;

   if (document.selection  && document.selection.createRange) { // IE/Opera

      //save window scroll position
      if (document.documentElement && document.documentElement.scrollTop)
         var winScroll = document.documentElement.scrollTop
      else if (document.body)
      var winScroll = document.body.scrollTop;
      //get current selection
      txtarea.focus();
      var range = document.selection.createRange();
      selText = range.text;
      //insert tags
      checkSelectedText();
      range.text = tagOpen + selText + tagClose;
      //mark sample text as selected
      if (isSample && range.moveStart) {
         if (window.opera)
            tagClose = tagClose.replace(/\n/g,'');
         range.moveStart('character', - tagClose.length - selText.length);
         range.moveEnd('character', - tagClose.length);
      }
      range.select();
      //restore window scroll position
      if (document.documentElement && document.documentElement.scrollTop)
         document.documentElement.scrollTop = winScroll
      else if (document.body)
      document.body.scrollTop = winScroll;

   } else if (txtarea.selectionStart || txtarea.selectionStart == '0') { // Mozilla

      //save textarea scroll position
      var textScroll = txtarea.scrollTop;
      //get current selection
      txtarea.focus();
      var startPos = txtarea.selectionStart;
      var endPos = txtarea.selectionEnd;
      selText = txtarea.value.substring(startPos, endPos);
      // pretend that we actually selected text
      if (!selText) {
         selText = sampleText;
      }
      // clean up trailing whitespace
      // we have to capture here otherwise it only matches one space
      var regex = /(\s*)$/;
      var trailing = selText.match(regex)[1];
      if (trailing.length) {
         // remove trailing whitespace from what we think is the selection
         selText = selText.replace(regex, '');
         // and move our end position back
         endPos -= trailing.length;
      }
      // insert our tags
      // use join here b/c this is inefficient for large strings
      txtarea.value = [txtarea.value.substring(0, startPos),
      tagOpen, selText, tagClose, txtarea.value.substring(endPos)].join('');
      //set new selection
         txtarea.selectionStart = startPos + tagOpen.length;
         txtarea.selectionEnd = txtarea.selectionStart + selText.length;
      //restore textarea scroll position
      txtarea.scrollTop = textScroll;
   }

   // leave in for Opera compat
   function checkSelectedText() {
      if (!selText) {
         selText = sampleText;
         isSample = true;
      } else if (selText.match(/\s+$/)) { // exclude all trailing whitespace
         var t;
         [selText, t] = selText.match(/(.*?)(\s+)$/).slice(1,3);
         tagClose += t;
      }
   }
}

// check for compatability
if (!document.getElementsByClassName) {
   var e = document.createElement('e');
   e.innerHTML = '<em>Quickpost requires Firefox 3.</em>';
   document.getElementsByTagName('body')[0].appendChild(e);
} else if (!GM_listValues) {
   var e = document.createElement('e');
   e.innerHTML = '<em>Quickpost requires Greasemonkey 0.8.20090123.1.</em>';
   document.getElementsByTagName('body')[0].appendChild(e);
} else {
   if (!Page.closed(document)) {
      // initialize the script
      LastFM.dom;
      Preview.dom;
      Update.check();
   }
}