From 34502dc153c399c77730e438116eed8ab4b23b2c Mon Sep 17 00:00:00 2001 From: jcao219 Date: Mon, 4 Jul 2011 02:47:27 -0500 Subject: [PATCH] set it all up --- LICENSE | 16 +++ botconfig.py | 6 + oyoyo/__init__.py | 22 ++++ oyoyo/client.py | 271 ++++++++++++++++++++++++++++++++++++++++++++ oyoyo/cmdhandler.py | 223 ++++++++++++++++++++++++++++++++++++ oyoyo/examplebot.py | 45 ++++++++ oyoyo/helpers.py | 90 +++++++++++++++ oyoyo/ircevents.py | 209 ++++++++++++++++++++++++++++++++++ oyoyo/parse.py | 97 ++++++++++++++++ wolfbot.py | 91 +++++++++++++++ 10 files changed, 1070 insertions(+) create mode 100644 LICENSE create mode 100644 botconfig.py create mode 100644 oyoyo/__init__.py create mode 100644 oyoyo/client.py create mode 100644 oyoyo/cmdhandler.py create mode 100644 oyoyo/examplebot.py create mode 100644 oyoyo/helpers.py create mode 100644 oyoyo/ircevents.py create mode 100644 oyoyo/parse.py create mode 100644 wolfbot.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dc97283 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) 2011 Jimmy Cao +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. \ No newline at end of file diff --git a/botconfig.py b/botconfig.py new file mode 100644 index 0000000..ade7c39 --- /dev/null +++ b/botconfig.py @@ -0,0 +1,6 @@ +PASS = "" +CHANNEL = "#example" +HOST = "irc.freenode.net" +PORT = 6667 +NICK = "" +ADMINS = ("") 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/client.py b/oyoyo/client.py new file mode 100644 index 0000000..fb2279e --- /dev/null +++ b/oyoyo/client.py @@ -0,0 +1,271 @@ +# 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 +import collections + +# 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 = True + + 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 'utf_8' + 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 as 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 logged 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 isinstance(cb, collections.Callable) + 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.items(): + if clientdesc.con is None: + clientdesc.con = client.connect() + + try: + next(clientdesc.con) + except Exception as 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/cmdhandler.py b/oyoyo/cmdhandler.py new file mode 100644 index 0000000..5f1921b --- /dev/null +++ b/oyoyo/cmdhandler.py @@ -0,0 +1,223 @@ +# 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, bytes): + in_command_parts = in_command_parts.split(b'.') + else: + in_command_parts = in_command_parts.split('.') + + command_parts = [] + for cmdpart in in_command_parts: + if isinstance(cmdpart, bytes): + cmdpart = cmdpart.decode('ascii') + command_parts.append(cmdpart) + + p = self + while command_parts: + cmd = command_parts.pop(0) + 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: + largs = list(args) + for i,arg in enumerate(largs): + if arg: largs[i] = arg.decode('ascii') + f(*largs) + except Exception as 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 as 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 as e: + helpers.msg(self.client, dest, str(e)) + return True + + + + + + + + + + diff --git a/oyoyo/examplebot.py b/oyoyo/examplebot.py new file mode 100644 index 0000000..b718bc9 --- /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: + next(conn) ## 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..cb4c70d --- /dev/null +++ b/oyoyo/helpers.py @@ -0,0 +1,90 @@ +# 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 mode(cli, chan, mod): + cli.send("MODE", chan, mod) + +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.items(): + setattr(m, name, numericcmd(num, name)) + +_addNumerics() + diff --git a/oyoyo/ircevents.py b/oyoyo/ircevents.py new file mode 100644 index 0000000..22ec0a7 --- /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 = { + b"001": "welcome", + b"002": "yourhost", + b"003": "created", + b"004": "myinfo", + b"005": "featurelist", # XXX + b"200": "tracelink", + b"201": "traceconnecting", + b"202": "tracehandshake", + b"203": "traceunknown", + b"204": "traceoperator", + b"205": "traceuser", + b"206": "traceserver", + b"207": "traceservice", + b"208": "tracenewtype", + b"209": "traceclass", + b"210": "tracereconnect", + b"211": "statslinkinfo", + b"212": "statscommands", + b"213": "statscline", + b"214": "statsnline", + b"215": "statsiline", + b"216": "statskline", + b"217": "statsqline", + b"218": "statsyline", + b"219": "endofstats", + b"221": "umodeis", + b"231": "serviceinfo", + b"232": "endofservices", + b"233": "service", + b"234": "servlist", + b"235": "servlistend", + b"241": "statslline", + b"242": "statsuptime", + b"243": "statsoline", + b"244": "statshline", + b"250": "luserconns", + b"251": "luserclient", + b"252": "luserop", + b"253": "luserunknown", + b"254": "luserchannels", + b"255": "luserme", + b"256": "adminme", + b"257": "adminloc1", + b"258": "adminloc2", + b"259": "adminemail", + b"261": "tracelog", + b"262": "endoftrace", + b"263": "tryagain", + b"265": "n_local", + b"266": "n_global", + b"300": "none", + b"301": "away", + b"302": "userhost", + b"303": "ison", + b"305": "unaway", + b"306": "nowaway", + b"311": "whoisuser", + b"312": "whoisserver", + b"313": "whoisoperator", + b"314": "whowasuser", + b"315": "endofwho", + b"316": "whoischanop", + b"317": "whoisidle", + b"318": "endofwhois", + b"319": "whoischannels", + b"321": "liststart", + b"322": "list", + b"323": "listend", + b"324": "channelmodeis", + b"329": "channelcreate", + b"331": "notopic", + b"332": "currenttopic", + b"333": "topicinfo", + b"341": "inviting", + b"342": "summoning", + b"346": "invitelist", + b"347": "endofinvitelist", + b"348": "exceptlist", + b"349": "endofexceptlist", + b"351": "version", + b"352": "whoreply", + b"353": "namreply", + b"361": "killdone", + b"362": "closing", + b"363": "closeend", + b"364": "links", + b"365": "endoflinks", + b"366": "endofnames", + b"367": "banlist", + b"368": "endofbanlist", + b"369": "endofwhowas", + b"371": "info", + b"372": "motd", + b"373": "infostart", + b"374": "endofinfo", + b"375": "motdstart", + b"376": "endofmotd", + b"377": "motd2", # 1997-10-16 -- tkil + b"381": "youreoper", + b"382": "rehashing", + b"384": "myportis", + b"391": "time", + b"392": "usersstart", + b"393": "users", + b"394": "endofusers", + b"395": "nousers", + b"401": "nosuchnick", + b"402": "nosuchserver", + b"403": "nosuchchannel", + b"404": "cannotsendtochan", + b"405": "toomanychannels", + b"406": "wasnosuchnick", + b"407": "toomanytargets", + b"409": "noorigin", + b"411": "norecipient", + b"412": "notexttosend", + b"413": "notoplevel", + b"414": "wildtoplevel", + b"421": "unknowncommand", + b"422": "nomotd", + b"423": "noadmininfo", + b"424": "fileerror", + b"431": "nonicknamegiven", + b"432": "erroneusnickname", # Thiss iz how its speld in thee RFC. + b"433": "nicknameinuse", + b"436": "nickcollision", + b"437": "unavailresource", # "Nick temporally unavailable" + b"441": "usernotinchannel", + b"442": "notonchannel", + b"443": "useronchannel", + b"444": "nologin", + b"445": "summondisabled", + b"446": "usersdisabled", + b"451": "notregistered", + b"461": "needmoreparams", + b"462": "alreadyregistered", + b"463": "nopermforhost", + b"464": "passwdmismatch", + b"465": "yourebannedcreep", # I love this one... + b"466": "youwillbebanned", + b"467": "keyset", + b"471": "channelisfull", + b"472": "unknownmode", + b"473": "inviteonlychan", + b"474": "bannedfromchan", + b"475": "badchannelkey", + b"476": "badchanmask", + b"477": "nochanmodes", # "Channel doesn't support modes" + b"478": "banlistfull", + b"481": "noprivileges", + b"482": "chanoprivsneeded", + b"483": "cantkillserver", + b"484": "restricted", # Connection is restricted + b"485": "uniqopprivsneeded", + b"491": "nooperhost", + b"492": "noservicehost", + b"501": "umodeunknownflag", + b"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 + list(numeric_events.values()) + diff --git a/oyoyo/parse.py b/oyoyo/parse.py new file mode 100644 index 0000000..767a617 --- /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 (nick, mode, rest, None) + + return (nick, mode, user, host) + diff --git a/wolfbot.py b/wolfbot.py new file mode 100644 index 0000000..ebc5da0 --- /dev/null +++ b/wolfbot.py @@ -0,0 +1,91 @@ +from oyoyo.client import IRCClient +from oyoyo.cmdhandler import DefaultCommandHandler +from oyoyo import helpers +from oyoyo.parse import parse_nick +import logging +import botconfig + +def connect_callback(cli): + helpers.identify(cli, botconfig.PASS) + helpers.join(cli, botconfig.CHANNEL) + helpers.msg(cli, "ChanServ", "op "+botconfig.CHANNEL) + helpers.msg(cli, botconfig.CHANNEL, "\u0002Wolfbot2 is here.\u0002") + +G_PM_COMMANDS = [] +G_COMMANDS = [] +COMMANDS = {} +PM_COMMANDS = {} + +HOOKS = {} + +def cmd(s, pmOnly = False): + def dec(f): + if s is None and pmOnly: + G_PM_COMMANDS.append(f) + elif s is None and not pmOnly: + G_COMMANDS.append(f) + elif pmOnly: + if s in PM_COMMANDS: + PM_COMMANDS[s].append(f) + else: PM_COMMANDS[s] = [f] + else: + if s in COMMANDS: + COMMANDS[s].append(f) + else: COMMANDS[s] = [f] + return f + return dec + +def hook(s): + def dec(f): + HOOKS[s] = f + return f + return dec + +class WolfBotHandler(DefaultCommandHandler): + def __init__(self, client): + super().__init__(client) + + def privmsg(self, rawnick, chan, msg): + print("{0} in {1} said: {2}".format(rawnick, chan, msg)) + + if chan != botconfig.NICK: #not a PM + for x in COMMANDS: + if msg.startswith(x): + msg = msg.replace(x, "", 1) + for f in COMMANDS[x]: + f(self.client, rawnick, chan, msg.lstrip()) + else: + for x in PM_COMMANDS: + if msg.startswith(x): + msg = msg.replace(x, "", 1) + for f in PM_COMMANDS[x]: + f(self.client, rawnick, msg.lstrip()) + + def nick(self, fro, to): + print(fro, to) + +def main(): + cli = IRCClient(WolfBotHandler, host="irc.freenode.net", port=6667, nick="wolfbot2-alpha", + connect_cb=connect_callback) + + conn = cli.connect() + while True: + next(conn) + +#Game Logic Begins: + +@cmd("!say", True) +def join(cli, rawnick, rest): + helpers.msg(cli, botconfig.CHANNEL, "{0} says: {1}".format(parse_nick(rawnick)[0], rest)) + +@cmd("!bye", True) +@cmd("!bye", False) +def forced_exit(cli, rawnick, *rest): + if parse_nick(rawnick)[0] in botconfig.ADMINS: + helpers.quit(cli, "Forced quit from admin") + raise SystemExit + +#Game Logic Ends + +if __name__ == "__main__": + main() \ No newline at end of file