You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
11 KiB
293 lines
11 KiB
# 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.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 |
|
|
|
def add_commands(d): |
|
def dec(cls): |
|
for c in d: |
|
def func(x): |
|
def gen(self, *a): |
|
self.send(x.upper(), *a) |
|
return gen |
|
setattr(cls, c, func(c)) |
|
return cls |
|
return dec |
|
@add_commands(("join", |
|
"mode", |
|
"nick", |
|
"notice", |
|
"part")) |
|
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): |
|
... c.join('#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.nickname = "" |
|
self.real_name = "" |
|
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 arg is None: |
|
continue |
|
else: |
|
raise IRCClientError('Refusing to send one of the args from provided: %s' |
|
% repr([(type(arg), arg) for arg in args])) |
|
|
|
msg = bytes(" ", "utf_8").join(bargs) |
|
logging.info('---> send "%s"' % msg) |
|
|
|
self.socket.send(msg + bytes("\r\n", "utf_8")) |
|
|
|
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) |
|
|
|
self.nick(self.nickname) |
|
self.user(self.nickname, 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() |
|
def msg(self, user, msg): |
|
for line in msg.split('\n'): |
|
self.send("PRIVMSG", user, ":{0}".format(line)) |
|
def quit(self, msg): |
|
self.send("QUIT :" + msg) |
|
def identify(self, passwd, authuser="NickServ"): |
|
self.msg(authuser, "IDENTIFY {0}".format(passwd)) |
|
def user(self, uname, rname): |
|
self.send("USER", uname, self.host, self.host, |
|
rname or uname) |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|