|
| 1 | +#!/usr/bin/python |
| 2 | + |
| 3 | +# flyrc: |
| 4 | +# Loosely based upon geventirc (https://github.com/gwik/geventirc) |
| 5 | + |
| 6 | +import gevent |
| 7 | +import gevent.pool |
| 8 | +from gevent import queue, socket |
| 9 | +from flyrc import handler, message, util |
| 10 | +from time import time |
| 11 | + |
| 12 | +# Client events: connected, disconnected, error, global_send, global_recv, load, unload |
| 13 | +# (client event names are prefixed with client_) |
| 14 | +# 'connected' fires when the client connects to the IRC server. |
| 15 | +# 'disconnected' fires when the client's socket closes. |
| 16 | +# 'error' fires when the socket generates an error. |
| 17 | +# 'global_send' fires any time a message is sent. |
| 18 | +# 'global_recv' fires any time a message is received. |
| 19 | +# 'load' fires when the handler is loaded (note: only the loading handler's 'load' will be triggered) |
| 20 | +# 'unload' fires when the handler is unloaded (note: only the unloading handler's 'unload' will be triggered) |
| 21 | + |
| 22 | +class ClientError(Exception): |
| 23 | + """A generic flyrc client error.""" |
| 24 | + |
| 25 | +class DuplicateHandlerObject(ClientError): |
| 26 | + """Exception raised when an already-added handler is added a |
| 27 | + second time. |
| 28 | +
|
| 29 | + Attributes: |
| 30 | + obj - the object itself. |
| 31 | + """ |
| 32 | + def __init__(self, obj): |
| 33 | + self.obj = obj |
| 34 | + |
| 35 | +class MissingHandlerObject(ClientError): |
| 36 | + """Exception raised when attempting to remove a handler that isn't |
| 37 | + loaded. |
| 38 | +
|
| 39 | + Attributes: |
| 40 | + obj - the object itself. |
| 41 | + """ |
| 42 | + def __init__(self, obj): |
| 43 | + self.obj = obj |
| 44 | + |
| 45 | +class DependencyViolation(ClientError): |
| 46 | + """Exception raised when adding or removing a handler causes a |
| 47 | + dependency violation of some kind. |
| 48 | +
|
| 49 | + Attributes: |
| 50 | + args - the object subject to the conflict. |
| 51 | + """ |
| 52 | + |
| 53 | +class UnsatisfiedDependency(DependencyViolation): |
| 54 | + """Exception raised when a dependency isn't satisfied. |
| 55 | +
|
| 56 | + Attributes: |
| 57 | + args - the missing dependency. |
| 58 | + """ |
| 59 | + |
| 60 | +class LingeringDependency(DependencyViolation): |
| 61 | + """Exception raised when a handler being unloaded still has |
| 62 | + dependencies. |
| 63 | +
|
| 64 | + Attributes: |
| 65 | + args - the handler being unloaded. |
| 66 | + """ |
| 67 | + |
| 68 | +class InvalidDependencyTree(DependencyViolation): |
| 69 | + """Exception raised when the dependency tree has become invalid |
| 70 | + (probably due to prior Dependency exceptions.) |
| 71 | +
|
| 72 | + Attributes: |
| 73 | + args - the handler with invalid dependency information. |
| 74 | + """ |
| 75 | + |
| 76 | +class Client(object): |
| 77 | + def __init__(self, host, port, ssl=False, timeout=300): |
| 78 | + self._rqueue = queue.Queue() |
| 79 | + self._squeue = queue.Queue() |
| 80 | + self.host = host |
| 81 | + self.port = port |
| 82 | + self.ssl = ssl |
| 83 | + self._timeout = timeout |
| 84 | + self._socket = None |
| 85 | + self._group = gevent.pool.Group() |
| 86 | + self._coregroup = gevent.pool.Group() |
| 87 | + |
| 88 | + self._handlers = {} |
| 89 | + self._handlerobjects = {} |
| 90 | + |
| 91 | + self.throttle_delay = 2 |
| 92 | + self.throttle_burst = 5 |
| 93 | + |
| 94 | + self.enforce_order = False |
| 95 | + |
| 96 | + def _create_socket(self): |
| 97 | + sock = gevent.socket.socket() |
| 98 | + if self.ssl: |
| 99 | + sock = gevent.ssl.wrap_socket(sock) |
| 100 | + |
| 101 | + sock.setblocking(1) |
| 102 | + sock.settimeout(self._timeout) |
| 103 | + |
| 104 | + return sock |
| 105 | + |
| 106 | + def _ioerror(self, e, step): |
| 107 | + self.stop() |
| 108 | + self._rqueue.put(message.Error(e, step)) |
| 109 | + |
| 110 | + @property |
| 111 | + def timeout(self): |
| 112 | + """The TCP socket's timeout.""" |
| 113 | + return self._timeout |
| 114 | + |
| 115 | + @timeout.setter |
| 116 | + def timeout(self, t): |
| 117 | + self._timeout = t |
| 118 | + self._socket.settimeout(self._timeout) |
| 119 | + |
| 120 | + def start(self): |
| 121 | + try: |
| 122 | + self._socket = self._create_socket() |
| 123 | + self._socket.connect((self.host, self.port)) |
| 124 | + except socket.error, e: |
| 125 | + self._ioerror(e, message.Step.CONNECT) |
| 126 | + else: |
| 127 | + self._coregroup.spawn(self._send_loop) |
| 128 | + self._coregroup.spawn(self._recv_loop) |
| 129 | + self._handle('client_connected') |
| 130 | + self._coregroup.spawn(self._process_loop) |
| 131 | + |
| 132 | + def stop(self): |
| 133 | + if self._socket: |
| 134 | + self._socket.close() |
| 135 | + self._socket = None |
| 136 | + |
| 137 | + self._handle('client_disconnected') |
| 138 | + |
| 139 | + def shutdown(self): |
| 140 | + self.stop() |
| 141 | + self._coregroup.kill() |
| 142 | + self._rqueue = queue.Queue() |
| 143 | + self._squeue = queue.Queue() |
| 144 | + |
| 145 | + def join(self): |
| 146 | + self._coregroup.join() |
| 147 | + self._group.join() |
| 148 | + |
| 149 | + def _send_loop(self): |
| 150 | + buf = '' |
| 151 | + burst_remaining = self.throttle_burst * 1.0 |
| 152 | + last_message = 0 |
| 153 | + while True: |
| 154 | + msg = self._squeue.get() |
| 155 | + buf += msg.render().encode('utf-8', 'replace') + '\r\n' |
| 156 | + try: |
| 157 | + self._socket.sendall(buf) |
| 158 | + buf = '' |
| 159 | + except socket.error, e: |
| 160 | + print "I/O error in SEND: " + str(e) |
| 161 | + self._ioerror(e, message.Step.SEND) |
| 162 | + except AttributeError: |
| 163 | + # Socket closed, exit. |
| 164 | + return |
| 165 | + |
| 166 | + # Do throttling, but only if throttle_delay != 0. |
| 167 | + if self.throttle_delay: |
| 168 | + # Add any owed slots. |
| 169 | + if last_message: |
| 170 | + burst_remaining += (time() - last_message) / self.throttle_delay |
| 171 | + if (burst_remaining > self.throttle_burst): |
| 172 | + burst_remaining = self.throttle_burst * 1.0 |
| 173 | + |
| 174 | + # Penalize for the message that was just sent. |
| 175 | + last_message = time() |
| 176 | + burst_remaining -= 1 |
| 177 | + |
| 178 | + # Sleep if we're out of burst. |
| 179 | + if burst_remaining < 1: |
| 180 | + gevent.sleep(self.throttle_delay) |
| 181 | + else: |
| 182 | + gevent.sleep(0) |
| 183 | + |
| 184 | + def _process_loop(self): |
| 185 | + while True: |
| 186 | + if self.enforce_order: |
| 187 | + self._group.join() |
| 188 | + msg = self._rqueue.get() |
| 189 | + if hasattr(msg, 'e'): |
| 190 | + self._handle('client_error', msg) |
| 191 | + else: |
| 192 | + self._handle_recv(msg) |
| 193 | + |
| 194 | + def _recv_loop(self): |
| 195 | + buf = '' |
| 196 | + while True: |
| 197 | + try: |
| 198 | + buf += self._socket.recv(4096) |
| 199 | + except socket.error, e: |
| 200 | + print "I/O error in RECV: " + str(e) |
| 201 | + self._ioerror(e, message.Step.RECV) |
| 202 | + except AttributeError: |
| 203 | + # Socket has been closed, exit. |
| 204 | + return |
| 205 | + else: |
| 206 | + lines = buf.split('\r\n') |
| 207 | + buf = lines.pop() |
| 208 | + for line in lines: |
| 209 | + self._rqueue.put(message.parse_message(line)) |
| 210 | + gevent.sleep(0) |
| 211 | + |
| 212 | + def dependency_satisfier(self, dep): |
| 213 | + for item in self._handlerobjects.keys(): |
| 214 | + if isinstance(item, dep): |
| 215 | + return item |
| 216 | + return None |
| 217 | + |
| 218 | + def add_handler(self, handler): |
| 219 | + if handler in self._handlerobjects.keys(): |
| 220 | + raise DuplicateHandlerObject(item) |
| 221 | + |
| 222 | + dependencies, h_funcs = util.get_handler_properties(handler) |
| 223 | + for dep in dependencies: |
| 224 | + sat = self.dependency_satisfier(dep) |
| 225 | + if not sat: |
| 226 | + raise UnsatisfiedDependency(dep) |
| 227 | + else: |
| 228 | + # Increment refcount. |
| 229 | + self._handlerobjects[sat] += 1 |
| 230 | + |
| 231 | + self._handlerobjects[handler] = 0 |
| 232 | + |
| 233 | + for h_name in h_funcs.iterkeys(): |
| 234 | + if self._handlers.has_key(h_name): |
| 235 | + self._handlers[h_name].add(h_funcs[h_name]) |
| 236 | + else: |
| 237 | + self._handlers[h_name] = set([h_funcs[h_name]]) |
| 238 | + |
| 239 | + # Special - spawn just this instance, not all "client_load" handlers. |
| 240 | + if h_funcs.has_key('client_load'): |
| 241 | + self._group.spawn(h_funcs['client_load'], self) |
| 242 | + |
| 243 | + def remove_handler(self, handler): |
| 244 | + if handler not in self._handlerobjects.keys(): |
| 245 | + raise MissingHandlerObject(handler) |
| 246 | + |
| 247 | + if self._handlerobjects[handler] > 0: |
| 248 | + raise LingeringDependency(handler) |
| 249 | + |
| 250 | + del self._handlerobjects[handler] |
| 251 | + |
| 252 | + dependencies, h_funcs = util.get_handler_properties(handler) |
| 253 | + for dep in dependencies: |
| 254 | + sat = self.dependency_satisfier(dep) |
| 255 | + if not sat: |
| 256 | + raise InvalidDependencyTree(dep) |
| 257 | + else: |
| 258 | + # Decrement refcount. |
| 259 | + self._handlerobjects[sat] -= 1 |
| 260 | + if self._handlerobjects[sat] < 0: |
| 261 | + raise InvalidDependencyTree(sat) |
| 262 | + |
| 263 | + for h_name in h_funcs.iterkeys(): |
| 264 | + self._handlers[h_name] -= set([h_funcs[h_name]]) |
| 265 | + # If there aren't any handler functions left, remove that event entirely. |
| 266 | + if not self._handlers[h_name]: |
| 267 | + del self._handlers[h_name] |
| 268 | + |
| 269 | + # Special - spawn just this instance of client_unload. |
| 270 | + if h_funcs.has_key('client_unload'): |
| 271 | + self._group.spawn(h_funcs['client_unload'], self) |
| 272 | + |
| 273 | + def _handle(self, hname, *args, **kwargs): |
| 274 | + if self._handlers.has_key(hname): |
| 275 | + for handler in self._handlers[hname]: |
| 276 | + self._group.spawn(handler, self, *args, **kwargs) |
| 277 | + |
| 278 | + def _handle_recv(self, message): |
| 279 | + self._handle('client_global_recv', message) |
| 280 | + self._handle(message.command.upper(), message) |
| 281 | + |
| 282 | + def send(self, message): |
| 283 | + self._handle('client_global_send', message) |
| 284 | + self._squeue.put(message) |
| 285 | + |
| 286 | + def trigger_handler(self, handler, *args, **kwargs): |
| 287 | + self._handle(handler, *args, **kwargs) |
| 288 | + |
| 289 | + def get_handled_events(self): |
| 290 | + return self._handlers.keys() |
| 291 | + |
| 292 | +# A simple client that can stay connected to an IRC network and supports NickServ/SASL authentication. |
| 293 | +class SimpleClient(Client): |
| 294 | + def __init__(self, nick, user, gecos, host, port, ssl=False, timeout=300, autoreconnect=False, version=None): |
| 295 | + super(SimpleClient, self).__init__(host, port, ssl, timeout) |
| 296 | + |
| 297 | + self.add_handler(handler.Ping()) |
| 298 | + self.add_handler(handler.User(nick, user, gecos)) |
| 299 | + self.add_handler(handler.NickInUse()) |
| 300 | + self.add_handler(handler.MessageProcessor()) |
| 301 | + |
| 302 | + if autoreconnect: |
| 303 | + self.add_handler(handler.AutoReconnect()) |
| 304 | + else: |
| 305 | + self.add_handler(handler.GenericDisconnect()) |
| 306 | + |
| 307 | + if version: |
| 308 | + self.add_handler(handler.BasicCTCP(version)) |
| 309 | + else: |
| 310 | + self.add_handler(handler.BasicCTCP()) |
0 commit comments