commit 7f76b9bb94aaf03de6810963c7dd02322cefd7f3 Author: Stephen Dranger Date: Fri Jan 21 04:18:22 2011 -0600 first commit diff --git a/Pesterchum.png b/Pesterchum.png new file mode 100644 index 0000000..4e76f52 Binary files /dev/null and b/Pesterchum.png differ diff --git a/oyoyo/__init__.py b/oyoyo/__init__.py new file mode 100644 index 0000000..16018de --- /dev/null +++ b/oyoyo/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2008 Duncan Fordyce +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""A small, simple irc lib for python suitable for bots, clients and anything else. + +For more information and documentation about this package: + http://code.google.com/p/oyoyo/ +""" diff --git a/oyoyo/__init__.pyc b/oyoyo/__init__.pyc new file mode 100644 index 0000000..c381bcc Binary files /dev/null and b/oyoyo/__init__.pyc differ diff --git a/oyoyo/client.py b/oyoyo/client.py new file mode 100644 index 0000000..795d9b1 --- /dev/null +++ b/oyoyo/client.py @@ -0,0 +1,268 @@ +# Copyright (c) 2008 Duncan Fordyce +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import socket +import sys +import re +import string +import time +import threading +import os +import traceback + +from oyoyo.parse import * +from oyoyo import helpers +from oyoyo.cmdhandler import CommandError + +# Python < 3 compatibility +if sys.version_info < (3,): + class bytes(object): + def __new__(self, b='', encoding='utf8'): + return str(b) + + +class IRCClientError(Exception): + pass + + +class IRCClient: + """ IRC Client class. This handles one connection to a server. + This can be used either with or without IRCApp ( see connect() docs ) + """ + + def __init__(self, cmd_handler, **kwargs): + """ the first argument should be an object with attributes/methods named + as the irc commands. You may subclass from one of the classes in + oyoyo.cmdhandler for convenience but it is not required. The + methods should have arguments (prefix, args). prefix is + normally the sender of the command. args is a list of arguments. + Its recommened you subclass oyoyo.cmdhandler.DefaultCommandHandler, + this class provides defaults for callbacks that are required for + normal IRC operation. + + all other arguments should be keyword arguments. The most commonly + used will be nick, host and port. You can also specify an "on connect" + callback. ( check the source for others ) + + Warning: By default this class will not block on socket operations, this + means if you use a plain while loop your app will consume 100% cpu. + To enable blocking pass blocking=True. + + >>> class My_Handler(DefaultCommandHandler): + ... def privmsg(self, prefix, command, args): + ... print "%s said %s" % (prefix, args[1]) + ... + >>> def connect_callback(c): + ... helpers.join(c, '#myroom') + ... + >>> cli = IRCClient(My_Handler, + ... host="irc.freenode.net", + ... port=6667, + ... nick="myname", + ... connect_cb=connect_callback) + ... + >>> cli_con = cli.connect() + >>> while 1: + ... cli_con.next() + ... + """ + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.nick = None + self.real_name = None + self.host = None + self.port = None + self.connect_cb = None + self.blocking = False + + self.__dict__.update(kwargs) + self.command_handler = cmd_handler(self) + + self._end = 0 + + def send(self, *args, **kwargs): + """ send a message to the connected server. all arguments are joined + with a space for convenience, for example the following are identical + + >>> cli.send("JOIN %s" % some_room) + >>> cli.send("JOIN", some_room) + + In python 2, all args must be of type str or unicode, *BUT* if they are + unicode they will be converted to str with the encoding specified by + the 'encoding' keyword argument (default 'utf8'). + In python 3, all args must be of type str or bytes, *BUT* if they are + str they will be converted to bytes with the encoding specified by the + 'encoding' keyword argument (default 'utf8'). + """ + # Convert all args to bytes if not already + encoding = kwargs.get('encoding') or 'utf8' + bargs = [] + for arg in args: + if isinstance(arg, str): + bargs.append(bytes(arg, encoding)) + elif isinstance(arg, bytes): + bargs.append(arg) + elif type(arg).__name__ == 'unicode': + bargs.append(arg.encode(encoding)) + else: + raise IRCClientError('Refusing to send one of the args from provided: %s' + % repr([(type(arg), arg) for arg in args])) + + msg = bytes(" ", "ascii").join(bargs) + logging.info('---> send "%s"' % msg) + self.socket.send(msg + bytes("\r\n", "ascii")) + + def connect(self): + """ initiates the connection to the server set in self.host:self.port + and returns a generator object. + + >>> cli = IRCClient(my_handler, host="irc.freenode.net", port=6667) + >>> g = cli.connect() + >>> while 1: + ... g.next() + + """ + try: + logging.info('connecting to %s:%s' % (self.host, self.port)) + self.socket.connect(("%s" % self.host, self.port)) + if not self.blocking: + self.socket.setblocking(0) + + helpers.nick(self, self.nick) + helpers.user(self, self.nick, self.real_name) + + if self.connect_cb: + self.connect_cb(self) + + buffer = bytes() + while not self._end: + try: + buffer += self.socket.recv(1024) + except socket.error, e: + try: # a little dance of compatibility to get the errno + errno = e.errno + except AttributeError: + errno = e[0] + if not self.blocking and errno == 11: + pass + else: + raise e + else: + data = buffer.split(bytes("\n", "ascii")) + buffer = data.pop() + + for el in data: + prefix, command, args = parse_raw_irc_command(el) + try: + self.command_handler.run(command, prefix, *args) + except CommandError: + # error will of already been loggingged by the handler + pass + + yield True + finally: + if self.socket: + logging.info('closing socket') + self.socket.close() + + +class IRCApp: + """ This class manages several IRCClient instances without the use of threads. + (Non-threaded) Timer functionality is also included. + """ + + class _ClientDesc: + def __init__(self, **kwargs): + self.con = None + self.autoreconnect = False + self.__dict__.update(kwargs) + + def __init__(self): + self._clients = {} + self._timers = [] + self.running = False + self.sleep_time = 0.5 + + def addClient(self, client, autoreconnect=False): + """ add a client object to the application. setting autoreconnect + to true will mean the application will attempt to reconnect the client + after every disconnect. you can also set autoreconnect to a number + to specify how many reconnects should happen. + + warning: if you add a client that has blocking set to true, + timers will no longer function properly """ + logging.info('added client %s (ar=%s)' % (client, autoreconnect)) + self._clients[client] = self._ClientDesc(autoreconnect=autoreconnect) + + def addTimer(self, seconds, cb): + """ add a timed callback. accuracy is not specified, you can only + garuntee the callback will be called after seconds has passed. + ( the only advantage to these timers is they dont use threads ) + """ + assert callable(cb) + logging.info('added timer to call %s in %ss' % (cb, seconds)) + self._timers.append((time.time() + seconds, cb)) + + def run(self): + """ run the application. this will block until stop() is called """ + # TODO: convert this to use generators too? + self.running = True + while self.running: + found_one_alive = False + + for client, clientdesc in self._clients.iteritems(): + if clientdesc.con is None: + clientdesc.con = client.connect() + + try: + clientdesc.con.next() + except Exception, e: + logging.error('client error %s' % e) + logging.error(traceback.format_exc()) + if clientdesc.autoreconnect: + clientdesc.con = None + if isinstance(clientdesc.autoreconnect, (int, float)): + clientdesc.autoreconnect -= 1 + found_one_alive = True + else: + clientdesc.con = False + else: + found_one_alive = True + + if not found_one_alive: + logging.info('nothing left alive... quiting') + self.stop() + + now = time.time() + timers = self._timers[:] + self._timers = [] + for target_time, cb in timers: + if now > target_time: + logging.info('calling timer cb %s' % cb) + cb() + else: + self._timers.append((target_time, cb)) + + time.sleep(self.sleep_time) + + def stop(self): + """ stop the application """ + self.running = False + + + + diff --git a/oyoyo/client.pyc b/oyoyo/client.pyc new file mode 100644 index 0000000..efc0bc5 Binary files /dev/null and b/oyoyo/client.pyc differ diff --git a/oyoyo/cmdhandler.py b/oyoyo/cmdhandler.py new file mode 100644 index 0000000..48505f7 --- /dev/null +++ b/oyoyo/cmdhandler.py @@ -0,0 +1,212 @@ +# Copyright (c) 2008 Duncan Fordyce +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import inspect +import logging +import sys +import traceback + +from oyoyo import helpers +from oyoyo.parse import parse_nick + +# Python < 3 compatibility +if sys.version_info < (3,): + class bytes(object): + def __new__(self, b='', encoding='utf8'): + return str(b) + + +def protected(func): + """ decorator to protect functions from being called """ + func.protected = True + return func + + +class CommandError(Exception): + def __init__(self, cmd): + self.cmd = cmd + +class NoSuchCommandError(CommandError): + def __str__(self): + return 'No such command "%s"' % ".".join(self.cmd) + +class ProtectedCommandError(CommandError): + def __str__(self): + return 'Command "%s" is protected' % ".".join(self.cmd) + + +class CommandHandler(object): + """ The most basic CommandHandler """ + + def __init__(self, client): + self.client = client + + @protected + def get(self, in_command_parts): + """ finds a command + commands may be dotted. each command part is checked that it does + not start with and underscore and does not have an attribute + "protected". if either of these is true, ProtectedCommandError + is raised. + its possible to pass both "command.sub.func" and + ["command", "sub", "func"]. + """ + if isinstance(in_command_parts, (str, bytes)): + in_command_parts = in_command_parts.split(bytes('.', 'ascii')) + command_parts = in_command_parts[:] + + p = self + while command_parts: + cmd = command_parts.pop(0).decode('ascii') + if cmd.startswith('_'): + raise ProtectedCommandError(in_command_parts) + + try: + f = getattr(p, cmd) + except AttributeError: + raise NoSuchCommandError(in_command_parts) + + if hasattr(f, 'protected'): + raise ProtectedCommandError(in_command_parts) + + if isinstance(f, CommandHandler) and command_parts: + return f.get(command_parts) + p = f + + return f + + @protected + def run(self, command, *args): + """ finds and runs a command """ + logging.debug("processCommand %s(%s)" % (command, args)) + + try: + f = self.get(command) + except NoSuchCommandError: + self.__unhandled__(command, *args) + return + + logging.debug('f %s' % f) + + try: + f(*args) + except Exception, e: + logging.error('command raised %s' % e) + logging.error(traceback.format_exc()) + raise CommandError(command) + + @protected + def __unhandled__(self, cmd, *args): + """The default handler for commands. Override this method to + apply custom behavior (example, printing) unhandled commands. + """ + logging.debug('unhandled command %s(%s)' % (cmd, args)) + + +class DefaultCommandHandler(CommandHandler): + """ CommandHandler that provides methods for the normal operation of IRC. + If you want your bot to properly respond to pings, etc, you should subclass this. + """ + + def ping(self, prefix, server): + self.client.send('PONG', server) + + +class DefaultBotCommandHandler(CommandHandler): + """ default command handler for bots. methods/attributes are made + available as commands """ + + @protected + def getVisibleCommands(self, obj=None): + test = (lambda x: isinstance(x, CommandHandler) or \ + inspect.ismethod(x) or inspect.isfunction(x)) + members = inspect.getmembers(obj or self, test) + return [m for m, _ in members + if (not m.startswith('_') and + not hasattr(getattr(obj, m), 'protected'))] + + def help(self, sender, dest, arg=None): + """list all available commands or get help on a specific command""" + logging.info('help sender=%s dest=%s arg=%s' % (sender, dest, arg)) + if not arg: + commands = self.getVisibleCommands() + commands.sort() + helpers.msg(self.client, dest, + "available commands: %s" % " ".join(commands)) + else: + try: + f = self.get(arg) + except CommandError, e: + helpers.msg(self.client, dest, str(e)) + return + + doc = f.__doc__.strip() if f.__doc__ else "No help available" + + if not inspect.ismethod(f): + subcommands = self.getVisibleCommands(f) + if subcommands: + doc += " [sub commands: %s]" % " ".join(subcommands) + + helpers.msg(self.client, dest, "%s: %s" % (arg, doc)) + + +class BotCommandHandler(DefaultCommandHandler): + """ complete command handler for bots """ + + def __init__(self, client, command_handler): + DefaultCommandHandler.__init__(self, client) + self.command_handler = command_handler + + def privmsg(self, prefix, dest, msg): + self.tryBotCommand(prefix, dest, msg) + + @protected + def tryBotCommand(self, prefix, dest, msg): + """ tests a command to see if its a command for the bot, returns True + and calls self.processBotCommand(cmd, sender) if its is. + """ + + logging.debug("tryBotCommand('%s' '%s' '%s')" % (prefix, dest, msg)) + + if dest == self.client.nick: + dest = parse_nick(prefix)[0] + elif msg.startswith(self.client.nick): + msg = msg[len(self.client.nick)+1:] + else: + return False + + msg = msg.strip() + + parts = msg.split(' ', 1) + command = parts[0] + arg = parts[1:] + + try: + self.command_handler.run(command, prefix, dest, *arg) + except CommandError, e: + helpers.msg(self.client, dest, str(e)) + return True + + + + + + + + + + diff --git a/oyoyo/cmdhandler.pyc b/oyoyo/cmdhandler.pyc new file mode 100644 index 0000000..8a4dea4 Binary files /dev/null and b/oyoyo/cmdhandler.pyc differ diff --git a/oyoyo/examplebot.py b/oyoyo/examplebot.py new file mode 100644 index 0000000..81aac02 --- /dev/null +++ b/oyoyo/examplebot.py @@ -0,0 +1,45 @@ +#!/usr/bin/python +"""Example bot for oyoyo that responds to !say""" + +import logging +import re + +from oyoyo.client import IRCClient +from oyoyo.cmdhandler import DefaultCommandHandler +from oyoyo import helpers + + +HOST = 'irc.freenode.net' +PORT = 6667 +NICK = 'oyoyo-example' +CHANNEL = '#oyoyo-test' + + +class MyHandler(DefaultCommandHandler): + def privmsg(self, nick, chan, msg): + msg = msg.decode() + match = re.match('\!say (.*)', msg) + if match: + to_say = match.group(1).strip() + print('Saying, "%s"' % to_say) + helpers.msg(self.client, chan, to_say) + + +def connect_cb(cli): + helpers.join(cli, CHANNEL) + + +def main(): + logging.basicConfig(level=logging.DEBUG) + + cli = IRCClient(MyHandler, host=HOST, port=PORT, nick=NICK, + connect_cb=connect_cb) + conn = cli.connect() + + while True: + conn.next() ## python 2 + # next(conn) ## python 3 + + +if __name__ == '__main__': + main() diff --git a/oyoyo/helpers.py b/oyoyo/helpers.py new file mode 100644 index 0000000..815ea35 --- /dev/null +++ b/oyoyo/helpers.py @@ -0,0 +1,87 @@ +# Copyright (c) 2008 Duncan Fordyce +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" contains helper functions for common irc commands """ + +import random + +def msg(cli, user, msg): + for line in msg.split('\n'): + cli.send("PRIVMSG", user, ":%s" % line) + +def msgrandom(cli, choices, dest, user=None): + o = "%s: " % user if user else "" + o += random.choice(choices) + msg(cli, dest, o) + +def _makeMsgRandomFunc(choices): + def func(cli, dest, user=None): + msgrandom(cli, choices, dest, user) + return func + +msgYes = _makeMsgRandomFunc(['yes', 'alright', 'ok']) +msgOK = _makeMsgRandomFunc(['ok', 'done']) +msgNo = _makeMsgRandomFunc(['no', 'no-way']) + + +def ns(cli, *args): + msg(cli, "NickServ", " ".join(args)) + +def cs(cli, *args): + msg(cli, "ChanServ", " ".join(args)) + +def identify(cli, passwd, authuser="NickServ"): + msg(cli, authuser, "IDENTIFY %s" % passwd) + +def quit(cli, msg='gone'): + cli.send("QUIT :%s" % msg) + cli._end = 1 + +def user(cli, username, realname=None): + cli.send("USER", username, cli.host, cli.host, + realname or username) + +_simple = ( + 'join', + 'part', + 'nick', + 'notice', +) +def _addsimple(): + import sys + def simplecmd(cmd_name): + def f(cli, *args): + cli.send(cmd_name, *args) + return f + m = sys.modules[__name__] + for t in _simple: + setattr(m, t, simplecmd(t.upper())) +_addsimple() + +def _addNumerics(): + import sys + from oyoyo import ircevents + def numericcmd(cmd_num, cmd_name): + def f(cli, *args): + cli.send(cmd_num, *args) + return f + m = sys.modules[__name__] + for num, name in ircevents.numeric_events.iteritems(): + setattr(m, name, numericcmd(num, name)) + +_addNumerics() + diff --git a/oyoyo/helpers.pyc b/oyoyo/helpers.pyc new file mode 100644 index 0000000..851dac5 Binary files /dev/null and b/oyoyo/helpers.pyc differ diff --git a/oyoyo/ircevents.py b/oyoyo/ircevents.py new file mode 100644 index 0000000..623530f --- /dev/null +++ b/oyoyo/ircevents.py @@ -0,0 +1,209 @@ +# Copyright (c) 2008 Duncan Fordyce +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# taken from python irclib.. who took it from... +# Numeric table mostly stolen from the Perl IRC module (Net::IRC). +numeric_events = { + "001": "welcome", + "002": "yourhost", + "003": "created", + "004": "myinfo", + "005": "featurelist", # XXX + "200": "tracelink", + "201": "traceconnecting", + "202": "tracehandshake", + "203": "traceunknown", + "204": "traceoperator", + "205": "traceuser", + "206": "traceserver", + "207": "traceservice", + "208": "tracenewtype", + "209": "traceclass", + "210": "tracereconnect", + "211": "statslinkinfo", + "212": "statscommands", + "213": "statscline", + "214": "statsnline", + "215": "statsiline", + "216": "statskline", + "217": "statsqline", + "218": "statsyline", + "219": "endofstats", + "221": "umodeis", + "231": "serviceinfo", + "232": "endofservices", + "233": "service", + "234": "servlist", + "235": "servlistend", + "241": "statslline", + "242": "statsuptime", + "243": "statsoline", + "244": "statshline", + "250": "luserconns", + "251": "luserclient", + "252": "luserop", + "253": "luserunknown", + "254": "luserchannels", + "255": "luserme", + "256": "adminme", + "257": "adminloc1", + "258": "adminloc2", + "259": "adminemail", + "261": "tracelog", + "262": "endoftrace", + "263": "tryagain", + "265": "n_local", + "266": "n_global", + "300": "none", + "301": "away", + "302": "userhost", + "303": "ison", + "305": "unaway", + "306": "nowaway", + "311": "whoisuser", + "312": "whoisserver", + "313": "whoisoperator", + "314": "whowasuser", + "315": "endofwho", + "316": "whoischanop", + "317": "whoisidle", + "318": "endofwhois", + "319": "whoischannels", + "321": "liststart", + "322": "list", + "323": "listend", + "324": "channelmodeis", + "329": "channelcreate", + "331": "notopic", + "332": "currenttopic", + "333": "topicinfo", + "341": "inviting", + "342": "summoning", + "346": "invitelist", + "347": "endofinvitelist", + "348": "exceptlist", + "349": "endofexceptlist", + "351": "version", + "352": "whoreply", + "353": "namreply", + "361": "killdone", + "362": "closing", + "363": "closeend", + "364": "links", + "365": "endoflinks", + "366": "endofnames", + "367": "banlist", + "368": "endofbanlist", + "369": "endofwhowas", + "371": "info", + "372": "motd", + "373": "infostart", + "374": "endofinfo", + "375": "motdstart", + "376": "endofmotd", + "377": "motd2", # 1997-10-16 -- tkil + "381": "youreoper", + "382": "rehashing", + "384": "myportis", + "391": "time", + "392": "usersstart", + "393": "users", + "394": "endofusers", + "395": "nousers", + "401": "nosuchnick", + "402": "nosuchserver", + "403": "nosuchchannel", + "404": "cannotsendtochan", + "405": "toomanychannels", + "406": "wasnosuchnick", + "407": "toomanytargets", + "409": "noorigin", + "411": "norecipient", + "412": "notexttosend", + "413": "notoplevel", + "414": "wildtoplevel", + "421": "unknowncommand", + "422": "nomotd", + "423": "noadmininfo", + "424": "fileerror", + "431": "nonicknamegiven", + "432": "erroneusnickname", # Thiss iz how its speld in thee RFC. + "433": "nicknameinuse", + "436": "nickcollision", + "437": "unavailresource", # "Nick temporally unavailable" + "441": "usernotinchannel", + "442": "notonchannel", + "443": "useronchannel", + "444": "nologin", + "445": "summondisabled", + "446": "usersdisabled", + "451": "notregistered", + "461": "needmoreparams", + "462": "alreadyregistered", + "463": "nopermforhost", + "464": "passwdmismatch", + "465": "yourebannedcreep", # I love this one... + "466": "youwillbebanned", + "467": "keyset", + "471": "channelisfull", + "472": "unknownmode", + "473": "inviteonlychan", + "474": "bannedfromchan", + "475": "badchannelkey", + "476": "badchanmask", + "477": "nochanmodes", # "Channel doesn't support modes" + "478": "banlistfull", + "481": "noprivileges", + "482": "chanoprivsneeded", + "483": "cantkillserver", + "484": "restricted", # Connection is restricted + "485": "uniqopprivsneeded", + "491": "nooperhost", + "492": "noservicehost", + "501": "umodeunknownflag", + "502": "usersdontmatch", +} + +generated_events = [ + # Generated events + "dcc_connect", + "dcc_disconnect", + "dccmsg", + "disconnect", + "ctcp", + "ctcpreply", +] + +protocol_events = [ + # IRC protocol events + "error", + "join", + "kick", + "mode", + "part", + "ping", + "privmsg", + "privnotice", + "pubmsg", + "pubnotice", + "quit", + "invite", + "pong", +] + +all_events = generated_events + protocol_events + numeric_events.values() + diff --git a/oyoyo/ircevents.pyc b/oyoyo/ircevents.pyc new file mode 100644 index 0000000..2ee05f8 Binary files /dev/null and b/oyoyo/ircevents.pyc differ diff --git a/oyoyo/parse.py b/oyoyo/parse.py new file mode 100644 index 0000000..ac5fc12 --- /dev/null +++ b/oyoyo/parse.py @@ -0,0 +1,97 @@ +# Copyright (c) 2008 Duncan Fordyce +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import sys + +from oyoyo.ircevents import * + +# Python < 3 compatibility +if sys.version_info < (3,): + class bytes(object): + def __new__(self, b='', encoding='utf8'): + return str(b) + + +def parse_raw_irc_command(element): + """ + This function parses a raw irc command and returns a tuple + of (prefix, command, args). + The following is a psuedo BNF of the input text: + + ::= [':' ] + ::= | [ '!' ] [ '@' ] + ::= { } | + ::= ' ' { ' ' } + ::= [ ':' | ] + + ::= + ::= + + ::= CR LF + """ + parts = element.strip().split(bytes(" ", "ascii")) + if parts[0].startswith(bytes(':', 'ascii')): + prefix = parts[0][1:] + command = parts[1] + args = parts[2:] + else: + prefix = None + command = parts[0] + args = parts[1:] + + if command.isdigit(): + try: + command = numeric_events[command] + except KeyError: + logging.warn('unknown numeric event %s' % command) + command = command.lower() + + if args[0].startswith(bytes(':', 'ascii')): + args = [bytes(" ", "ascii").join(args)[1:]] + else: + for idx, arg in enumerate(args): + if arg.startswith(bytes(':', 'ascii')): + args = args[:idx] + [bytes(" ", 'ascii').join(args[idx:])[1:]] + break + + return (prefix, command, args) + + +def parse_nick(name): + """ parse a nickname and return a tuple of (nick, mode, user, host) + + [ '!' [ = ] ] [ '@' ] + """ + + try: + nick, rest = name.split('!') + except ValueError: + return (name, None, None, None) + try: + mode, rest = rest.split('=') + except ValueError: + mode, rest = None, rest + try: + user, host = rest.split('@') + except ValueError: + return (name, mode, rest, None) + + return (name, mode, user, host) + diff --git a/oyoyo/parse.pyc b/oyoyo/parse.pyc new file mode 100644 index 0000000..24e7278 Binary files /dev/null and b/oyoyo/parse.pyc differ diff --git a/pesterchum.js b/pesterchum.js new file mode 100644 index 0000000..6be8b0d --- /dev/null +++ b/pesterchum.js @@ -0,0 +1,6 @@ +{ + "chums": ["gamblingGenocider", + "grimAuxiliatrix", + "gardenGnostic" + ] +} \ No newline at end of file diff --git a/pesterchum.py b/pesterchum.py new file mode 100644 index 0000000..872910f --- /dev/null +++ b/pesterchum.py @@ -0,0 +1,117 @@ +# pesterchum +from oyoyo.client import IRCClient +from oyoyo.cmdhandler import DefaultCommandHandler +from oyoyo import helpers +import logging +import sys +import json +from PyQt4 import QtGui, QtCore + +logging.basicConfig(level=logging.INFO) + +class PesterHandler(DefaultCommandHandler): + def privmsg(self, nick, chan, msg): + # display msg, do other stuff + print "%s: %s" % (nick, msg) + if chan == "#pesterchum": + # follow instructions + self.window.newMessage() + pass + else: + # private message + pass + def welcome(self, server, nick, msg): + helpers.join(self.client, "#pesterchum") + +class userConfig(object): + def __init__(self): + fp = open("pesterchum.js") + self.config = json.load(fp) + fp.close() + def chums(self): + return self.config['chums'] + +class exitButton(QtGui.QPushButton): + def __init__(self, icon, parent=None): + QtGui.QPushButton.__init__(self, icon, "", parent) + self.setFlat(True) + def clicked(self): + print "X" + +class chumArea(QtGui.QListWidget): + def __init__(self, chums, parent=None): + QtGui.QListWidget.__init__(self, parent) + self.setGeometry(75, 100, 350, 500) + self.setStyleSheet(""" +background-color: black; +color: white; +font: bold; +font-family: "Courier New"; +""") + self.chums = chums + for c in self.chums: + chumLabel = QtGui.QListWidgetItem(c) +# chumLabel.setFont(QtGui.QFont("Courier New", pointSize=12, +# weight=75)) + self.addItem(chumLabel) + + +class PesterWindow(QtGui.QWidget): + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent, + flags=QtCore.Qt.CustomizeWindowHint) + self.config = userConfig() + self.setGeometry(100,100, 500, 700) + self.setWindowIcon(QtGui.QIcon('themes/pesterchum/trayicon.gif')) + self.setStyleSheet(""" +background-color: #fdb302; +""") + self.closeButton = exitButton(QtGui.QIcon('themes/pesterchum/x.gif'), self) + s = self.size() - self.closeButton.icon().availableSizes()[0] + self.closeButton.move(s.width(), 0) + self.connect(self.closeButton, QtCore.SIGNAL('clicked()'), + self, QtCore.SLOT('close()')) + self.chumList = chumArea(self.config.chums(), self) + + self.cli = IRCClient(PesterHandler, host="irc.tymoon.eu", port=6667, nick="superGhost") + self.cli.command_handler.window = self + + self.conn = self.cli.connect() + self.irctimer = QtCore.QTimer(self) + self.connect(self.irctimer, QtCore.SIGNAL('timeout()'), + self, QtCore.SLOT('updateIRC()')) + self.irctimer.start() + self.moving = None + def mouseMoveEvent(self, event): + if self.moving: + move = event.globalPos() - self.moving + self.move(self.pos() + move) + self.moving = event.globalPos() + def mousePressEvent(self, event): + if event.button() == 1: + self.moving = event.globalPos() + def mouseReleaseEvent(self, event): + if event.button() == 1: + self.moving = None + @QtCore.pyqtSlot() + def updateIRC(self): + self.conn.next() + def newMessage(self): + pass + + +def main(): + + app = QtGui.QApplication(sys.argv) + widget = PesterWindow() + widget.show() + trayicon = QtGui.QSystemTrayIcon(QtGui.QIcon("themes/pesterchum/trayicon.gif"), app) + traymenu = QtGui.QMenu() + traymenu.addAction("Hi!! HI!!") + trayicon.setContextMenu(traymenu) + trayicon.show() + + sys.exit(app.exec_()) + + +main() diff --git a/themes/pesterchum/Thumbs.db b/themes/pesterchum/Thumbs.db new file mode 100644 index 0000000..8517079 Binary files /dev/null and b/themes/pesterchum/Thumbs.db differ diff --git a/themes/pesterchum/abouticon.png b/themes/pesterchum/abouticon.png new file mode 100644 index 0000000..02fa887 Binary files /dev/null and b/themes/pesterchum/abouticon.png differ diff --git a/themes/pesterchum/alarm.wav b/themes/pesterchum/alarm.wav new file mode 100644 index 0000000..910abdc Binary files /dev/null and b/themes/pesterchum/alarm.wav differ diff --git a/themes/pesterchum/chummy.gif b/themes/pesterchum/chummy.gif new file mode 100644 index 0000000..26c9ad3 Binary files /dev/null and b/themes/pesterchum/chummy.gif differ diff --git a/themes/pesterchum/detestful.gif b/themes/pesterchum/detestful.gif new file mode 100644 index 0000000..e8f3e08 Binary files /dev/null and b/themes/pesterchum/detestful.gif differ diff --git a/themes/pesterchum/devious.gif b/themes/pesterchum/devious.gif new file mode 100644 index 0000000..06ba8be Binary files /dev/null and b/themes/pesterchum/devious.gif differ diff --git a/themes/pesterchum/discontent.gif b/themes/pesterchum/discontent.gif new file mode 100644 index 0000000..addcf54 Binary files /dev/null and b/themes/pesterchum/discontent.gif differ diff --git a/themes/pesterchum/distraught.gif b/themes/pesterchum/distraught.gif new file mode 100644 index 0000000..0103ac5 Binary files /dev/null and b/themes/pesterchum/distraught.gif differ diff --git a/themes/pesterchum/estatic.gif b/themes/pesterchum/estatic.gif new file mode 100644 index 0000000..dc3ee61 Binary files /dev/null and b/themes/pesterchum/estatic.gif differ diff --git a/themes/pesterchum/h.gif b/themes/pesterchum/h.gif new file mode 100644 index 0000000..184514e Binary files /dev/null and b/themes/pesterchum/h.gif differ diff --git a/themes/pesterchum/m.gif b/themes/pesterchum/m.gif new file mode 100644 index 0000000..489abaa Binary files /dev/null and b/themes/pesterchum/m.gif differ diff --git a/themes/pesterchum/offline.gif b/themes/pesterchum/offline.gif new file mode 100644 index 0000000..e3c6b53 Binary files /dev/null and b/themes/pesterchum/offline.gif differ diff --git a/themes/pesterchum/pcbg.png b/themes/pesterchum/pcbg.png new file mode 100644 index 0000000..1f93fa9 Binary files /dev/null and b/themes/pesterchum/pcbg.png differ diff --git a/themes/pesterchum/pleasant.gif b/themes/pesterchum/pleasant.gif new file mode 100644 index 0000000..3233ff4 Binary files /dev/null and b/themes/pesterchum/pleasant.gif differ diff --git a/themes/pesterchum/rancorous.gif b/themes/pesterchum/rancorous.gif new file mode 100644 index 0000000..a826c75 Binary files /dev/null and b/themes/pesterchum/rancorous.gif differ diff --git a/themes/pesterchum/relaxed.gif b/themes/pesterchum/relaxed.gif new file mode 100644 index 0000000..55a31a0 Binary files /dev/null and b/themes/pesterchum/relaxed.gif differ diff --git a/themes/pesterchum/sleek.gif b/themes/pesterchum/sleek.gif new file mode 100644 index 0000000..5573aa5 Binary files /dev/null and b/themes/pesterchum/sleek.gif differ diff --git a/themes/pesterchum/smooth.gif b/themes/pesterchum/smooth.gif new file mode 100644 index 0000000..a261759 Binary files /dev/null and b/themes/pesterchum/smooth.gif differ diff --git a/themes/pesterchum/style.pcs b/themes/pesterchum/style.pcs new file mode 100644 index 0000000..8ea9940 --- /dev/null +++ b/themes/pesterchum/style.pcs @@ -0,0 +1,162 @@ +#PESTERCHUM STYLE + +// The title will appear at the top of the window. +// Instead of a space in the name or title, use an '_'. It will come out as a space. +// The name and author will only be in the 'about' section. +// The name you will enter in the 'options' is the name of the folder this is in. +// The alarm only plays when a message is recieved. +// The mode can be set to 'regular' or 'trollian'. + +name Pesterchum +title Pesterchum +author Grimlive95 +alarm alarm.wav +mode regular + +// Colors are in the format of 'red/green/blue/alpha', alpha being transparency. +// 255 is solid and 000 is invisible. + +// MAIN WINDOW + +// If you have a background image, set 'c 3' alpa to '000'. +// If your background image has a header on it, do the same with the title text color. + +c 1 000 000 000 000 // title text +c 2 000 000 000 255 // chumhandle/ mood label text +c 3 000 000 000 255 // outside menu text +c 4 000 000 000 255 // inside menu text +c 5 255 181 000 000 // main background +c 6 255 181 000 255 // menu background + +// BUTTONS + +// Main buttons are the moods that aren't 'rancorous', the 'pester' and 'add chum' buttons. +// They are also the buttons on the Options, Quirks Manager, and Trollslum. +// Block buttons are the 'rancorous' and 'block' buttons. + +c 7 196 135 000 255 // main button borders +c 8 196 135 000 255 // block button borders +c 9 196 135 000 255 // offline button border + +c 10 255 255 000 255 // main buttons +c 11 240 000 000 255 // block buttons +c 12 000 000 000 255 // offline button + +c 13 000 000 000 255 // main button text +c 14 000 000 000 255 // block button text +c 15 255 255 255 255 // offline button text + +// CHUMROLL & CHUMHANDLE + +c 16 255 255 000 255 // chumroll border +c 17 000 000 000 255 // chumroll background +c 18 150 150 150 255 // chumroll highlight + +c 19 100 100 100 255 // chumroll usertext +c 20 255 255 255 255 // chumroll highlighted usertext + +c 21 255 255 000 255 // my chumhandle border +c 22 000 000 000 255 // my chumhandle background +c 23 255 255 255 255 // my chumhandle usertext + +// PESTER WINDOW + +c 24 255 181 000 255 // pester window background +c 25 255 255 255 255 // pesterlog background +c 26 255 255 255 255 // text field background +c 27 000 000 000 255 // text field text color + +c 28 196 135 000 255 // pesterwindow topbar +c 29 255 255 255 255 // pesterwindow top bar text + +c 30 196 135 000 255 // pesterlog border +c 31 196 135 000 255 // text field border + +// PESTER WINDOW BUTTONS + +c 32 196 135 000 255 // main button borders +c 33 196 135 000 255 // block button border + +c 34 255 255 000 255 // main buttons +c 35 255 000 000 255 // block button + +c 36 000 000 000 255 // pesterwindow button text +c 37 000 000 000 255 // pesterwindow block button text + +// OPTIONS WINDOW + +c 38 255 181 000 255 // background +c 39 000 000 000 255 // text color +c 40 196 135 000 255 // button borders +c 41 255 255 000 255 // buttons +c 42 000 000 000 255 // button text + +// QUIRKS MANAGER WINDOW + +c 43 255 181 000 255 // background +c 44 000 000 000 255 // text color +c 45 196 135 000 255 // button borders +c 46 255 255 000 255 // buttons +c 47 000 000 000 255 // button text + +// TROLLSLUM WINDOW + +c 48 255 181 000 255 // background +c 49 000 000 000 255 // text color +c 50 196 135 000 255 // button borders +c 51 255 255 000 255 // buttons +c 52 000 000 000 255 // button text + +// FONTS (In the format: Font_name MODE size) + +// menu is for the top menu. +// tagandmood is for the CHUMHANDLE and MOOD labels. +// message is for the text field in the pester window. +// topbar is for the ::nameName:: text on the top of the pester window. +// tray is for your tray popups. +// debug is for the debug console. + +f title Courier BOLD 14 +f menu Courier BOLD 12 +f buttons Courier BOLD 14 + +f tagandmood Courier BOLD 14 +f chumroll Courier BOLD 14 +f chumhandle Courier BOLD 14 + +f message Courier BOLD 13 +f topbar Courier BOLD 13 + +f tray Courier BOLD 12 + +f debug Courier BOLD 13 + +// PESTERCHUM MOOD ICONS + +i ichummy chummy.gif +i ipleasant pleasant.gif +i idistraught distraught.gif +i iunruly unruly.gif +i ismooth smooth.gif +i irancorous rancorous.gif +i ioffline offline.gif + +// TROLLIAN MOOD ICONS + +i iestatic estatic.gif +i irelaxed relaxed.gif +i idiscontent discontent.gif +i idevious devious.gif +i isleek sleek.gif +i idetestful detestful.gif + +// ETC. + +// iicon only appears in the about section. +// ticon will appear in both your tray and your pesterwindow. + +i ticon trayicon2.png +i iicon abouticon.png +i iclose x.gif +i ihide m.gif +i ibg pcbg.png \ No newline at end of file diff --git a/themes/pesterchum/trayicon.gif b/themes/pesterchum/trayicon.gif new file mode 100644 index 0000000..926ccc7 Binary files /dev/null and b/themes/pesterchum/trayicon.gif differ diff --git a/themes/pesterchum/trayicon2.png b/themes/pesterchum/trayicon2.png new file mode 100644 index 0000000..817bf4b Binary files /dev/null and b/themes/pesterchum/trayicon2.png differ diff --git a/themes/pesterchum/unruly.gif b/themes/pesterchum/unruly.gif new file mode 100644 index 0000000..09c58ec Binary files /dev/null and b/themes/pesterchum/unruly.gif differ diff --git a/themes/pesterchum/x.gif b/themes/pesterchum/x.gif new file mode 100644 index 0000000..41b47e6 Binary files /dev/null and b/themes/pesterchum/x.gif differ