set it all up
This commit is contained in:
commit
6d40b403c6
16
LICENSE
Normal file
16
LICENSE
Normal file
@ -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.
|
6
botconfig.py
Normal file
6
botconfig.py
Normal file
@ -0,0 +1,6 @@
|
||||
PASS = ""
|
||||
CHANNEL = "#example"
|
||||
HOST = "irc.freenode.net"
|
||||
PORT = 6667
|
||||
NICK = ""
|
||||
ADMINS = ("")
|
22
oyoyo/__init__.py
Normal file
22
oyoyo/__init__.py
Normal file
@ -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/
|
||||
"""
|
271
oyoyo/client.py
Normal file
271
oyoyo/client.py
Normal file
@ -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
|
||||
|
||||
|
||||
|
||||
|
223
oyoyo/cmdhandler.py
Normal file
223
oyoyo/cmdhandler.py
Normal file
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
45
oyoyo/examplebot.py
Normal file
45
oyoyo/examplebot.py
Normal file
@ -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()
|
90
oyoyo/helpers.py
Normal file
90
oyoyo/helpers.py
Normal file
@ -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()
|
||||
|
209
oyoyo/ircevents.py
Normal file
209
oyoyo/ircevents.py
Normal file
@ -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())
|
||||
|
97
oyoyo/parse.py
Normal file
97
oyoyo/parse.py
Normal file
@ -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:
|
||||
|
||||
<message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
|
||||
<prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
|
||||
<command> ::= <letter> { <letter> } | <number> <number> <number>
|
||||
<SPACE> ::= ' ' { ' ' }
|
||||
<params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
|
||||
|
||||
<middle> ::= <Any *non-empty* sequence of octets not including SPACE
|
||||
or NUL or CR or LF, the first of which may not be ':'>
|
||||
<trailing> ::= <Any, possibly *empty*, sequence of octets not including
|
||||
NUL or CR or LF>
|
||||
|
||||
<crlf> ::= 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)
|
||||
|
||||
<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)
|
||||
|
91
wolfbot.py
Normal file
91
wolfbot.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user