You are not logged in.

#1 2009-02-08 10:29:58

HashBox
Member
Registered: 2009-01-22
Posts: 271

pidgin/weechat fifo plugins for use with dzen

I created a couple of very simple plugins for libpurple (Pidgin) and weechat for use with dzen.
These are extremely basic and do nothing more than what I require, therefore you will likely want to modify them for your own usage. There is loads of room for improvement wink

To create fifos

mkfifo ~/.purple/fifo
mkfifo ~/.weechat/fifo

pidginsrc/libpurple/plugins/messagefifo.c

#define PURPLE_PLUGINS

#include <stdio.h>

#include "conversation.h"
#include "signals.h"
#include "version.h"

char temp[256];

static void
write_fifo(const char *buffer)
{
    FILE *pipe;
    if ((pipe = fopen("/home/HOMEDIR/.purple/fifo", "w"))) { // ATTENTION, REPLACE HOMEDIR WITH YOUR USERNAME
        fprintf(pipe, "^tw()%s\n%s\n", buffer, buffer);
        fclose(pipe);
    }
}

static void
buddy_signed_on_cb(PurpleBuddy *buddy, void *data)
{
    snprintf(temp, sizeof(temp), "^fg(lightblue)%s ^fg()has signed ^fg(white)on", purple_buddy_get_alias(buddy));
    write_fifo(temp);
}

static void
buddy_signed_off_cb(PurpleBuddy *buddy, void *data)
{
    snprintf(temp, sizeof(temp), "^fg(lightblue)%s ^fg()has signed ^fg(white)off", purple_buddy_get_alias(buddy));
    write_fifo(temp);
}

static void
received_im_msg_cb(PurpleAccount *account, char *sender, char *buffer, PurpleConversation *conv, PurpleMessageFlags flags, void *data)
{
    snprintf(temp, sizeof(temp), "^fg(lightblue)%s ^fg()says, ^fg(grey50)%s", purple_buddy_get_alias(purple_find_buddy(account, sender)), buffer);
    write_fifo(temp);
}

static void
buddy_typing_cb(PurpleAccount *account, const char *name, void *data)
{
    snprintf(temp, sizeof(temp), "^fg(lightblue)%s ^fg()is typing...", purple_buddy_get_alias(purple_find_buddy(account, name)));
    write_fifo(temp);
}

/*static void
buddy_typing_stopped_cb(PurpleAccount *account, const char *name, void *data)
{
    sprintf(temp, "%s has stopped typing", purple_buddy_get_alias(purple_find_buddy(account, name)));
    write_fifo(temp);
}*/

static gboolean
plugin_load(PurplePlugin *plugin)
{
    void *blist_handle    = purple_blist_get_handle();
    void *conv_handle     = purple_conversations_get_handle();

    /* Buddy List subsystem signals */
    purple_signal_connect(blist_handle, "buddy-signed-on",
                        plugin, PURPLE_CALLBACK(buddy_signed_on_cb), NULL);
    purple_signal_connect(blist_handle, "buddy-signed-off",
                        plugin, PURPLE_CALLBACK(buddy_signed_off_cb), NULL);

    /* Conversations subsystem signals */
    purple_signal_connect(conv_handle, "received-im-msg",
                        plugin, PURPLE_CALLBACK(received_im_msg_cb), NULL);
    purple_signal_connect(conv_handle, "buddy-typing",
                        plugin, PURPLE_CALLBACK(buddy_typing_cb), NULL);
/*    purple_signal_connect(conv_handle, "buddy-typing-stopped",
                        plugin, PURPLE_CALLBACK(buddy_typing_stopped_cb), NULL);*/

    return TRUE;
}

static PurplePluginInfo info =
{
    PURPLE_PLUGIN_MAGIC,
    PURPLE_MAJOR_VERSION,
    PURPLE_MINOR_VERSION,
    PURPLE_PLUGIN_STANDARD,                             /**< type           */
    NULL,                                             /**< ui_requirement */
    0,                                                /**< flags          */
    NULL,                                             /**< dependencies   */
    PURPLE_PRIORITY_DEFAULT,                            /**< priority       */

    "messagefifo",                            /**< id             */
    "Message Fifo",                               /**< name           */
    "1.0",                                  /**< version        */
                                                      /**  summary        */
    "Writes some messages to a ~/.purple/fifo",
                                                      /**  description    */
    "Writes some messages to a ~/.purple/fifo",
    "Zane Ashby <zane.a@demonastery.org>",       /**< author         */
    "http://demonastery.org",                                     /**< homepage       */

    plugin_load,                                      /**< load           */
    NULL,                                             /**< unload         */
    NULL,                                             /**< destroy        */

    NULL,                                             /**< ui_info        */
    NULL,                                             /**< extra_info     */
    NULL,
    NULL,
    /* Padding */
    NULL,
    NULL,
    NULL,
    NULL
};

static void
init_plugin(PurplePlugin *plugin)
{
}

PURPLE_INIT_PLUGIN(messagefifo, init_plugin, info)

Compile with (IIRC)

make messagefifo.so
mv messagefifo.so ~/.purple/plugins/

weechat/plugins/messagefifo.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "weechat-plugin.h"

char plugin_name[]        = "MessageFIFO";
char plugin_version[]     = "0.1";
char plugin_description[] = "Output incoming messages into fifo";

t_plugin_handler *privmsg;

int mf_privmsg (t_weechat_plugin *plugin, int argc, char **argv, char *handler_args, void *handler_pointer)
{
        FILE *pipe;
        if ((pipe = fopen("/home/HOMEDIR/.weechat/fifo", "w"))) { // ATTENTION, CHANGE HOMEDIR
                char temp[256];
                char *tok, *dp;
                dp = strdup(argv[2]);
                tok = strtok(dp, ":!");
                snprintf(temp, sizeof(temp), "^fg(lightblue)%s ^fg()says,^fg(grey50)", tok);
                tok = strtok(NULL, ":!");
                tok = strtok(NULL, ":!");
                snprintf(temp, sizeof(temp), "%s %s", temp, tok);
                while ((tok = strtok(NULL, "")) != NULL) {
                        snprintf(temp, sizeof(temp), "%s:%s", temp, tok);
                }
                fprintf(pipe, "^tw()%s\n%s\n", temp, temp);
                fclose(pipe);
                free(dp);
        } else {
                plugin->print(plugin, NULL, NULL, "Failed to open fifo");
        }

        return PLUGIN_RC_OK;
}

int weechat_plugin_init (t_weechat_plugin *plugin)
{
        privmsg = plugin->msg_handler_add (plugin, "PRIVMSG", &mf_privmsg, NULL, NULL);
        if (privmsg != NULL) {
                return PLUGIN_RC_OK;
        } else {
                plugin->print(plugin, NULL, NULL, "Failed to add Message Handler");
                return PLUGIN_RC_KO;
        }
}

void weechat_plugin_end (t_weechat_plugin *plugin)
{
        plugin->handler_remove(plugin, privmsg);
}

Compile with

gcc -fPIC -Wall -c messagefifo.c
gcc -shared -fPIC -o messagefifo.so messagefifo.o

To use with dzen (adjust accordingly)

tail -f ~/.weechat/fifo | dzen2 -l 10 -fn "-*-terminus-*-*-*-*-12-*-*-*-*-*-*-*" -ta c -y 754 -w 512 -x 0 &
tail -f ~/.purple/fifo | sed -u -e 's@<[/]*[A-Za-z][A-Za-z0-9]*[^>]*>@@g' -e 's@[&]nbsp;@ @g' -e 's@[&]amp;@\&@g' -e 's@[&]quot;@"@g' -e 's@[&][a-z][a-z]*;@ @g' "$@" | dzen2 -l 10 -fn "-*-terminus-*-*-*-*-12-*-*-*-*-*-*-*" -ta c -y 754 -w 512 -x 512 &

Last edited by HashBox (2009-04-23 02:46:27)

Offline

#2 2009-04-23 02:53:02

HashBox
Member
Registered: 2009-01-22
Posts: 271

Re: pidgin/weechat fifo plugins for use with dzen

Another one to wake the display when a message is received, using DPMS. Doubt this will be useful to others but it serves as an example.

#define PURPLE_PLUGINS

#include <X11/Xlib.h>
#include <X11/extensions/dpms.h>

#include "conversation.h"
#include "signals.h"
#include "version.h"

static void
received_im_msg_cb(PurpleAccount *account, char *sender, char *buffer, PurpleConversation *conv, PurpleMessageFlags flags, void *data)
{
    Display *dpy = XOpenDisplay(NULL);
    if (dpy) {
        DPMSEnable(dpy);             // Enable DPMS
        DPMSForceLevel(dpy, DPMSModeOn);     // Force Monitor ON
        XCloseDisplay(dpy);
    }
}

static gboolean
plugin_load(PurplePlugin *plugin)
{
    void *conv_handle     = purple_conversations_get_handle();
[url][/url]
    purple_signal_connect(conv_handle, "received-im-msg", plugin, PURPLE_CALLBACK(received_im_msg_cb), NULL);

    return TRUE;
}

static PurplePluginInfo info =
{
    PURPLE_PLUGIN_MAGIC,
    PURPLE_MAJOR_VERSION,
    PURPLE_MINOR_VERSION,
    PURPLE_PLUGIN_STANDARD,
    NULL,
    0,
    NULL,
    PURPLE_PRIORITY_DEFAULT,

    "wake-on-message",
    "Wake On Message",
    "1.0",
    "Wakes display on message receive",
    "Wakes display on message receive",
    "Zane Ashby <zane.a@demonastery.org>",
    "http://demonastery.org",

    plugin_load,
    NULL,
    NULL,

    NULL,
    NULL,
    NULL,
    NULL,

    NULL,
    NULL,
    NULL,
    NULL
};

static void
init_plugin(PurplePlugin *plugin)
{
}

PURPLE_INIT_PLUGIN(wake-on-message, init_plugin, info)

Offline

#3 2009-04-23 03:42:39

josomebody
Member
Registered: 2008-06-20
Posts: 190

Re: pidgin/weechat fifo plugins for use with dzen

The mkfifo plugins look pretty cool. I might have to fool with them some this weekend.


: () { : | :& } ;:

Offline

#4 2009-04-23 11:51:06

gondil
Member
Registered: 2008-05-25
Posts: 19

Re: pidgin/weechat fifo plugins for use with dzen

Yes! Damn I almost forgot, I subscribed earlier to this thread without reacting, thanks a lot for the idea! I made something that might interest you..

I'm running finch in a screen session on a shell server somewhere, therefore I don't have any notifications if someone starts talking to me when the screen is detached... This kind of plug-in was right what needed!
I modified the script to put a file in my public_html dir with nicknames of buddies who have unanswered conversations. To get the notifications, a script on my home box curl'ed the file every 10 seconds and piped that into dzen, nothing extraordinary..
As you can see the file gets erased as soon as I type something back. Although there might be better ways to do it, this was a reasonable trigger to me, since most of my contacts say reply-worthy things most of the time.

But! This isn't exactly what happens anymore.... since I still need dzen to get my notifications, it means my home box has to be turned on, which defeats the use of a shell server, only a little......

Therefore, Ladies and Gentlemen, I present you a new way of getting notified: Google Calendar SMS notifications.
As soon as I get talked to in a conversation that doesn't have my attention*, a Google Calendar-event is created by the Python-script below (This was a quite of a pain in the wuts, luckily I didn't have to use any Google-specific modules... The script only uses mechanize which doesn't come standard with Arch Python), and as friendly as Google is: they instantly send me a free SMS saying I have a new meeting (called (L)~°(*)LY4eVaH sNoOpy(*)°~(L) or similar). My sms notifications are configured to come 0 minutes before the event and if you don't specify a specific hour when creating an event, it is scheduled 'now'.

*This has to change to something more elegant in the plugin, one message per conversation would be less spammy for starting... Of course other things are SMS'able too... but please don't be evil with the second script!

plugin:

#define PURPLE_PLUGINS

#include <stdio.h>

#include "conversation.h"
#include "signals.h"
#include "version.h"

char temp[256];

static void
write_file(const char *buffer)
{
    FILE *pipe;
    if ((pipe = fopen("/home/gondil/public_html/fifo", "a"))) { // ATTENTION, REPLACE HOMEDIR WITH YOUR USERNAME
        fprintf(pipe, "%s\n", buffer);
        fclose(pipe);
    }
    system("echo $(sort -u /home/gondil/public_html/fifo) > /home/gondil/public_html/fifo");
    system("/home/gondil/calendar &");
}


static void
empty_file(const char *buffer)
{
    FILE *pipe;
    if ((pipe = fopen("/home/gondil/public_html/fifo", "w"))) { // ATTENTION, REPLACE HOMEDIR WITH YOUR USERNAME
        fprintf(pipe, "", buffer);
        fclose(pipe);
    }
}


static void
received_im_msg_cb(PurpleAccount *account, char *sender, char *buffer, PurpleConversation *conv, PurpleMessageFlags flags, void *data)
{
    if (purple_conversation_has_focus(conv)!= 1) {
        snprintf(temp, sizeof(temp), "%s", purple_buddy_get_alias(purple_find_buddy(account, sender)));
        write_file(temp);
    }
}

static void
sent_im_msg_cb(PurpleAccount *account, const char *receiver, const char *message, void *data)
{
    snprintf(temp, sizeof(temp), "", message);
    empty_file(temp);
}


static gboolean
plugin_load(PurplePlugin *plugin)
{
    void *conv_handle     = purple_conversations_get_handle();
    purple_signal_connect(conv_handle, "received-im-msg",
                        plugin, PURPLE_CALLBACK(received_im_msg_cb), NULL);
    purple_signal_connect(conv_handle, "sent-im-msg",
                        plugin, PURPLE_CALLBACK(sent_im_msg_cb), NULL);
    return TRUE;
}

static PurplePluginInfo info =
{
    PURPLE_PLUGIN_MAGIC,
    PURPLE_MAJOR_VERSION,
    PURPLE_MINOR_VERSION,
    PURPLE_PLUGIN_STANDARD,                             /**< type           */
    NULL,                                             /**< ui_requirement */
    0,                                                /**< flags          */
    NULL,                                             /**< dependencies   */
    PURPLE_PRIORITY_DEFAULT,                            /**< priority       */

    "messagefile",                            /**< id             */
    "Message File",                               /**< name           */
    "1.1",                                  /**< version        */
                                                      /**  summary        */
    "Writes some nicknames to a file",
                                                      /**  description    */
    "Writes some nicknames to a file",
    "Zane Ashby and gondil",       /**< author         */
    NULL,                                     /**< homepage       */

    plugin_load,                                      /**< load           */
    NULL,                                             /**< unload         */
    NULL,                                             /**< destroy        */

    NULL,                                             /**< ui_info        */
    NULL,                                             /**< extra_info     */
    NULL,
    NULL,
    /* Padding */
    NULL,
    NULL,
    NULL,
    NULL
};

static void
init_plugin(PurplePlugin *plugin)
{
}

PURPLE_INIT_PLUGIN(messagefifo, init_plugin, info)

calendar event creator:

#!/usr/bin/python

import mechanize
import sys
import urllib2


# Create a mechanize browser
mech = mechanize.Browser()
mech.set_handle_robots(False)

# With Goggles desired UAgent
mech.addheaders = [("User-agent", "Java/1.5.0_06")]

# See the G-API: We need a some token here, that is displayed by /~pkelchte/ (mah personal php site on ESAT that only does that)
mech.open("https://www.google.com/accounts/AuthSubRequest?scope=http://www.google.com/calendar/feeds/&session=1&secure=0&next=http://homes.esat.kuleuven.be/~pkelchte/index.php")

# But before we get the token, we need to submit our user-data
mech.select_form(nr=0)
mech["Email"] = ""  # REPLACE THIS WITH YOUR GOOGLE ACCOUNT
mech["Passwd"] = "" # AND THIS WITH YOUR PASSWORD

try:
    mech.submit()
except:
    print "Did not submit credentials"
    sys.exit("Error in submitting credentials")

# Because that's not enough, we need to confirm that we want to go trough with this and submit another form...
mech.select_form(nr=0)
mech.submit(id='allow')

# By now, we have one token, but it's not the final token! We need *another* token, called the AuthSubSessionToken, sigh...
mech.addheaders = [("Authorization", "AuthSub token=\"" + mech.response().read() +  "\""), ("User-agent", "Java/1.5.0_06")]
mech.open("http://www.google.com/accounts/AuthSubSessionToken")

# A bunch of tokens later...
# Let's use urllib2 to do this POST request (some xml-y thing is the string you would manually type in the "New event" box on Google Calendar)

# Encore some headers
headers = {"Authorization": "AuthSub token=\"" + mech.response().read().replace("\n","").split("=")[1] + "\"", "User-Agent": "Java/1.5.0_06", "Content-Type": "application/atom+xml"}

# Read the file that we're interested in! Damn, it's so interesting!!
file = open('/home/gondil/public_html/fifo', 'r')
message = file.read()
file.close()

# The actual event
event = """
    <entry xmlns='http://www.w3.org/2005/Atom' xmlns:gCal='http://schemas.google.com/gCal/2005'>
      <content type="html">""" + message  +  """</content>
      <gCal:quickadd value="true"/>
      </entry>
    """
req = urllib2.Request("http://www.google.com/calendar/feeds/default/private/full", event, headers)
calresponse = urllib2.urlopen(req)

# Normally, we stop here... but since Google likes traffic, we need to go to a slightly different url, with the same headers and POST data
req2 = urllib2.Request(calresponse.geturl(), event, headers)
try:
    calresponse2 = urllib2.urlopen(req2)
except:
#    print "You can check but normally this is a 201 CREATED response or something, I don't really care... It's my code, right :P"
    pass

#this might not even work, it's only here to remind me
#print urllib2.urlopen(urllib2.Request("http://www.google.com/calendar/feeds/default/owncalendars/full","",headers)).read()

index.php: (If you don't want to use that esat.kuleuven site for this... because probably I'm not going to keep this on index.php forever...)

<?php
print $_POST['token'];
print $_GET['token'];
?>

Offline

#5 2009-04-23 13:30:40

HashBox
Member
Registered: 2009-01-22
Posts: 271

Re: pidgin/weechat fifo plugins for use with dzen

Really nice stuff gondil!
I had played around with a script to send myself sms using the gcalcli tool, but decided I should probably refrain from using that for obvious reasons hehe.
I can't imagine how painful that must've been to get that mechanize script working right, but it looks great! (definitely serves as an intro for me in using mechanize tongue)
I might see if I can modify that for my own personal use, not sure why I didn't think to hook something like this up to libpurple earlier smile
I do have one question though, do you know how feasible it would be to remove those calendar entries after they have been "used"? That was one of the things keeping me from using this sms hack a bit more.

Offline

#6 2009-04-23 14:00:30

Berticus
Member
Registered: 2008-06-11
Posts: 731

Re: pidgin/weechat fifo plugins for use with dzen

Oh sweet! I was wondering what I would have to do to get notification in dzen from weechat! Was gonna hack up one of the plugins.

Offline

#7 2009-04-23 14:20:32

HashBox
Member
Registered: 2009-01-22
Posts: 271

Re: pidgin/weechat fifo plugins for use with dzen

Berticus wrote:

Oh sweet! I was wondering what I would have to do to get notification in dzen from weechat! Was gonna hack up one of the plugins.

I must point out that unless dzen is capturing the output of the fifo then weechat will actually be frozen and blocking as far as I'm aware, I've had it happen accidentally on occasion. I must look into fixing that :\ I can only assume the same happens with the libpurple version as well.

Offline

#8 2009-04-23 22:21:24

gondil
Member
Registered: 2008-05-25
Posts: 19

Re: pidgin/weechat fifo plugins for use with dzen

Hi HashBox! I was so lazy first and just occasionally cleared out my whole calendar when it got too messy... tongue

It seemed not that much extra work to delete the newly created event because Google sends back a unique ID, below is the code! (I inelegantly inserted a sleep of 20s before doing the deletion, of course you can change it (I don't know if it is needed, I think I'm at my SMS limit for today, couldn't test it, lol))
At least I can see that it does what you would expect (saw it by clicking refresh on the calendar website while executing the script)

Mechanize is handy indeed! It originated in Perl but some people are pushing me to write in Python... tongue (To be honest, I don't feel very comfortable yet but hey... as long as it works hehe)

I find it strange for example that there seemed no other way to retrieve what Google sends back when posting the event than catching the HTTPError and read()'ing it... (Receiving a 201 would not be an expected reason to throw an exception, I thought at first... but the whole urllib2 response gets lost when it happens)

Apart from this all: have fun with it and please post if you have new ideas!!

#!/usr/bin/python

import mechanize
import sys
import urllib2
import httplib
from urllib2 import HTTPError

# Create a mechanize browser
mech = mechanize.Browser()
mech.set_handle_robots(False)

# With Goggles desired UAgent
mech.addheaders = [("User-agent", "Java/1.5.0_06")]

# See the G-API: We need a some token here, that is displayed by /~pkelchte/ (mah personal php site on ESAT that only does that)
mech.open("https://www.google.com/accounts/AuthSubRequest?scope=http://www.google.com/calendar/feeds/&session=1&secure=0&next=http://homes.esat.kuleuven.be/~pkelchte/index.php")

# But before we get the token, we need to submit our user-data
mech.select_form(nr=0)
mech["Email"] = ""
mech["Passwd"] = ""

try:
    mech.submit()
except:
    print "Did not submit credentials"
    sys.exit("Error in submitting credentials")

# Because that's not enough, we need to confirm that we want to go trough with this and submit another form...
mech.select_form(nr=0)
mech.submit(id='allow')

# By now, we have one token, but it's not the final token! We need *another* token, called the AuthSubSessionToken, sigh...
mech.addheaders = [("Authorization", "AuthSub token=\"" + mech.response().read() +  "\""), ("User-agent", "Java/1.5.0_06")]
mech.open("http://www.google.com/accounts/AuthSubSessionToken")

# A bunch of tokens later...
# Let's use urllib2 to do this POST request (some xml-y thing is the string you would manually type in the "New event" box on Google Calendar)

# Encore some headers
authsub = "AuthSub token=\"" + mech.response().read().replace("\n","").split("=")[1] + "\""
headers = {"Authorization": authsub, "User-Agent": "Java/1.5.0_06", "Content-Type": "application/atom+xml", "GData-Version": "2"}

file = open('/home/gondil/public_html/fifo', 'r')
message = file.read()
file.close()

# The actual event
event = """
    <entry xmlns='http://www.w3.org/2005/Atom' xmlns:gCal='http://schemas.google.com/gCal/2005'>
      <content type="html">""" + message  +  """</content>
      <gCal:quickadd value="true"/>
      </entry>
    """
req = urllib2.Request("http://www.google.com/calendar/feeds/default/private/full", event, headers)
calresponse = urllib2.urlopen(req)

# Normally, we stop here... but since Google likes traffic, we need to go to a slightly different url, with the same headers and POST data
req2 = urllib2.Request(calresponse.geturl(), event, headers)
try:
    calresponse2 = urllib2.urlopen(req2)
# You can check but normally this is a 201 CREATED response or something, I don't really care... It's my code, right :P
except HTTPError, e :
    # I placed this sleep to give the event at least a 20 second lifetime (poor, poor event...)
    import time
    time.sleep(20)
    # Retrieve the event's edit url
    eventurl = e.read().split("<link rel='edit' type='application/atom+xml' href='http://www.google.com")[1].split("'/>")[0]
    # The Deletion has to be done via httplib, because Google wants a DELETE request (urllib2 only handles GET and POST)
    conn = httplib.HTTPConnection("www.google.com")
    conn.request("DELETE", eventurl, "", headers)
    calresponse3 = conn.getresponse()
    # Again, they like to have a little more traffic, we need to append a session ID to that last url (we can find it in the redirect page)
    eventurl2 = calresponse3.read().split("HREF=\"")[1].split("\"")[0]
    # Ooh and here there is need of a new header, no questions please
    headers2 = {"Authorization": authsub, "User-Agent": "Java/1.5.0_06", "Content-Type": "application/atom+xml", "GData-Version": "2", "If-Match": "*"}
    conn.request("DELETE", eventurl2, "", headers2)
    calresponse4 = conn.getresponse()
    # No errors? Ok we can close the connection
    conn.close()

Last edited by gondil (2009-04-23 22:35:05)

Offline

#9 2009-04-23 22:44:31

HashBox
Member
Registered: 2009-01-22
Posts: 271

Re: pidgin/weechat fifo plugins for use with dzen

Oh wow that's awesome! I will definitely make use of this now big_smile Thanks!

Edit: I've decided to go the non hacky route and bought 800 SMS credits from Clickatell, and would definitely recommend them at this stage. Your choice of API, HTTP(S) being the simplest. I can now do "sendsms.py <message> [optional number]", just need to hook this up to a notification system so I can be alerted to stuff when my pidgin status is Away smile

Last edited by HashBox (2009-04-28 12:34:58)

Offline

Board footer

Powered by FluxBB