diff --git a/pydle/__init__.py b/pydle/__init__.py index 2f8acff..61a3032 100644 --- a/pydle/__init__.py +++ b/pydle/__init__.py @@ -1,12 +1,15 @@ -from . import async, connection, protocol, client, features +from . import connection, protocol, client, features -from .async import coroutine, Future from .client import Error, NotInChannel, AlreadyInChannel, BasicClient, ClientPool -from .features.ircv3.cap import NEGOTIATING as CAPABILITY_NEGOTIATING, FAILED as CAPABILITY_FAILED, NEGOTIATED as CAPABILITY_NEGOTIATED +from .features.ircv3.cap import NEGOTIATING as CAPABILITY_NEGOTIATING, FAILED as CAPABILITY_FAILED, \ + NEGOTIATED as CAPABILITY_NEGOTIATED + +# noinspection PyUnresolvedReferences +from asyncio import coroutine, Future __name__ = 'pydle' -__version__ = '0.8.4' -__version_info__ = (0, 8, 4) +__version__ = '0.8.5' +__version_info__ = (0, 8, 5) __license__ = 'BSD' @@ -22,13 +25,16 @@ def featurize(*features): return 0 sorted_features = sorted(features, key=cmp_to_key(compare_subclass)) - name = 'FeaturizedClient[{features}]'.format(features=', '.join(feature.__name__ for feature in sorted_features)) + name = 'FeaturizedClient[{features}]'.format( + features=', '.join(feature.__name__ for feature in sorted_features)) return type(name, tuple(sorted_features), {}) + class Client(featurize(*features.ALL)): """ A fully featured IRC client. """ pass + class MinimalClient(featurize(*features.LITE)): """ A cut-down, less-featured IRC client. """ pass diff --git a/pydle/async.py b/pydle/async.py deleted file mode 100644 index f7014ee..0000000 --- a/pydle/async.py +++ /dev/null @@ -1,215 +0,0 @@ -## async.py -# Light wrapper around whatever async library pydle uses. -import functools -import datetime -import traceback -import asyncio - -FUTURE_TIMEOUT = 30 - - -class Future(asyncio.Future): - """ - A future. An object that represents a result that has yet to be created or returned. - """ - -def coroutine(f): - return asyncio.coroutine(f) - -def parallel(*futures): - return asyncio.gather(*futures) - - -class EventLoop: - """ A light wrapper around what event loop mechanism pydle uses underneath. """ - - def __init__(self, loop=None): - self.loop = loop or asyncio.get_event_loop() - self.future_timeout = FUTURE_TIMEOUT - self._future_timeouts = {} - self._tasks = [] - - def __del__(self): - self.loop.close() - - - @property - def running(self): - return self.loop.is_running() - - def create_future(self): - return Future(loop=self.loop) - - @asyncio.coroutine - def connect(self, dest, tls=None, **kwargs): - (host, port) = dest - return (yield from asyncio.open_connection(host=host, port=port, ssl=tls, **kwargs)) - - def on_future(self, _future, _callback, *_args, **_kwargs): - """ Add a callback for when the given future has been resolved. """ - callback = functools.partial(self._do_on_future, _callback, _args, _kwargs) - - # Create timeout handler and regular handler. - self._future_timeouts[_future] = self.schedule_in(self.future_timeout, callback) - future.add_done_callback(callback) - - def _do_on_future(self, callback, args, kwargs, future): - # This was a time-out. - if not future.done(): - future.set_exception(TimeoutError('Future timed out before yielding a result.')) - del self._future_timeouts[future] - # This was a time-out that already has been handled. - elif isinstance(future.exception(), TimeoutError): - return - # A regular result. Cancel the timeout. - else: - self.unschedule(self._future_timeouts.pop(future)) - - # Call callback. - callback(*args, **kwargs) - - - def schedule(self, _callback, *_args, **_kwargs): - """ - Schedule a callback to be ran as soon as possible in this loop. - Will return an opaque handle that can be passed to `unschedule` to unschedule the function. - """ - @coroutine - @functools.wraps(_callback) - def inner(): - _callback(*_args, **_kwargs) - - return self.schedule_async(inner()) - - def schedule_async(self, _callback): - """ - Schedule a coroutine to be ran as soon as possible in this loop. - Will return an opaque handle that can be passed to `unschedule` to unschedule the function. - """ - @coroutine - @functools.wraps(_callback) - def inner(): - try: - return (yield from _callback) - except (GeneratorExit, asyncio.CancelledError): - raise - except: - traceback.print_exc() - - task = asyncio.ensure_future(inner()) - self._tasks.append(task) - return task - - def schedule_in(self, _when, _callback, *_args, **_kwargs): - """ - Schedule a callback to be ran as soon as possible after `when` seconds have passed. - Will return an opaque handle that can be passed to `unschedule` to unschedule the function. - """ - if isinstance(_when, datetime.timedelta): - _when = _when.total_seconds() - - @coroutine - @functools.wraps(_callback) - def inner(): - yield from asyncio.sleep(_when) - _callback(*_args, **_kwargs) - - return self.schedule_async(inner()) - - def schedule_async_in(self, _when, _callback): - """ - Schedule a coroutine to be ran as soon as possible after `when` seconds have passed. - Will return an opaque handle that can be passed to `unschedule` to unschedule the function. - """ - if isinstance(_when, datetime.timedelta): - _when = _when.total_seconds() - - @coroutine - @functools.wraps(_callback) - def inner(): - yield from asyncio.sleep(_when) - yield from _callback - - return self.schedule_async(inner()) - - def schedule_periodically(self, _interval, _callback, *_args, **_kwargs): - """ - Schedule a callback to be ran every `interval` seconds. - Will return an opaque handle that can be passed to unschedule() to unschedule the interval function. - A function will also stop being scheduled if it returns False or raises an Exception. - """ - if isinstance(_interval, datetime.timedelta): - _interval = _interval.total_seconds() - - @coroutine - @functools.wraps(_callback) - def inner(): - while True: - yield from asyncio.sleep(_when) - ret = _callback(*_args, **_kwargs) - if ret is False: - break - - return self.schedule_async(inner()) - - def schedule_async_periodically(self, _interval, _callback, *_args, **_kwargs): - """ - Schedule a coroutine to be ran every `interval` seconds. - Will return an opaque handle that can be passed to unschedule() to unschedule the interval function. - A function will also stop being scheduled if it returns False or raises an Exception. - """ - if isinstance(_when, datetime.timedelta): - _when = _when.total_seconds() - - @coroutine - @functools.wraps(_callback) - def inner(): - while True: - yield from asyncio.sleep(_when) - ret = yield from _callback(*_args, **_kwargs) - if ret is False: - break - - return self.schedule_async(inner()) - - def is_scheduled(self, handle): - """ Return whether or not the given handle is still scheduled. """ - return not handle.cancelled() - - def unschedule(self, handle): - """ Unschedule a given timeout or periodical callback. """ - if self.is_scheduled(handle): - self.schedule(handle.cancel) - - def _unschedule_all(self): - for task in self._tasks: - task.cancel() - - - def run(self): - """ Run the event loop. """ - if not self.running: - self.loop.run_forever() - - def run_with(self, func): - """ Run loop, call function, stop loop. If function returns a future, run until the future has been resolved. """ - @coroutine - @functools.wraps(func) - def inner(): - yield from func - self._unschedule_all() - self.loop.run_until_complete(asyncio.ensure_future(inner())) - - def run_until(self, future): - """ Run until future is resolved. """ - @coroutine - def inner(): - yield from future - self._unschedule_all() - self.loop.run_until_complete(asyncio.ensure_future(inner())) - - def stop(self): - """ Stop the event loop. """ - if self.running: - self._unschedule_all() - self.loop.stop() diff --git a/pydle/client.py b/pydle/client.py index ebb3808..ced10d7 100644 --- a/pydle/client.py +++ b/pydle/client.py @@ -1,8 +1,9 @@ ## client.py # Basic IRC client implementation. import logging +from asyncio import ensure_future, new_event_loop, BaseEventLoop, gather, TimerHandle, get_event_loop +from typing import Set -from . import async from . import connection from . import protocol @@ -14,11 +15,13 @@ class Error(Exception): """ Base class for all pydle errors. """ pass + class NotInChannel(Error): def __init__(self, channel): super().__init__('Not in channel: {}'.format(channel)) self.channel = channel + class AlreadyInChannel(Error): def __init__(self, channel): super().__init__('Already in channel: {}'.format(channel)) @@ -36,7 +39,8 @@ class BasicClient: RECONNECT_DELAYED = True RECONNECT_DELAYS = [0, 5, 10, 30, 120, 600] - def __init__(self, nickname, fallback_nicknames=[], username=None, realname=None, eventloop=None, **kwargs): + def __init__(self, nickname, fallback_nicknames=[], username=None, realname=None, + eventloop=None, **kwargs): """ Create a client. """ self._nicknames = [nickname] + fallback_nicknames self.username = username or nickname.lower() @@ -44,7 +48,8 @@ class BasicClient: if eventloop: self.eventloop = eventloop else: - self.eventloop = async.EventLoop() + self.eventloop: BaseEventLoop = get_event_loop() + self.own_eventloop = not eventloop self._reset_connection_attributes() self._reset_attributes() @@ -78,19 +83,17 @@ class BasicClient: self._autojoin_channels = [] self._reconnect_attempts = 0 - ## Connection. def run(self, *args, **kwargs): """ Connect and run bot in event loop. """ - self.eventloop.schedule_async(self.connect(*args, **kwargs)) + self.eventloop.run_until_complete(self.connect(*args, **kwargs)) try: - self.eventloop.run() + self.eventloop.run_forever() finally: self.eventloop.stop() - @async.coroutine - def connect(self, hostname=None, port=None, reconnect=False, **kwargs): + async def connect(self, hostname=None, port=None, reconnect=False, **kwargs): """ Connect to IRC server. """ if (not hostname or not port) and not reconnect: raise ValueError('Have to specify hostname and port if not reconnecting.') @@ -102,33 +105,28 @@ class BasicClient: # Reset attributes and connect. if not reconnect: self._reset_connection_attributes() - try: - yield from self._connect(hostname=hostname, port=port, reconnect=reconnect, **kwargs) - except OSError: - yield from self.on_disconnect(expected=False) + await self._connect(hostname=hostname, port=port, reconnect=reconnect, **kwargs) # Set logger name. if self.server_tag: self.logger = logging.getLogger(self.__class__.__name__ + ':' + self.server_tag) - - self.eventloop.schedule_async(self.handle_forever()) + ensure_future(self.handle_forever(), loop=self.eventloop) def disconnect(self, expected=True): """ Disconnect from server. """ if self.connected: # Unschedule ping checker. if self._ping_checker_handle: - self.eventloop.unschedule(self._ping_checker_handle) + self._ping_checker_handle.cancel() # Schedule disconnect. - self.eventloop.schedule_async(self._disconnect(expected)) + ensure_future(self._disconnect(expected), loop=self.eventloop) - @async.coroutine - def _disconnect(self, expected): + async def _disconnect(self, expected): # Shutdown connection. - yield from self.connection.disconnect() + await self.connection.disconnect() # Callback. - yield from self.on_disconnect(expected) + await self.on_disconnect(expected) # Shut down event loop. if expected and self.own_eventloop: @@ -137,17 +135,18 @@ class BasicClient: # Reset any attributes. self._reset_attributes() - @async.coroutine - def _connect(self, hostname, port, reconnect=False, channels=[], encoding=protocol.DEFAULT_ENCODING, source_address=None): + async def _connect(self, hostname, port, reconnect=False, channels=[], + encoding=protocol.DEFAULT_ENCODING, source_address=None): """ Connect to IRC host. """ # Create connection if we can't reuse it. if not reconnect or not self.connection: self._autojoin_channels = channels - self.connection = connection.Connection(hostname, port, source_address=source_address, eventloop=self.eventloop) + self.connection = connection.Connection(hostname, port, source_address=source_address, + eventloop=self.eventloop) self.encoding = encoding # Connect. - yield from self.connection.connect() + await self.connection.connect() def _reconnect_delay(self): """ Calculate reconnection delay. """ @@ -159,12 +158,12 @@ class BasicClient: else: return 0 - @async.coroutine - def _perform_ping_timeout(self): + async def _perform_ping_timeout(self): """ Handle timeout gracefully. """ - error = TimeoutError('Ping timeout: no data received from server in {timeout} seconds.'.format(timeout=self.PING_TIMEOUT)) - yield from self.on_data_error(error) - + error = TimeoutError( + 'Ping timeout: no data received from server in {timeout} seconds.'.format( + timeout=self.PING_TIMEOUT)) + await self.on_data_error(error) ## Internal database management. @@ -179,7 +178,6 @@ class BasicClient: self._destroy_user(user, channel) del self.channels[channel] - def _create_user(self, nickname): # Servers are NOT users. if not nickname or '.' in nickname: @@ -198,7 +196,6 @@ class BasicClient: self._create_user(nick) if nick not in self.users: return - self.users[nick].update(metadata) def _rename_user(self, user, new): @@ -219,7 +216,7 @@ class BasicClient: def _destroy_user(self, nickname, channel=None): if channel: - channels = [ self.channels[channel] ] + channels = [self.channels[channel]] else: channels = self.channels.values() @@ -237,13 +234,13 @@ class BasicClient: raise NotImplementedError() def _format_user_mask(self, nickname): - user = self.users.get(nickname, { "nickname": nickname, "username": "*", "hostname": "*" }) - return self._format_host_mask(user['nickname'], user['username'] or '*', user['hostname'] or '*') + user = self.users.get(nickname, {"nickname": nickname, "username": "*", "hostname": "*"}) + return self._format_host_mask(user['nickname'], user['username'] or '*', + user['hostname'] or '*') def _format_host_mask(self, nick, user, host): return '{n}!{u}@{h}'.format(n=nick, u=user, h=host) - ## IRC helpers. def is_channel(self, chan): @@ -262,7 +259,6 @@ class BasicClient: """ Check if given channel names are equal. """ return left == right - ## IRC attributes. @property @@ -297,49 +293,44 @@ class BasicClient: else: return None - ## IRC API. - @async.coroutine - def raw(self, message): + async def raw(self, message): """ Send raw command. """ - yield from self._send(message) + await self._send(message) - @async.coroutine - def rawmsg(self, command, *args, **kwargs): + async def rawmsg(self, command, *args, **kwargs): """ Send raw message. """ message = str(self._create_message(command, *args, **kwargs)) - yield from self._send(message) - + await self._send(message) ## Overloadable callbacks. - @async.coroutine - def on_connect(self): + async def on_connect(self): """ Callback called when the client has connected successfully. """ # Reset reconnect attempts. self._reconnect_attempts = 0 - @async.coroutine - def on_disconnect(self, expected): + async def on_disconnect(self, expected): if not expected: # Unexpected disconnect. Reconnect? - if self.RECONNECT_ON_ERROR and (self.RECONNECT_MAX_ATTEMPTS is None or self._reconnect_attempts < self.RECONNECT_MAX_ATTEMPTS): + if self.RECONNECT_ON_ERROR and ( + self.RECONNECT_MAX_ATTEMPTS is None or self._reconnect_attempts < self.RECONNECT_MAX_ATTEMPTS): # Calculate reconnect delay. delay = self._reconnect_delay() self._reconnect_attempts += 1 if delay > 0: - self.logger.error('Unexpected disconnect. Attempting to reconnect within %s seconds.', delay) + self.logger.error( + 'Unexpected disconnect. Attempting to reconnect within %s seconds.', delay) else: self.logger.error('Unexpected disconnect. Attempting to reconnect.') # Wait and reconnect. - self.eventloop.schedule_async_in(delay, self.connect(reconnect=True)) + self.eventloop.call_soon(delay, self.connect(reconnect=True)) else: self.logger.error('Unexpected disconnect. Giving up.') - ## Message dispatch. def _has_message(self): @@ -352,56 +343,54 @@ class BasicClient: def _parse_message(self): raise NotImplementedError() - @async.coroutine - def _send(self, input): + async def _send(self, input): if not isinstance(input, (bytes, str)): input = str(input) if isinstance(input, str): input = input.encode(self.encoding) self.logger.debug('>> %s', input.decode(self.encoding)) - yield from self.connection.send(input) + await self.connection.send(input) - @async.coroutine - def handle_forever(self): + async def handle_forever(self): """ Handle data forever. """ while self.connected: - data = yield from self.connection.recv() + data = await self.connection.recv() if not data: if self.connected: self.disconnect(expected=False) break - yield from self.on_data(data) - + await self.on_data(data) ## Raw message handlers. - @async.coroutine - def on_data(self, data): + async def on_data(self, data): """ Handle received data. """ self._receive_buffer += data # Schedule new timeout event. if self._ping_checker_handle: - self.eventloop.unschedule(self._ping_checker_handle) - self._ping_checker_handle = self.eventloop.schedule_async_in(self.PING_TIMEOUT, self._perform_ping_timeout()) + self._ping_checker_handle.cancel() + + self._ping_checker_handle: TimerHandle = self.eventloop.call_later(self.PING_TIMEOUT, + self._perform_ping_timeout) while self._has_message(): message = self._parse_message() - self.eventloop.schedule_async(self.on_raw(message)) + ensure_future(self.on_raw(message), loop=self.eventloop) - @async.coroutine - def on_data_error(self, exception): + async def on_data_error(self, exception): """ Handle error. """ - self.logger.error('Encountered error on socket.', exc_info=(type(exception), exception, None)) + self.logger.error('Encountered error on socket.', + exc_info=(type(exception), exception, None)) self.disconnect(expected=False) - @async.coroutine - def on_raw(self, message): + async def on_raw(self, message): """ Handle a single message. """ self.logger.debug('<< %s', message._raw) if not message._valid: - self.logger.warning('Encountered strictly invalid IRC message from server: %s', message._raw) + self.logger.warning('Encountered strictly invalid IRC message from server: %s', + message._raw) if isinstance(message.command, int): cmd = str(message.command).zfill(3) @@ -418,17 +407,16 @@ class BasicClient: handler = getattr(self, method) self._handler_top_level = False - yield from handler(message) + await handler(message) except: self.logger.exception('Failed to execute %s handler.', method) - @async.coroutine - def on_unknown(self, message): + async def on_unknown(self, message): """ Unknown command. """ - self.logger.warning('Unknown command: [%s] %s %s', message.source, message.command, message.params) + self.logger.warning('Unknown command: [%s] %s %s', message.source, message.command, + message.params) - @async.coroutine - def _ignored(self, message): + async def _ignored(self, message): """ Ignore message. """ pass @@ -453,14 +441,17 @@ class ClientPool: """ A pool of clients that are ran and handled in parallel. """ def __init__(self, clients=None, eventloop=None): - self.eventloop = eventloop or async.EventLoop() - self.clients = set(clients or []) + self.eventloop = eventloop if eventloop else new_event_loop() + self.clients: Set[BasicClient] = set(clients or []) self.connect_args = {} - def connect(self, client, *args, **kwargs): + def connect(self, client: BasicClient, *args, **kwargs): """ Add client to pool. """ self.clients.add(client) self.connect_args[client] = (args, kwargs) + # hack the clients event loop to use the pools own event loop + client.eventloop = self.eventloop + # necessary to run multiple clients in the same thread via the pool def disconnect(self, client): """ Remove client from pool. """ @@ -471,16 +462,23 @@ class ClientPool: def __contains__(self, item): return item in self.clients - ## High-level. def handle_forever(self): """ Main loop of the pool: handle clients forever, until the event loop is stopped. """ - for c in self.clients: - args, kwargs = self.connect_args[c] - self.eventloop.schedule_async(c.connect(*args, **kwargs)) + # container for all the client connection coros + connection_list = [] + for client in self.clients: + args, kwargs = self.connect_args[client] + connection_list.append(client.connect(*args, **kwargs)) + # single future for executing the connections + connections = gather(*connection_list, loop=self.eventloop) - self.eventloop.run() + # run the connections + self.eventloop.run_until_complete(connections) - for c in self.clients: - c.disconnect() + # run the clients + self.eventloop.run_forever() + + for client in self.clients: + client.disconnect() diff --git a/pydle/connection.py b/pydle/connection.py index 9c1dd5b..49dcd6a 100644 --- a/pydle/connection.py +++ b/pydle/connection.py @@ -1,9 +1,7 @@ -import sys +import asyncio import os.path as path - import ssl - -from . import async +import sys __all__ = ['Connection'] @@ -21,7 +19,9 @@ class Connection: """ A TCP connection over the IRC protocol. """ CONNECT_TIMEOUT = 10 - def __init__(self, hostname, port, tls=False, tls_verify=True, tls_certificate_file=None, tls_certificate_keyfile=None, tls_certificate_password=None, ping_timeout=240, source_address=None, eventloop=None): + def __init__(self, hostname, port, tls=False, tls_verify=True, tls_certificate_file=None, + tls_certificate_keyfile=None, tls_certificate_password=None, ping_timeout=240, + source_address=None, eventloop=None): self.hostname = hostname self.port = port self.source_address = source_address @@ -36,16 +36,22 @@ class Connection: self.reader = None self.writer = None - self.eventloop = eventloop or async.EventLoop() + self.eventloop = eventloop or asyncio.new_event_loop() - @async.coroutine - def connect(self): + async def connect(self): """ Connect to target. """ self.tls_context = None if self.tls: self.tls_context = self.create_tls_context() - (self.reader, self.writer) = yield from self.eventloop.connect((self.hostname, self.port), local_addr=self.source_address, tls=self.tls_context) + + (self.reader, self.writer) = await asyncio.open_connection( + host=self.hostname, + port=self.port, + local_addr=self.source_address, + ssl=self.tls_context, + loop=self.eventloop + ) def create_tls_context(self): """ Transform our regular socket into a TLS socket. """ @@ -54,7 +60,8 @@ class Connection: # Load client/server certificate. if self.tls_certificate_file: - tls_context.load_cert_chain(self.tls_certificate_file, self.tls_certificate_keyfile, password=self.tls_certificate_password) + tls_context.load_cert_chain(self.tls_certificate_file, self.tls_certificate_keyfile, + password=self.tls_certificate_password) # Set some relevant options: # - No server should use SSLv2 or SSLv3 any more, they are outdated and full of security holes. (RFC6176, RFC7568) @@ -97,8 +104,7 @@ class Connection: return None - @async.coroutine - def disconnect(self): + async def disconnect(self): """ Disconnect from target. """ if not self.connected: return @@ -112,18 +118,14 @@ class Connection: """ Whether this connection is... connected to something. """ return self.reader is not None and self.writer is not None - def stop(self): """ Stop event loop. """ self.eventloop.schedule(lambda: self.eventloop.stop()) - - @async.coroutine - def send(self, data): + async def send(self, data): """ Add data to send queue. """ self.writer.write(data) - yield from self.writer.drain() + await self.writer.drain() - @async.coroutine - def recv(self): - return (yield from self.reader.readline()) + async def recv(self): + return await self.reader.readline() diff --git a/pydle/features/account.py b/pydle/features/account.py index fd9e5ee..e181561 100644 --- a/pydle/features/account.py +++ b/pydle/features/account.py @@ -1,7 +1,7 @@ ## account.py # Account system support. -from pydle import async from pydle.features import rfc1459 +import asyncio class AccountSupport(rfc1459.RFC1459Support): @@ -18,24 +18,20 @@ class AccountSupport(rfc1459.RFC1459Support): def _rename_user(self, user, new): super()._rename_user(user, new) # Unset account info to be certain until we get a new response. - self._sync_user(new, { 'account': None, 'identified': False }) + self._sync_user(new, {'account': None, 'identified': False}) self.whois(new) - ## IRC API. - - @async.coroutine + @asyncio.coroutine def whois(self, nickname): info = yield from super().whois(nickname) info.setdefault('account', None) info.setdefault('identified', False) return info - ## Message handlers. - @async.coroutine - def on_raw_307(self, message): + async def on_raw_307(self, message): """ WHOIS: User has identified for this nickname. (Anope) """ target, nickname = message.params[:2] info = { @@ -47,8 +43,7 @@ class AccountSupport(rfc1459.RFC1459Support): if nickname in self._pending['whois']: self._whois_info[nickname].update(info) - @async.coroutine - def on_raw_330(self, message): + async def on_raw_330(self, message): """ WHOIS account name (Atheme). """ target, nickname, account = message.params[:3] info = { diff --git a/pydle/features/ctcp.py b/pydle/features/ctcp.py index 30a054e..84cd631 100644 --- a/pydle/features/ctcp.py +++ b/pydle/features/ctcp.py @@ -1,6 +1,5 @@ ## ctcp.py # Client-to-Client-Protocol (CTCP) support. -from pydle import async import pydle.protocol from pydle.features import rfc1459 @@ -16,8 +15,7 @@ class CTCPSupport(rfc1459.RFC1459Support): ## Callbacks. - @async.coroutine - def on_ctcp(self, by, target, what, contents): + async def on_ctcp(self, by, target, what, contents): """ Callback called when the user received a CTCP message. Client subclasses can override on_ctcp_ to be called when receiving a message of that specific CTCP type, @@ -25,8 +23,7 @@ class CTCPSupport(rfc1459.RFC1459Support): """ pass - @async.coroutine - def on_ctcp_reply(self, by, target, what, response): + async def on_ctcp_reply(self, by, target, what, response): """ Callback called when the user received a CTCP response. Client subclasses can override on_ctcp__reply to be called when receiving a reply of that specific CTCP type, @@ -34,8 +31,7 @@ class CTCPSupport(rfc1459.RFC1459Support): """ pass - @async.coroutine - def on_ctcp_version(self, by, target, contents): + async def on_ctcp_version(self, by, target, contents): """ Built-in CTCP version as some networks seem to require it. """ import pydle @@ -45,27 +41,24 @@ class CTCPSupport(rfc1459.RFC1459Support): ## IRC API. - @async.coroutine - def ctcp(self, target, query, contents=None): + async def ctcp(self, target, query, contents=None): """ Send a CTCP request to a target. """ if self.is_channel(target) and not self.in_channel(target): raise client.NotInChannel(target) - yield from self.message(target, construct_ctcp(query, contents)) + await self.message(target, construct_ctcp(query, contents)) - @async.coroutine - def ctcp_reply(self, target, query, response): + async def ctcp_reply(self, target, query, response): """ Send a CTCP reply to a target. """ if self.is_channel(target) and not self.in_channel(target): raise client.NotInChannel(target) - yield from self.notice(target, construct_ctcp(query, response)) + await self.notice(target, construct_ctcp(query, response)) ## Handler overrides. - @async.coroutine - def on_raw_privmsg(self, message): + async def on_raw_privmsg(self, message): """ Modify PRIVMSG to redirect CTCP messages. """ nick, metadata = self._parse_user(message.source) target, msg = message.params @@ -77,14 +70,14 @@ class CTCPSupport(rfc1459.RFC1459Support): # Find dedicated handler if it exists. attr = 'on_ctcp_' + pydle.protocol.identifierify(type) if hasattr(self, attr): - yield from getattr(self, attr)(nick, target, contents) + await getattr(self, attr)(nick, target, contents) # Invoke global handler. - yield from self.on_ctcp(nick, target, type, contents) + await self.on_ctcp(nick, target, type, contents) else: - yield from super().on_raw_privmsg(message) + await super().on_raw_privmsg(message) - @async.coroutine - def on_raw_notice(self, message): + + async def on_raw_notice(self, message): """ Modify NOTICE to redirect CTCP messages. """ nick, metadata = self._parse_user(message.source) target, msg = message.params @@ -96,11 +89,11 @@ class CTCPSupport(rfc1459.RFC1459Support): # Find dedicated handler if it exists. attr = 'on_ctcp_' + pydle.protocol.identifierify(type) + '_reply' if hasattr(self, attr): - yield from getattr(self, attr)(user, target, response) + await getattr(self, attr)(user, target, response) # Invoke global handler. - yield from self.on_ctcp_reply(user, target, type, response) + await self.on_ctcp_reply(user, target, type, response) else: - yield from super().on_raw_notice(message) + await super().on_raw_notice(message) ## Helpers. diff --git a/pydle/features/ircv3/cap.py b/pydle/features/ircv3/cap.py index 448e5f3..c64cdb1 100644 --- a/pydle/features/ircv3/cap.py +++ b/pydle/features/ircv3/cap.py @@ -1,7 +1,6 @@ ## cap.py # Server <-> client optional extension indication support. # See also: http://ircv3.atheme.org/specification/capability-negotiation-3.1 -from pydle import async import pydle.protocol from pydle.features import rfc1459 @@ -29,17 +28,16 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): self._capabilities_requested = set() self._capabilities_negotiating = set() - @async.coroutine - def _register(self): + async def _register(self): """ Hijack registration to send a CAP LS first. """ if self.registered: return # Ask server to list capabilities. - yield from self.rawmsg('CAP', 'LS', '302') + await self.rawmsg('CAP', 'LS', '302') # Register as usual. - yield from super()._register() + await super()._register() def _capability_normalize(self, cap): cap = cap.lstrip(PREFIXES).lower() @@ -53,19 +51,17 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): ## API. - @async.coroutine - def _capability_negotiated(self, capab): + async def _capability_negotiated(self, capab): """ Mark capability as negotiated, and end negotiation if we're done. """ self._capabilities_negotiating.discard(capab) if not self._capabilities_requested and not self._capabilities_negotiating: - yield from self.rawmsg('CAP', 'END') + await self.rawmsg('CAP', 'END') ## Message handlers. - @async.coroutine - def on_raw_cap(self, message): + async def on_raw_cap(self, message): """ Handle CAP message. """ target, subcommand = message.params[:2] params = message.params[2:] @@ -73,12 +69,11 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): # Call handler. attr = 'on_raw_cap_' + pydle.protocol.identifierify(subcommand) if hasattr(self, attr): - yield from getattr(self, attr)(params) + await getattr(self, attr)(params) else: self.logger.warning('Unknown CAP subcommand sent from server: %s', subcommand) - @async.coroutine - def on_raw_cap_ls(self, params): + async def on_raw_cap_ls(self, params): """ Update capability mapping. Request capabilities. """ to_request = set() @@ -91,7 +86,7 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): # Check if we support the capability. attr = 'on_capability_' + pydle.protocol.identifierify(capab) + '_available' - supported = (yield from getattr(self, attr)(value)) if hasattr(self, attr) else False + supported = (await getattr(self, attr)(value)) if hasattr(self, attr) else False if supported: if isinstance(supported, str): @@ -104,13 +99,12 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): if to_request: # Request some capabilities. self._capabilities_requested.update(x.split(CAPABILITY_VALUE_DIVIDER, 1)[0] for x in to_request) - yield from self.rawmsg('CAP', 'REQ', ' '.join(to_request)) + await self.rawmsg('CAP', 'REQ', ' '.join(to_request)) else: # No capabilities requested, end negotiation. - yield from self.rawmsg('CAP', 'END') + await self.rawmsg('CAP', 'END') - @async.coroutine - def on_raw_cap_list(self, params): + async def on_raw_cap_list(self, params): """ Update active capabilities. """ self._capabilities = { capab: False for capab in self._capabilities } @@ -118,8 +112,7 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): capab, value = self._capability_normalize(capab) self._capabilities[capab] = value if value else True - @async.coroutine - def on_raw_cap_ack(self, params): + async def on_raw_cap_ack(self, params): """ Update active capabilities: requested capability accepted. """ for capab in params[0].split(): cp, value = self._capability_normalize(capab) @@ -139,11 +132,11 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): # Indicate we're gonna use this capability if needed. if capab.startswith(ACKNOWLEDGEMENT_REQUIRED_PREFIX): - yield from self.rawmsg('CAP', 'ACK', cp) + await self.rawmsg('CAP', 'ACK', cp) # Run callback. if hasattr(self, attr): - status = yield from getattr(self, attr)() + status = await getattr(self, attr)() else: status = NEGOTIATED @@ -154,15 +147,14 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): # Ruh-roh, negotiation failed. Disable the capability. self.logger.warning('Capability negotiation for %s failed. Attempting to disable capability again.', cp) - yield from self.rawmsg('CAP', 'REQ', '-' + cp) + await self.rawmsg('CAP', 'REQ', '-' + cp) self._capabilities_requested.add(cp) # If we have no capabilities left to process, end it. if not self._capabilities_requested and not self._capabilities_negotiating: - yield from self.rawmsg('CAP', 'END') + await self.rawmsg('CAP', 'END') - @async.coroutine - def on_raw_cap_nak(self, params): + async def on_raw_cap_nak(self, params): """ Update active capabilities: requested capability rejected. """ for capab in params[0].split(): capab, _ = self._capability_normalize(capab) @@ -171,39 +163,34 @@ class CapabilityNegotiationSupport(rfc1459.RFC1459Support): # If we have no capabilities left to process, end it. if not self._capabilities_requested and not self._capabilities_negotiating: - yield from self.rawmsg('CAP', 'END') + await self.rawmsg('CAP', 'END') - @async.coroutine - def on_raw_cap_del(self, params): + async def on_raw_cap_del(self, params): for capab in params[0].split(): attr = 'on_capability_{}_disabled'.format(pydle.protocol.identifierify(capab)) if self._capabilities.get(capab, False) and hasattr(self, attr): - yield from getattr(self, attr)() - yield from self.on_raw_cap_nak(params) + await getattr(self, attr)() + await self.on_raw_cap_nak(params) - @async.coroutine - def on_raw_cap_new(self, params): - yield from self.on_raw_cap_ls(params) + async def on_raw_cap_new(self, params): + await self.on_raw_cap_ls(params) - @async.coroutine - def on_raw_410(self, message): + async def on_raw_410(self, message): """ Unknown CAP subcommand or CAP error. Force-end negotiations. """ self.logger.error('Server sent "Unknown CAP subcommand: %s". Aborting capability negotiation.', message.params[0]) self._capabilities_requested = set() self._capabilities_negotiating = set() - yield from self.rawmsg('CAP', 'END') + await self.rawmsg('CAP', 'END') - @async.coroutine - def on_raw_421(self, message): + async def on_raw_421(self, message): """ Hijack to ignore the absence of a CAP command. """ if message.params[0] == 'CAP': return - yield from super().on_raw_421(message) + await super().on_raw_421(message) - @async.coroutine - def on_raw_451(self, message): + async def on_raw_451(self, message): """ Hijack to ignore the absence of a CAP command. """ if message.params[0] == 'CAP': return - yield from super().on_raw_451(message) + await super().on_raw_451(message) diff --git a/pydle/features/ircv3/ircv3_1.py b/pydle/features/ircv3/ircv3_1.py index 07cc31d..ad47a0e 100644 --- a/pydle/features/ircv3/ircv3_1.py +++ b/pydle/features/ircv3/ircv3_1.py @@ -1,6 +1,5 @@ ## ircv3_1.py # IRCv3.1 full spec support. -from pydle import async from pydle.features import account, tls from . import cap from . import sasl @@ -27,36 +26,30 @@ class IRCv3_1Support(sasl.SASLSupport, cap.CapabilityNegotiationSupport, account ## IRC callbacks. - @async.coroutine - def on_capability_account_notify_available(self, value): + async def on_capability_account_notify_available(self, value): """ Take note of user account changes. """ return True - @async.coroutine - def on_capability_away_notify_available(self, value): + async def on_capability_away_notify_available(self, value): """ Take note of AWAY messages. """ return True - @async.coroutine - def on_capability_extended_join_available(self, value): + async def on_capability_extended_join_available(self, value): """ Take note of user account and realname on JOIN. """ return True - @async.coroutine - def on_capability_multi_prefix_available(self, value): + async def on_capability_multi_prefix_available(self, value): """ Thanks to how underlying client code works we already support multiple prefixes. """ return True - @async.coroutine - def on_capability_tls_available(self, value): + async def on_capability_tls_available(self, value): """ We never need to request this explicitly. """ return False ## Message handlers. - @async.coroutine - def on_raw_account(self, message): + async def on_raw_account(self, message): """ Changes in the associated account for a nickname. """ if not self._capabilities.get('account-notify', False): return @@ -73,8 +66,7 @@ class IRCv3_1Support(sasl.SASLSupport, cap.CapabilityNegotiationSupport, account else: self._sync_user(nick, { 'account': account, 'identified': True }) - @async.coroutine - def on_raw_away(self, message): + async def on_raw_away(self, message): """ Process AWAY messages. """ if 'away-notify' not in self._capabilities or not self._capabilities['away-notify']: return @@ -87,8 +79,7 @@ class IRCv3_1Support(sasl.SASLSupport, cap.CapabilityNegotiationSupport, account self.users[nick]['away'] = len(message.params) > 0 self.users[nick]['away_message'] = message.params[0] if len(message.params) > 0 else None - @async.coroutine - def on_raw_join(self, message): + async def on_raw_join(self, message): """ Process extended JOIN messages. """ if 'extended-join' in self._capabilities and self._capabilities['extended-join']: nick, metadata = self._parse_user(message.source) @@ -98,11 +89,11 @@ class IRCv3_1Support(sasl.SASLSupport, cap.CapabilityNegotiationSupport, account # Emit a fake join message. fakemsg = self._create_message('JOIN', channels, source=message.source) - yield from super().on_raw_join(fakemsg) + await super().on_raw_join(fakemsg) if account == NO_ACCOUNT: account = None self.users[nick]['account'] = account self.users[nick]['realname'] = realname else: - yield from super().on_raw_join(message) + await super().on_raw_join(message) diff --git a/pydle/features/ircv3/ircv3_2.py b/pydle/features/ircv3/ircv3_2.py index 5865c7c..29f5831 100644 --- a/pydle/features/ircv3/ircv3_2.py +++ b/pydle/features/ircv3/ircv3_2.py @@ -1,6 +1,5 @@ ## ircv3_2.py # IRCv3.2 support (in progress). -from pydle import async from . import ircv3_1 from . import tags from . import monitor @@ -14,75 +13,64 @@ class IRCv3_2Support(metadata.MetadataSupport, monitor.MonitoringSupport, tags.T ## IRC callbacks. - @async.coroutine - def on_capability_account_tag_available(self, value): + async def on_capability_account_tag_available(self, value): """ Add an account message tag to user messages. """ return True - @async.coroutine - def on_capability_cap_notify_available(self, value): + async def on_capability_cap_notify_available(self, value): """ Take note of new or removed capabilities. """ return True - @async.coroutine - def on_capability_chghost_available(self, value): + async def on_capability_chghost_available(self, value): """ Server reply to indicate a user we are in a common channel with changed user and/or host. """ return True - @async.coroutine - def on_capability_echo_message_available(self, value): + async def on_capability_echo_message_available(self, value): """ Echo PRIVMSG and NOTICEs back to client. """ return True - @async.coroutine - def on_capability_invite_notify_available(self, value): + async def on_capability_invite_notify_available(self, value): """ Broadcast invite messages to certain other clients. """ return True - @async.coroutine - def on_capability_userhost_in_names_available(self, value): + async def on_capability_userhost_in_names_available(self, value): """ Show full user!nick@host in NAMES list. We already parse it like that. """ return True - @async.coroutine - def on_capability_uhnames_available(self, value): + async def on_capability_uhnames_available(self, value): """ Possibly outdated alias for userhost-in-names. """ - return (yield from self.on_capability_userhost_in_names_available(value)) + return (await self.on_capability_userhost_in_names_available(value)) - @async.coroutine - def on_isupport_uhnames(self, value): + async def on_isupport_uhnames(self, value): """ Let the server know that we support UHNAMES using the old ISUPPORT method, for legacy support. """ - yield from self.rawmsg('PROTOCTL', 'UHNAMES') + await self.rawmsg('PROTOCTL', 'UHNAMES') ## API overrides. - @async.coroutine - def message(self, target, message): - yield from super().message(target, message) + async def message(self, target, message): + await super().message(target, message) if not self._capabilities.get('echo-message'): - yield from self.on_message(target, self.nickname, message) + await self.on_message(target, self.nickname, message) if self.is_channel(target): - yield from self.on_channel_message(target, self.nickname, message) + await self.on_channel_message(target, self.nickname, message) else: - yield from self.on_private_message(target, self.nickname, message) + await self.on_private_message(target, self.nickname, message) - @async.coroutine - def notice(self, target, message): - yield from super().notice(target, message) + async def notice(self, target, message): + await super().notice(target, message) if not self._capabilities.get('echo-message'): - yield from self.on_notice(target, self.nickname, message) + await self.on_notice(target, self.nickname, message) if self.is_channel(target): - yield from self.on_channel_notice(target, self.nickname, message) + await self.on_channel_notice(target, self.nickname, message) else: - yield from self.on_private_notice(target, self.nickname, message) + await self.on_private_notice(target, self.nickname, message) ## Message handlers. - @async.coroutine - def on_raw(self, message): + async def on_raw(self, message): if 'account' in message.tags: nick, _ = self._parse_user(message.source) if nick in self.users: @@ -91,10 +79,9 @@ class IRCv3_2Support(metadata.MetadataSupport, monitor.MonitoringSupport, tags.T 'account': message.tags['account'] } self._sync_user(nick, metadata) - yield from super().on_raw(message) + await super().on_raw(message) - @async.coroutine - def on_raw_chghost(self, message): + async def on_raw_chghost(self, message): """ Change user and/or host of user. """ if 'chghost' not in self._capabilities or not self._capabilities['chghost']: return diff --git a/pydle/features/ircv3/ircv3_3.py b/pydle/features/ircv3/ircv3_3.py index 8efcdbd..62b9201 100644 --- a/pydle/features/ircv3/ircv3_3.py +++ b/pydle/features/ircv3/ircv3_3.py @@ -1,6 +1,5 @@ ## ircv3_3.py # IRCv3.3 support (in progress). -from pydle import async from . import ircv3_2 __all__ = [ 'IRCv3_3Support' ] @@ -11,7 +10,6 @@ class IRCv3_3Support(ircv3_2.IRCv3_2Support): ## IRC callbacks. - @async.coroutine - def on_capability_message_tags_available(self, value): + async def on_capability_message_tags_available(self, value): """ Indicate that we can in fact parse arbitrary tags. """ return True diff --git a/pydle/features/ircv3/metadata.py b/pydle/features/ircv3/metadata.py index 1c9b079..ac99b4e 100644 --- a/pydle/features/ircv3/metadata.py +++ b/pydle/features/ircv3/metadata.py @@ -1,4 +1,3 @@ -from pydle import async from . import cap VISIBLITY_ALL = '*' @@ -17,16 +16,15 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): ## IRC API. - @async.coroutine - def get_metadata(self, target): + async def get_metadata(self, target): """ Return user metadata information. This is a blocking asynchronous method: it has to be called from a coroutine, as follows: - metadata = yield from self.get_metadata('#foo') + metadata = await self.get_metadata('#foo') """ if target not in self._pending['metadata']: - yield from self.rawmsg('METADATA', target, 'LIST') + await self.rawmsg('METADATA', target, 'LIST') self._metadata_queue.append(target) self._metadata_info[target] = {} @@ -34,34 +32,28 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): return self._pending['metadata'][target] - @async.coroutine - def set_metadata(self, target, key, value): - yield from self.rawmsg('METADATA', target, 'SET', key, value) + async def set_metadata(self, target, key, value): + await self.rawmsg('METADATA', target, 'SET', key, value) - @async.coroutine - def unset_metadata(self, target, key): - yield from self.rawmsg('METADATA', target, 'SET', key) + async def unset_metadata(self, target, key): + await self.rawmsg('METADATA', target, 'SET', key) - @async.coroutine - def clear_metadata(self, target): - yield from self.rawmsg('METADATA', target, 'CLEAR') + async def clear_metadata(self, target): + await self.rawmsg('METADATA', target, 'CLEAR') ## Callbacks. - @async.coroutine - def on_metadata(self, target, key, value, visibility=None): + async def on_metadata(self, target, key, value, visibility=None): pass ## Message handlers. - @async.coroutine - def on_capability_metadata_notify_available(self, value): + async def on_capability_metadata_notify_available(self, value): return True - @async.coroutine - def on_raw_metadata(self, message): + async def on_raw_metadata(self, message): """ Metadata event. """ target, targetmeta = self._parse_user(message.params[0]) key, visibility, value = message.params[1:4] @@ -70,10 +62,9 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): if target in self.users: self._sync_user(target, targetmeta) - yield from self.on_metadata(target, key, value, visibility=visibility) + await self.on_metadata(target, key, value, visibility=visibility) - @async.coroutine - def on_raw_760(self, message): + async def on_raw_760(self, message): """ Metadata key/value for whois. """ target, targetmeta = self._parse_user(message.params[0]) key, _, value = message.params[1:4] @@ -86,8 +77,7 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): self._whois_info[target].setdefault('metadata', {}) self._whois_info[target]['metadata'][key] = value - @async.coroutine - def on_raw_761(self, message): + async def on_raw_761(self, message): """ Metadata key/value. """ target, targetmeta = self._parse_user(message.params[0]) key, visibility = message.params[1:3] @@ -100,8 +90,7 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): self._metadata_info[target][key] = value - @async.coroutine - def on_raw_762(self, message): + async def on_raw_762(self, message): """ End of metadata. """ # No way to figure out whose query this belongs to, so make a best guess # it was the first one. @@ -112,13 +101,11 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): future = self._pending['metadata'].pop(nickname) future.set_result(self._metadata_info.pop(nickname)) - @async.coroutine - def on_raw_764(self, message): + async def on_raw_764(self, message): """ Metadata limit reached. """ pass - @async.coroutine - def on_raw_765(self, message): + async def on_raw_765(self, message): """ Invalid metadata target. """ target, targetmeta = self._parse_user(message.params[0]) @@ -133,22 +120,18 @@ class MetadataSupport(cap.CapabilityNegotiationSupport): future = self._pending['metadata'].pop(target) future.set_result(None) - @async.coroutine - def on_raw_766(self, message): + async def on_raw_766(self, message): """ Unknown metadata key. """ pass - @async.coroutine - def on_raw_767(self, message): + async def on_raw_767(self, message): """ Invalid metadata key. """ pass - @async.coroutine - def on_raw_768(self, message): + async def on_raw_768(self, message): """ Metadata key not set. """ pass - @async.coroutine - def on_raw_769(self, message): + async def on_raw_769(self, message): """ Metadata permission denied. """ pass diff --git a/pydle/features/ircv3/monitor.py b/pydle/features/ircv3/monitor.py index 1191066..72de3cc 100644 --- a/pydle/features/ircv3/monitor.py +++ b/pydle/features/ircv3/monitor.py @@ -1,6 +1,5 @@ ## monitor.py # Online status monitoring support. -from pydle import async from . import cap @@ -62,46 +61,39 @@ class MonitoringSupport(cap.CapabilityNegotiationSupport): ## Callbacks. - @async.coroutine - def on_user_online(self, nickname): + async def on_user_online(self, nickname): """ Callback called when a monitored user appears online. """ pass - @async.coroutine - def on_user_offline(self, nickname): + async def on_user_offline(self, nickname): """ Callback called when a monitored users goes offline. """ pass ## Message handlers. - @async.coroutine - def on_capability_monitor_notify_available(self, value): + async def on_capability_monitor_notify_available(self, value): return True - @async.coroutine - def on_raw_730(self, message): + async def on_raw_730(self, message): """ Someone we are monitoring just came online. """ for nick in message.params[1].split(','): self._create_user(nick) - yield from self.on_user_online(nickname) + await self.on_user_online(nickname) - @async.coroutine - def on_raw_731(self, message): + async def on_raw_731(self, message): """ Someone we are monitoring got offline. """ for nick in message.params[1].split(','): self._destroy_user(nick, monitor_override=True) - yield from self.on_user_offline(nickname) + await self.on_user_offline(nickname) - @async.coroutine - def on_raw_732(self, message): + async def on_raw_732(self, message): """ List of users we're monitoring. """ self._monitoring.update(message.params[1].split(',')) on_raw_733 = cap.CapabilityNegotiationSupport._ignored # End of MONITOR list. - @async.coroutine - def on_raw_734(self, message): + async def on_raw_734(self, message): """ Monitor list is full, can't add target. """ # Remove from monitoring list, not much else we can do. self._monitoring.difference_update(message.params[1].split(',')) diff --git a/pydle/features/ircv3/sasl.py b/pydle/features/ircv3/sasl.py index 2ab3359..6d830f6 100644 --- a/pydle/features/ircv3/sasl.py +++ b/pydle/features/ircv3/sasl.py @@ -1,13 +1,14 @@ ## sasl.py # SASL authentication support. Currently we only support PLAIN authentication. import base64 +from functools import partial + try: import puresasl import puresasl.client except ImportError: puresasl = None -from pydle import async from . import cap __all__ = [ 'SASLSupport' ] @@ -41,15 +42,15 @@ class SASLSupport(cap.CapabilityNegotiationSupport): ## SASL functionality. - @async.coroutine - def _sasl_start(self, mechanism): + async def _sasl_start(self, mechanism): """ Initiate SASL authentication. """ # The rest will be handled in on_raw_authenticate()/_sasl_respond(). - yield from self.rawmsg('AUTHENTICATE', mechanism) - self._sasl_timer = self.eventloop.schedule_async_in(self.SASL_TIMEOUT, self._sasl_abort(timeout=True)) + await self.rawmsg('AUTHENTICATE', mechanism) + # create a partial, required for our callback to get the kwarg + _sasl_partial = partial(self._sasl_abort, timeout=True) + self._sasl_timer = self.eventloop.call_later(self.SASL_TIMEOUT, _sasl_partial) - @async.coroutine - def _sasl_abort(self, timeout=False): + async def _sasl_abort(self, timeout=False): """ Abort SASL authentication. """ if timeout: self.logger.error('SASL authentication timed out: aborting.') @@ -57,23 +58,22 @@ class SASLSupport(cap.CapabilityNegotiationSupport): self.logger.error('SASL authentication aborted.') if self._sasl_timer: - self.eventloop.unschedule(self._sasl_timer) + self._sasl_timer.cancel() + self._sasl_timer = None # We're done here. - yield from self.rawmsg('AUTHENTICATE', ABORT_MESSAGE) - yield from self._capability_negotiated('sasl') + await self.rawmsg('AUTHENTICATE', ABORT_MESSAGE) + await self._capability_negotiated('sasl') - @async.coroutine - def _sasl_end(self): + async def _sasl_end(self): """ Finalize SASL authentication. """ if self._sasl_timer: - self.eventloop.unschedule(self._sasl_timer) + self._sasl_timer.cancel() self._sasl_timer = None - yield from self._capability_negotiated('sasl') + await self._capability_negotiated('sasl') - @async.coroutine - def _sasl_respond(self): + async def _sasl_respond(self): """ Respond to SASL challenge with response. """ # Formulate a response. if self._sasl_client: @@ -84,7 +84,7 @@ class SASLSupport(cap.CapabilityNegotiationSupport): if response is None: self.logger.warning('SASL challenge processing failed: aborting SASL authentication.') - yield from self._sasl_abort() + await self._sasl_abort() else: response = b'' @@ -94,19 +94,18 @@ class SASLSupport(cap.CapabilityNegotiationSupport): # Send response in chunks. while to_send > 0: - yield from self.rawmsg('AUTHENTICATE', response[:RESPONSE_LIMIT]) + await self.rawmsg('AUTHENTICATE', response[:RESPONSE_LIMIT]) response = response[RESPONSE_LIMIT:] to_send -= RESPONSE_LIMIT # If our message fit exactly in SASL_RESPOSE_LIMIT-byte chunks, send an empty message to indicate we're done. if to_send == 0: - yield from self.rawmsg('AUTHENTICATE', EMPTY_MESSAGE) + await self.rawmsg('AUTHENTICATE', EMPTY_MESSAGE) ## Capability callbacks. - @async.coroutine - def on_capability_sasl_available(self, value): + async def on_capability_sasl_available(self, value): """ Check whether or not SASL is available. """ if value: self._sasl_mechanisms = value.upper().split(',') @@ -119,8 +118,7 @@ class SASLSupport(cap.CapabilityNegotiationSupport): self.logger.warning('SASL credentials set but puresasl module not found: not initiating SASL authentication.') return False - @async.coroutine - def on_capability_sasl_enabled(self): + async def on_capability_sasl_enabled(self): """ Start SASL authentication. """ if self.sasl_mechanism: if self._sasl_mechanisms and self.sasl_mechanism not in self._sasl_mechanisms: @@ -147,19 +145,18 @@ class SASLSupport(cap.CapabilityNegotiationSupport): mechanism = self._sasl_client.mechanism.upper() # Initialize SASL. - yield from self._sasl_start(mechanism) + await self._sasl_start(mechanism) # Tell caller we need more time, and to not end capability negotiation just yet. return cap.NEGOTIATING ## Message handlers. - @async.coroutine - def on_raw_authenticate(self, message): + async def on_raw_authenticate(self, message): """ Received part of the authentication challenge. """ # Cancel timeout timer. if self._sasl_timer: - self.eventloop.unschedule(self._sasl_timer) + self._sasl_timer.cancel() self._sasl_timer = None # Add response data. @@ -169,28 +166,25 @@ class SASLSupport(cap.CapabilityNegotiationSupport): # If the response ain't exactly SASL_RESPONSE_LIMIT bytes long, it's the end. Process. if len(response) % RESPONSE_LIMIT > 0: - yield from self._sasl_respond() + await self._sasl_respond() else: # Response not done yet. Restart timer. - self._sasl_timer = self.eventloop.schedule_async_in(self.SASL_TIMEOUT, self._sasl_abort(timeout=True)) + self._sasl_timer = self.eventloop.call_later(self.SASL_TIMEOUT, self._sasl_abort(timeout=True)) on_raw_900 = cap.CapabilityNegotiationSupport._ignored # You are now logged in as... - @async.coroutine - def on_raw_903(self, message): + async def on_raw_903(self, message): """ SASL authentication successful. """ - yield from self._sasl_end() + await self._sasl_end() - @async.coroutine - def on_raw_904(self, message): + async def on_raw_904(self, message): """ Invalid mechanism or authentication failed. Abort SASL. """ - yield from self._sasl_abort() + await self._sasl_abort() - @async.coroutine - def on_raw_905(self, message): + async def on_raw_905(self, message): """ Authentication failed. Abort SASL. """ - yield from self._sasl_abort() + await self._sasl_abort() on_raw_906 = cap.CapabilityNegotiationSupport._ignored # Completed registration while authenticating/registration aborted. on_raw_907 = cap.CapabilityNegotiationSupport._ignored # Already authenticated over SASL. diff --git a/pydle/features/isupport.py b/pydle/features/isupport.py index a69fbb4..0611cf3 100644 --- a/pydle/features/isupport.py +++ b/pydle/features/isupport.py @@ -2,7 +2,6 @@ # ISUPPORT (server-side IRC extension indication) support. # See: http://tools.ietf.org/html/draft-hardy-irc-isupport-00 import collections -from pydle import async import pydle.protocol from pydle.features import rfc1459 @@ -36,8 +35,7 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): ## Command handlers. - @async.coroutine - def on_raw_005(self, message): + async def on_raw_005(self, message): """ ISUPPORT indication. """ isupport = {} @@ -64,31 +62,27 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): method = 'on_isupport_' + pydle.protocol.identifierify(entry) if hasattr(self, method): - yield from getattr(self, method)(value) + await getattr(self, method)(value) ## ISUPPORT handlers. - @async.coroutine - def on_isupport_awaylen(self, value): + async def on_isupport_awaylen(self, value): """ Away message length limit. """ self._away_message_length_limit = int(value) - @async.coroutine - def on_isupport_casemapping(self, value): + async def on_isupport_casemapping(self, value): """ IRC case mapping for nickname and channel name comparisons. """ if value in rfc1459.protocol.CASE_MAPPINGS: self._case_mapping = value self.channels = rfc1459.parsing.NormalizingDict(self.channels, case_mapping=value) self.users = rfc1459.parsing.NormalizingDict(self.users, case_mapping=value) - @async.coroutine - def on_isupport_channellen(self, value): + async def on_isupport_channellen(self, value): """ Channel name length limit. """ self._channel_length_limit = int(value) - @async.coroutine - def on_isupport_chanlimit(self, value): + async def on_isupport_chanlimit(self, value): """ Simultaneous channel limits for user. """ self._channel_limits = {} @@ -100,8 +94,7 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): for prefix in types: self._channel_limit_groups[prefix] = frozenset(types) - @async.coroutine - def on_isupport_chanmodes(self, value): + async def on_isupport_chanmodes(self, value): """ Valid channel modes and their behaviour. """ list, param, param_set, noparams = [ set(modes) for modes in value.split(',')[:4] ] self._channel_modes.update(set(value.replace(',', ''))) @@ -123,45 +116,39 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_NO_PARAMETER] = set() self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_NO_PARAMETER].update(noparams) - @async.coroutine - def on_isupport_chantypes(self, value): + async def on_isupport_chantypes(self, value): """ Channel name prefix symbols. """ if not value: value = '' self._channel_prefixes = set(value) - @async.coroutine - def on_isupport_excepts(self, value): + async def on_isupport_excepts(self, value): """ Server allows ban exceptions. """ if not value: value = BAN_EXCEPT_MODE self._channel_modes.add(value) self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_LIST].add(value) - @async.coroutine - def on_isupport_extban(self, value): + async def on_isupport_extban(self, value): """ Extended ban prefixes. """ self._extban_prefix, types = value.split(',') self._extban_types = set(types) - @async.coroutine - def on_isupport_invex(self, value): + async def on_isupport_invex(self, value): """ Server allows invite exceptions. """ if not value: value = INVITE_EXCEPT_MODE self._channel_modes.add(value) self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_LIST].add(value) - @async.coroutine - def on_isupport_maxbans(self, value): + async def on_isupport_maxbans(self, value): """ Maximum entries in ban list. Replaced by MAXLIST. """ if 'MAXLIST' not in self._isupport: if not self._list_limits: self._list_limits = {} self._list_limits['b'] = int(value) - @async.coroutine - def on_isupport_maxchannels(self, value): + async def on_isupport_maxchannels(self, value): """ Old version of CHANLIMIT. """ if 'CHANTYPES' in self._isupport and 'CHANLIMIT' not in self._isupport: self._channel_limits = {} @@ -172,8 +159,7 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): for prefix in prefixes: self._channel_limit_groups[prefix] = frozenset(prefixes) - @async.coroutine - def on_isupport_maxlist(self, value): + async def on_isupport_maxlist(self, value): """ Limits on channel modes involving lists. """ self._list_limits = {} @@ -185,33 +171,27 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): for mode in modes: self._list_limit_groups[mode] = frozenset(modes) - @async.coroutine - def on_isupport_maxpara(self, value): + async def on_isupport_maxpara(self, value): """ Limits to parameters given to command. """ self._command_parameter_limit = int(value) - @async.coroutine - def on_isupport_modes(self, value): + async def on_isupport_modes(self, value): """ Maximum number of variable modes to change in a single MODE command. """ self._mode_limit = int(value) - @async.coroutine - def on_isupport_namesx(self, value): + async def on_isupport_namesx(self, value): """ Let the server know we do in fact support NAMESX. Effectively the same as CAP multi-prefix. """ - yield from self.rawmsg('PROTOCTL', 'NAMESX') + await self.rawmsg('PROTOCTL', 'NAMESX') - @async.coroutine - def on_isupport_network(self, value): + async def on_isupport_network(self, value): """ IRC network name. """ self.network = value - @async.coroutine - def on_isupport_nicklen(self, value): + async def on_isupport_nicklen(self, value): """ Nickname length limit. """ self._nickname_length_limit = int(value) - @async.coroutine - def on_isupport_prefix(self, value): + async def on_isupport_prefix(self, value): """ Nickname prefixes on channels and their associated modes. """ if not value: # No prefixes support. @@ -230,13 +210,11 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): for mode, prefix in zip(modes, prefixes): self._nickname_prefixes[prefix] = mode - @async.coroutine - def on_isupport_statusmsg(self, value): + async def on_isupport_statusmsg(self, value): """ Support for messaging every member on a channel with given status or higher. """ self._status_message_prefixes.update(value) - @async.coroutine - def on_isupport_targmax(self, value): + async def on_isupport_targmax(self, value): """ The maximum number of targets certain types of commands can affect. """ if not value: return @@ -247,13 +225,11 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): continue self._target_limits[command] = int(limit) - @async.coroutine - def on_isupport_topiclen(self, value): + async def on_isupport_topiclen(self, value): """ Channel topic length limit. """ self._topic_length_limit = int(value) - @async.coroutine - def on_isupport_wallchops(self, value): + async def on_isupport_wallchops(self, value): """ Support for messaging every opped member or higher on a channel. Replaced by STATUSMSG. """ for prefix, mode in self._nickname_prefixes.items(): if mode == 'o': @@ -262,8 +238,7 @@ class ISUPPORTSupport(rfc1459.RFC1459Support): prefix = '@' self._status_message_prefixes.add(prefix) - @async.coroutine - def on_isupport_wallvoices(self, value): + async def on_isupport_wallvoices(self, value): """ Support for messaging every voiced member or higher on a channel. Replaced by STATUSMSG. """ for prefix, mode in self._nickname_prefixes.items(): if mode == 'v': diff --git a/pydle/features/rfc1459/client.py b/pydle/features/rfc1459/client.py index 2d978f7..02ecac0 100644 --- a/pydle/features/rfc1459/client.py +++ b/pydle/features/rfc1459/client.py @@ -1,11 +1,10 @@ ## rfc1459.py # Basic RFC1459 stuff. -import datetime -import itertools import copy +import datetime import ipaddress +import itertools -from pydle import async from pydle.client import BasicClient, NotInChannel, AlreadyInChannel from . import parsing from . import protocol @@ -98,7 +97,7 @@ class RFC1459Support(BasicClient): def _destroy_user(self, user, channel=None): if channel: - channels = [ self.channels[channel] ] + channels = [self.channels[channel]] else: channels = self.channels.values() @@ -185,19 +184,18 @@ class RFC1459Support(BasicClient): ## Connection. - @async.coroutine - def connect(self, hostname=None, port=None, password=None, **kwargs): + async def connect(self, hostname=None, port=None, password=None, **kwargs): port = port or protocol.DEFAULT_PORT # Connect... - yield from super().connect(hostname, port, **kwargs) + await super().connect(hostname, port, **kwargs) # Set password. self.password = password # And initiate the IRC connection. - yield from self._register() + await self._register() - @async.coroutine - def _register(self): + + async def _register(self): """ Perform IRC connection registration. """ if self.registered: return @@ -209,15 +207,14 @@ class RFC1459Support(BasicClient): # Password first. if self.password: - yield from self.rawmsg('PASS', self.password) + await self.rawmsg('PASS', self.password) # Then nickname... - yield from self.set_nickname(self._attempt_nicknames.pop(0)) + await self.set_nickname(self._attempt_nicknames.pop(0)) # And now for the rest of the user information. - yield from self.rawmsg('USER', self.username, '0', '*', self.realname) + await self.rawmsg('USER', self.username, '0', '*', self.realname) - @async.coroutine - def _registration_completed(self, message): + async def _registration_completed(self, message): """ We're connected and registered. Receive proper nickname and emit fake NICK message. """ if not self.registered: # Re-enable throttling. @@ -226,8 +223,7 @@ class RFC1459Support(BasicClient): target = message.params[0] fakemsg = self._create_message('NICK', target, source=self.nickname) - yield from self.on_raw_nick(fakemsg) - + await self.on_raw_nick(fakemsg) ## Message handling. @@ -245,53 +241,48 @@ class RFC1459Support(BasicClient): self._receive_buffer = data return parsing.RFC1459Message.parse(message + sep, encoding=self.encoding) - ## IRC API. - @async.coroutine - def set_nickname(self, nickname): + async def set_nickname(self, nickname): """ Set nickname to given nickname. Users should only rely on the nickname actually being changed when receiving an on_nick_change callback. """ - yield from self.rawmsg('NICK', nickname) + await self.rawmsg('NICK', nickname) - @async.coroutine - def join(self, channel, password=None): + async def join(self, channel, password=None): """ Join channel, optionally with password. """ if self.in_channel(channel): raise AlreadyInChannel(channel) if password: - yield from self.rawmsg('JOIN', channel, password) + await self.rawmsg('JOIN', channel, password) else: - yield from self.rawmsg('JOIN', channel) + await self.rawmsg('JOIN', channel) - @async.coroutine - def part(self, channel, message=None): + async def part(self, channel, message=None): """ Leave channel, optionally with message. """ if not self.in_channel(channel): raise NotInChannel(channel) # Message seems to be an extension to the spec. if message: - yield from self.rawmsg('PART', channel, message) + await self.rawmsg('PART', channel, message) else: - yield from self.rawmsg('PART', channel) + await self.rawmsg('PART', channel) - @async.coroutine - def kick(self, channel, target, reason=None): + + async def kick(self, channel, target, reason=None): """ Kick user from channel. """ if not self.in_channel(channel): raise NotInChannel(channel) if reason: - yield from self.rawmsg('KICK', channel, target, reason) + await self.rawmsg('KICK', channel, target, reason) else: - yield from self.rawmsg('KICK', channel, target) + await self.rawmsg('KICK', channel, target) - @async.coroutine - def ban(self, channel, target, range=0): + async def ban(self, channel, target, range=0): """ Ban user from channel. Target can be either a user or a host. This command will not kick: use kickban() for that. @@ -305,10 +296,9 @@ class RFC1459Support(BasicClient): host = self._format_host_range(host, range) mask = self._format_host_mask('*', '*', host) - yield from self.rawmsg('MODE', channel, '+b', mask) + await self.rawmsg('MODE', channel, '+b', mask) - @async.coroutine - def unban(self, channel, target, range=0): + async def unban(self, channel, target, range=0): """ Unban user from channel. Target can be either a user or a host. See ban documentation for the range parameter. @@ -320,60 +310,56 @@ class RFC1459Support(BasicClient): host = self._format_host_range(host, range) mask = self._format_host_mask('*', '*', host) - yield from self.rawmsg('MODE', channel, '-b', mask) + await self.rawmsg('MODE', channel, '-b', mask) - @async.coroutine - def kickban(self, channel, target, reason=None, range=0): + async def kickban(self, channel, target, reason=None, range=0): """ Kick and ban user from channel. """ - yield from self.ban(channel, target, range) - yield from self.kick(channel, target, reason) + await self.ban(channel, target, range) + await self.kick(channel, target, reason) - @async.coroutine - def quit(self, message=None): + async def quit(self, message=None): """ Quit network. """ if message is None: message = self.DEFAULT_QUIT_MESSAGE - yield from self.rawmsg('QUIT', message) + await self.rawmsg('QUIT', message) self.disconnect(expected=True) - @async.coroutine - def cycle(self, channel): + async def cycle(self, channel): """ Rejoin channel. """ if not self.in_channel(channel): raise NotInChannel(channel) password = self.channels[channel]['password'] - yield from self.part(channel) - yield from self.join(channel, password) + await self.part(channel) + await self.join(channel, password) - @async.coroutine - def message(self, target, message): + async def message(self, target, message): """ Message channel or user. """ hostmask = self._format_user_mask(self.nickname) # Leeway. - chunklen = protocol.MESSAGE_LENGTH_LIMIT - len('{hostmask} PRIVMSG {target} :'.format(hostmask=hostmask, target=target)) - 25 + chunklen = protocol.MESSAGE_LENGTH_LIMIT - len( + '{hostmask} PRIVMSG {target} :'.format(hostmask=hostmask, target=target)) - 25 for line in message.replace('\r', '').split('\n'): for chunk in chunkify(line, chunklen): # Some IRC servers respond with "412 Bot :No text to send" on empty messages. - yield from self.rawmsg('PRIVMSG', target, chunk or ' ') + await self.rawmsg('PRIVMSG', target, chunk or ' ') - @async.coroutine - def notice(self, target, message): + async def notice(self, target, message): """ Notice channel or user. """ hostmask = self._format_user_mask(self.nickname) # Leeway. - chunklen = protocol.MESSAGE_LENGTH_LIMIT - len('{hostmask} NOTICE {target} :'.format(hostmask=hostmask, target=target)) - 25 + chunklen = protocol.MESSAGE_LENGTH_LIMIT - len( + '{hostmask} NOTICE {target} :'.format(hostmask=hostmask, target=target)) - 25 for line in message.replace('\r', '').split('\n'): for chunk in chunkify(line, chunklen): - yield from self.rawmsg('NOTICE', target, chunk) + await self.rawmsg('NOTICE', target, chunk) - @async.coroutine - def set_mode(self, target, *modes): + async def set_mode(self, target, *modes): """ Set mode on target. Users should only rely on the mode actually being changed when receiving an on_{channel,user}_mode_change callback. @@ -381,10 +367,9 @@ class RFC1459Support(BasicClient): if self.is_channel(target) and not self.in_channel(target): raise NotInChannel(target) - yield from self.rawmsg('MODE', target, *modes) + await self.rawmsg('MODE', target, *modes) - @async.coroutine - def set_topic(self, channel, topic): + async def set_topic(self, channel, topic): """ Set topic on channel. Users should only rely on the topic actually being changed when receiving an on_topic_change callback. @@ -394,25 +379,22 @@ class RFC1459Support(BasicClient): elif not self.in_channel(channel): raise NotInChannel(channel) - yield from self.rawmsg('TOPIC', channel, topic) + await self.rawmsg('TOPIC', channel, topic) - @async.coroutine - def away(self, message): + async def away(self, message): """ Mark self as away. """ - yield from self.rawmsg('AWAY', message) + await self.rawmsg('AWAY', message) - @async.coroutine - def back(self): + async def back(self): """ Mark self as not away. """ - yield from self.rawmsg('AWAY') + await self.rawmsg('AWAY') - @async.coroutine - def whois(self, nickname): + async def whois(self, nickname): """ Return information about user. This is an blocking asynchronous method: it has to be called from a coroutine, as follows: - info = yield from self.whois('Nick') + info = await self.whois('Nick') """ # Some IRCDs are wonky and send strange responses for spaces in nicknames. # We just check if there's a space in the nickname -- if there is, @@ -423,7 +405,7 @@ class RFC1459Support(BasicClient): return result if nickname not in self._pending['whois']: - yield from self.rawmsg('WHOIS', nickname) + await self.rawmsg('WHOIS', nickname) self._whois_info[nickname] = { 'oper': False, 'idle': 0, @@ -434,15 +416,14 @@ class RFC1459Support(BasicClient): # Create a future for when the WHOIS requests succeeds. self._pending['whois'][nickname] = self.eventloop.create_future() - return (yield from self._pending['whois'][nickname]) + return await self._pending['whois'][nickname] - @async.coroutine - def whowas(self, nickname): + async def whowas(self, nickname): """ Return information about offline user. This is an blocking asynchronous method: it has to be called from a coroutine, as follows: - info = yield from self.whowas('Nick') + info = await self.whowas('Nick') """ # Same treatment as nicknames in whois. if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None: @@ -451,14 +432,13 @@ class RFC1459Support(BasicClient): return result if nickname not in self._pending['whowas']: - yield from self.rawmsg('WHOWAS', nickname) + await self.rawmsg('WHOWAS', nickname) self._whowas_info[nickname] = {} # Create a future for when the WHOWAS requests succeeds. self._pending['whowas'][nickname] = self.eventloop.create_future() - return (yield from self._pending['whowas'][nickname]) - + return (await self._pending['whowas'][nickname]) ## IRC helpers. @@ -476,111 +456,89 @@ class RFC1459Support(BasicClient): """ Check if given nicknames are equal in the server's case mapping. """ return self.normalize(left) == self.normalize(right) - ## Overloadable callbacks. - @async.coroutine - def on_connect(self): + async def on_connect(self): # Auto-join channels. for channel in self._autojoin_channels: - yield from self.join(channel) + await self.join(channel) - @async.coroutine - def on_invite(self, channel, by): + async def on_invite(self, channel, by): """ Callback called when the client was invited into a channel by someone. """ pass - @async.coroutine - def on_user_invite(self, target, channel, by): + async def on_user_invite(self, target, channel, by): """ Callback called when another user was invited into a channel by someone. """ pass - @async.coroutine - def on_join(self, channel, user): + async def on_join(self, channel, user): """ Callback called when a user, possibly the client, has joined the channel. """ pass - @async.coroutine - def on_kill(self, target, by, reason): + async def on_kill(self, target, by, reason): """ Callback called when a user, possibly the client, was killed from the server. """ pass - @async.coroutine - def on_kick(self, channel, target, by, reason=None): + async def on_kick(self, channel, target, by, reason=None): """ Callback called when a user, possibly the client, was kicked from a channel. """ pass - @async.coroutine - def on_mode_change(self, channel, modes, by): + async def on_mode_change(self, channel, modes, by): """ Callback called when the mode on a channel was changed. """ pass - @async.coroutine - def on_user_mode_change(self, modes): + async def on_user_mode_change(self, modes): """ Callback called when a user mode change occurred for the client. """ pass - @async.coroutine - def on_message(self, target, by, message): + async def on_message(self, target, by, message): """ Callback called when the client received a message. """ pass - @async.coroutine - def on_channel_message(self, target, by, message): + async def on_channel_message(self, target, by, message): """ Callback received when the client received a message in a channel. """ pass - @async.coroutine - def on_private_message(self, target, by, message): + async def on_private_message(self, target, by, message): """ Callback called when the client received a message in private. """ pass - @async.coroutine - def on_nick_change(self, old, new): + async def on_nick_change(self, old, new): """ Callback called when a user, possibly the client, changed their nickname. """ pass - @async.coroutine - def on_notice(self, target, by, message): + async def on_notice(self, target, by, message): """ Callback called when the client received a notice. """ pass - @async.coroutine - def on_channel_notice(self, target, by, message): + async def on_channel_notice(self, target, by, message): """ Callback called when the client received a notice in a channel. """ pass - @async.coroutine - def on_private_notice(self, target, by, message): + async def on_private_notice(self, target, by, message): """ Callback called when the client received a notice in private. """ pass - @async.coroutine - def on_part(self, channel, user, message=None): + async def on_part(self, channel, user, message=None): """ Callback called when a user, possibly the client, left a channel. """ pass - @async.coroutine - def on_topic_change(self, channel, message, by): + async def on_topic_change(self, channel, message, by): """ Callback called when the topic for a channel was changed. """ pass - @async.coroutine - def on_quit(self, user, message=None): + async def on_quit(self, user, message=None): """ Callback called when a user, possibly the client, left the network. """ pass - ## Callback handlers. - @async.coroutine - def on_raw_error(self, message): + async def on_raw_error(self, message): """ Server encountered an error and will now close the connection. """ error = protocol.ServerError(' '.join(message.params)) - yield from self.on_data_error(error) + await self.on_data_error(error) - @async.coroutine - def on_raw_invite(self, message): + async def on_raw_invite(self, message): """ INVITE command. """ nick, metadata = self._parse_user(message.source) self._sync_user(nick, metadata) @@ -589,12 +547,11 @@ class RFC1459Support(BasicClient): target, metadata = self._parse_user(target) if self.is_same_nick(self.nickname, target): - yield from self.on_invite(channel, nick) + await self.on_invite(channel, nick) else: - yield from self.on_user_invite(target, channel, nick) + await self.on_user_invite(target, channel, nick) - @async.coroutine - def on_raw_join(self, message): + async def on_raw_join(self, message): """ JOIN command. """ nick, metadata = self._parse_user(message.source) self._sync_user(nick, metadata) @@ -607,7 +564,7 @@ class RFC1459Support(BasicClient): self._create_channel(channel) # Request channel mode from IRCd. - yield from self.rawmsg('MODE', channel) + await self.rawmsg('MODE', channel) else: # Add user to channel user list. for channel in channels: @@ -615,10 +572,9 @@ class RFC1459Support(BasicClient): self.channels[channel]['users'].add(nick) for channel in channels: - yield from self.on_join(channel, nick) + await self.on_join(channel, nick) - @async.coroutine - def on_raw_kick(self, message): + async def on_raw_kick(self, message): """ KICK command. """ kicker, kickermeta = self._parse_user(message.source) self._sync_user(kicker, kickermeta) @@ -643,10 +599,9 @@ class RFC1459Support(BasicClient): if self.in_channel(channel): self._destroy_user(target, channel) - yield from self.on_kick(channel, target, kicker, reason) + await self.on_kick(channel, target, kicker, reason) - @async.coroutine - def on_raw_kill(self, message): + async def on_raw_kill(self, message): """ KILL command. """ by, bymeta = self._parse_user(message.source) target, targetmeta = self._parse_user(message.params[0]) @@ -656,14 +611,13 @@ class RFC1459Support(BasicClient): if by in self.users: self._sync_user(by, bymeta) - yield from self.on_kill(target, by, reason) + await self.on_kill(target, by, reason) if self.is_same_nick(self.nickname, target): self.disconnect(expected=False) else: self._destroy_user(target) - @async.coroutine - def on_raw_mode(self, message): + async def on_raw_mode(self, message): """ MODE command. """ nick, metadata = self._parse_user(message.source) target, modes = message.params[0], message.params[1:] @@ -674,7 +628,7 @@ class RFC1459Support(BasicClient): # Parse modes. self.channels[target]['modes'] = self._parse_channel_modes(target, modes) - yield from self.on_mode_change(target, modes, nick) + await self.on_mode_change(target, modes, nick) else: target, targetmeta = self._parse_user(target) self._sync_user(target, targetmeta) @@ -683,10 +637,9 @@ class RFC1459Support(BasicClient): if self.is_same_nick(self.nickname, nick): self._mode = self._parse_user_modes(nick, modes, current=self._mode) - yield from self.on_user_mode_change(modes) + await self.on_user_mode_change(modes) - @async.coroutine - def on_raw_nick(self, message): + async def on_raw_nick(self, message): """ NICK command. """ nick, metadata = self._parse_user(message.source) new = message.params[0] @@ -701,24 +654,22 @@ class RFC1459Support(BasicClient): self._rename_user(nick, new) # Call handler. - yield from self.on_nick_change(nick, new) + await self.on_nick_change(nick, new) - @async.coroutine - def on_raw_notice(self, message): + async def on_raw_notice(self, message): """ NOTICE command. """ nick, metadata = self._parse_user(message.source) target, message = message.params self._sync_user(nick, metadata) - yield from self.on_notice(target, nick, message) + await self.on_notice(target, nick, message) if self.is_channel(target): - yield from self.on_channel_notice(target, nick, message) + await self.on_channel_notice(target, nick, message) else: - yield from self.on_private_notice(target, nick, message) + await self.on_private_notice(target, nick, message) - @async.coroutine - def on_raw_part(self, message): + async def on_raw_part(self, message): """ PART command. """ nick, metadata = self._parse_user(message.source) channels = message.params[0].split(',') @@ -733,35 +684,32 @@ class RFC1459Support(BasicClient): for channel in channels: if self.in_channel(channel): self._destroy_channel(channel) - yield from self.on_part(channel, nick, reason) + await self.on_part(channel, nick, reason) else: # Someone else left. Remove them. for channel in channels: self._destroy_user(nick, channel) - yield from self.on_part(channel, nick, reason) + await self.on_part(channel, nick, reason) - @async.coroutine - def on_raw_ping(self, message): + async def on_raw_ping(self, message): """ PING command. """ # Respond with a pong. - yield from self.rawmsg('PONG', *message.params) + await self.rawmsg('PONG', *message.params) - @async.coroutine - def on_raw_privmsg(self, message): + async def on_raw_privmsg(self, message): """ PRIVMSG command. """ nick, metadata = self._parse_user(message.source) target, message = message.params self._sync_user(nick, metadata) - yield from self.on_message(target, nick, message) + await self.on_message(target, nick, message) if self.is_channel(target): - yield from self.on_channel_message(target, nick, message) + await self.on_channel_message(target, nick, message) else: - yield from self.on_private_message(target, nick, message) + await self.on_private_message(target, nick, message) - @async.coroutine - def on_raw_quit(self, message): + async def on_raw_quit(self, message): """ QUIT command. """ nick, metadata = self._parse_user(message.source) @@ -771,7 +719,7 @@ class RFC1459Support(BasicClient): else: reason = None - yield from self.on_quit(nick, reason) + await self.on_quit(nick, reason) # Remove user from database. if not self.is_same_nick(self.nickname, nick): self._destroy_user(nick) @@ -779,8 +727,7 @@ class RFC1459Support(BasicClient): elif self.connected: self.disconnect(expected=True) - @async.coroutine - def on_raw_topic(self, message): + async def on_raw_topic(self, message): """ TOPIC command. """ setter, settermeta = self._parse_user(message.source) target, topic = message.params @@ -793,20 +740,18 @@ class RFC1459Support(BasicClient): self.channels[target]['topic_by'] = setter self.channels[target]['topic_set'] = datetime.datetime.now() - yield from self.on_topic_change(target, topic, setter) - + await self.on_topic_change(target, topic, setter) ## Numeric responses. # Since RFC1459 specifies no specific banner message upon completion of registration, # take any of the below commands as an indication that registration succeeded. - on_raw_001 = _registration_completed # Welcome message. - on_raw_002 = _registration_completed # Server host. - on_raw_003 = _registration_completed # Server creation time. + on_raw_001 = _registration_completed # Welcome message. + on_raw_002 = _registration_completed # Server host. + on_raw_003 = _registration_completed # Server creation time. - @async.coroutine - def on_raw_004(self, message): + async def on_raw_004(self, message): """ Basic server information. """ target, hostname, ircd, user_modes, channel_modes = message.params[:5] @@ -814,19 +759,18 @@ class RFC1459Support(BasicClient): self._channel_modes = set(channel_modes) self._user_modes = set(user_modes) - on_raw_008 = _registration_completed # Server notice mask. - on_raw_042 = _registration_completed # Unique client ID. - on_raw_250 = _registration_completed # Connection statistics. - on_raw_251 = _registration_completed # Amount of users online. - on_raw_252 = _registration_completed # Amount of operators online. - on_raw_253 = _registration_completed # Amount of unknown connections. - on_raw_254 = _registration_completed # Amount of channels. - on_raw_255 = _registration_completed # Amount of local users and servers. - on_raw_265 = _registration_completed # Amount of local users. - on_raw_266 = _registration_completed # Amount of global users. + on_raw_008 = _registration_completed # Server notice mask. + on_raw_042 = _registration_completed # Unique client ID. + on_raw_250 = _registration_completed # Connection statistics. + on_raw_251 = _registration_completed # Amount of users online. + on_raw_252 = _registration_completed # Amount of operators online. + on_raw_253 = _registration_completed # Amount of unknown connections. + on_raw_254 = _registration_completed # Amount of channels. + on_raw_255 = _registration_completed # Amount of local users and servers. + on_raw_265 = _registration_completed # Amount of local users. + on_raw_266 = _registration_completed # Amount of global users. - @async.coroutine - def on_raw_301(self, message): + async def on_raw_301(self, message): """ User is away. """ target, nickname, message = message.params info = { @@ -839,8 +783,7 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whois']: self._whois_info[nickname].update(info) - @async.coroutine - def on_raw_311(self, message): + async def on_raw_311(self, message): """ WHOIS user info. """ target, nickname, username, hostname, _, realname = message.params info = { @@ -853,8 +796,7 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whois']: self._whois_info[nickname].update(info) - @async.coroutine - def on_raw_312(self, message): + async def on_raw_312(self, message): """ WHOIS server info. """ target, nickname, server, serverinfo = message.params info = { @@ -867,8 +809,7 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whowas']: self._whowas_info[nickname].update(info) - @async.coroutine - def on_raw_313(self, message): + async def on_raw_313(self, message): """ WHOIS operator info. """ target, nickname = message.params[:2] info = { @@ -878,8 +819,7 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whois']: self._whois_info[nickname].update(info) - @async.coroutine - def on_raw_314(self, message): + async def on_raw_314(self, message): """ WHOWAS user info. """ target, nickname, username, hostname, _, realname = message.params info = { @@ -891,10 +831,9 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whowas']: self._whowas_info[nickname].update(info) - on_raw_315 = BasicClient._ignored # End of /WHO list. + on_raw_315 = BasicClient._ignored # End of /WHO list. - @async.coroutine - def on_raw_317(self, message): + async def on_raw_317(self, message): """ WHOIS idle time. """ target, nickname, idle_time = message.params[:3] info = { @@ -904,21 +843,19 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whois']: self._whois_info[nickname].update(info) - @async.coroutine - def on_raw_318(self, message): + async def on_raw_318(self, message): """ End of /WHOIS list. """ - target, nickname = message.params[:2] + target, nickname = message.params[:2] # Mark future as done. if nickname in self._pending['whois']: future = self._pending['whois'].pop(nickname) future.set_result(self._whois_info[nickname]) - @async.coroutine - def on_raw_319(self, message): + async def on_raw_319(self, message): """ WHOIS active channels. """ target, nickname, channels = message.params[:3] - channels = { channel.lstrip() for channel in channels.strip().split(' ') } + channels = {channel.lstrip() for channel in channels.strip().split(' ')} info = { 'channels': channels } @@ -926,8 +863,7 @@ class RFC1459Support(BasicClient): if nickname in self._pending['whois']: self._whois_info[nickname].update(info) - @async.coroutine - def on_raw_324(self, message): + async def on_raw_324(self, message): """ Channel mode. """ target, channel = message.params[:2] modes = message.params[2:] @@ -936,8 +872,7 @@ class RFC1459Support(BasicClient): self.channels[channel]['modes'] = self._parse_channel_modes(channel, modes) - @async.coroutine - def on_raw_329(self, message): + async def on_raw_329(self, message): """ Channel creation time. """ target, channel, timestamp = message.params if not self.in_channel(channel): @@ -945,8 +880,7 @@ class RFC1459Support(BasicClient): self.channels[channel]['created'] = datetime.datetime.fromtimestamp(int(timestamp)) - @async.coroutine - def on_raw_332(self, message): + async def on_raw_332(self, message): """ Current topic on channel join. """ target, channel, topic = message.params if not self.in_channel(channel): @@ -954,8 +888,7 @@ class RFC1459Support(BasicClient): self.channels[channel]['topic'] = topic - @async.coroutine - def on_raw_333(self, message): + async def on_raw_333(self, message): """ Topic setter and time on channel join. """ target, channel, setter, timestamp = message.params if not self.in_channel(channel): @@ -965,8 +898,7 @@ class RFC1459Support(BasicClient): self.channels[channel]['topic_by'] = self._parse_user(setter)[0] self.channels[channel]['topic_set'] = datetime.datetime.fromtimestamp(int(timestamp)) - @async.coroutine - def on_raw_353(self, message): + async def on_raw_353(self, message): """ Response to /NAMES. """ target, visibility, channel, names = message.params if not self.in_channel(channel): @@ -1004,29 +936,25 @@ class RFC1459Support(BasicClient): self.channels[channel]['modes'][status] = [] self.channels[channel]['modes'][status].append(nick) - on_raw_366 = BasicClient._ignored # End of /NAMES list. + on_raw_366 = BasicClient._ignored # End of /NAMES list. - @async.coroutine - def on_raw_375(self, message): + async def on_raw_375(self, message): """ Start message of the day. """ self._registration_completed(message) self.motd = message.params[1] + '\n' - @async.coroutine - def on_raw_372(self, message): + async def on_raw_372(self, message): """ Append message of the day. """ self.motd += message.params[1] + '\n' - @async.coroutine - def on_raw_376(self, message): + async def on_raw_376(self, message): """ End of message of the day. """ self.motd += message.params[1] + '\n' # MOTD is done, let's tell our bot the connection is ready. - yield from self.on_connect() + await self.on_connect() - @async.coroutine - def on_raw_401(self, message): + async def on_raw_401(self, message): """ No such nick/channel. """ nickname = message.params[1] @@ -1036,32 +964,27 @@ class RFC1459Support(BasicClient): future.set_result(None) del self._whois_info[nickname] - @async.coroutine - def on_raw_402(self, message): + async def on_raw_402(self, message): """ No such server. """ - return (yield from self.on_raw_401(message)) + return (await self.on_raw_401(message)) - @async.coroutine - def on_raw_422(self, message): + async def on_raw_422(self, message): """ MOTD is missing. """ - self._registration_completed(message) + await self._registration_completed(message) self.motd = None - yield from self.on_connect() + await self.on_connect() - @async.coroutine - def on_raw_421(self, message): + async def on_raw_421(self, message): """ Server responded with 'unknown command'. """ self.logger.warning('Server responded with "Unknown command: %s"', message.params[0]) - @async.coroutine - def on_raw_432(self, message): + async def on_raw_432(self, message): """ Erroneous nickname. """ if not self.registered: # Nothing else we can do than try our next nickname. - yield from self.on_raw_433(message) + await self.on_raw_433(message) - @async.coroutine - def on_raw_433(self, message): + async def on_raw_433(self, message): """ Nickname in use. """ if not self.registered: self._registration_attempts += 1 @@ -1069,17 +992,17 @@ class RFC1459Support(BasicClient): if self._attempt_nicknames: self.set_nickname(self._attempt_nicknames.pop(0)) else: - self.set_nickname(self._nicknames[0] + '_' * (self._registration_attempts - len(self._nicknames))) + self.set_nickname( + self._nicknames[0] + '_' * (self._registration_attempts - len(self._nicknames))) - on_raw_436 = BasicClient._ignored # Nickname collision, issued right before the server kills us. + on_raw_436 = BasicClient._ignored # Nickname collision, issued right before the server kills us. - @async.coroutine - def on_raw_451(self, message): + async def on_raw_451(self, message): """ We have to register first before doing X. """ self.logger.warning('Attempted to send non-registration command before being registered.') - on_raw_451 = BasicClient._ignored # You have to register first. - on_raw_462 = BasicClient._ignored # You may not re-register. + on_raw_451 = BasicClient._ignored # You have to register first. + on_raw_462 = BasicClient._ignored # You may not re-register. ## Helpers. diff --git a/pydle/features/tls.py b/pydle/features/tls.py index c8d748a..c8fefbe 100644 --- a/pydle/features/tls.py +++ b/pydle/features/tls.py @@ -1,6 +1,5 @@ ## tls.py # TLS support. -from pydle import async import pydle.protocol from pydle.features import rfc1459 from .. import connection @@ -26,18 +25,16 @@ class TLSSupport(rfc1459.RFC1459Support): self.tls_client_cert_key = tls_client_cert_key self.tls_client_cert_password = tls_client_cert_password - @async.coroutine - def connect(self, hostname=None, port=None, tls=False, **kwargs): + async def connect(self, hostname=None, port=None, tls=False, **kwargs): """ Connect to a server, optionally over TLS. See pydle.features.RFC1459Support.connect for misc parameters. """ if not port: if tls: port = DEFAULT_TLS_PORT else: port = rfc1459.protocol.DEFAULT_PORT - return (yield from super().connect(hostname, port, tls=tls, **kwargs)) + return await super().connect(hostname, port, tls=tls, **kwargs) - @async.coroutine - def _connect(self, hostname, port, reconnect=False, password=None, encoding=pydle.protocol.DEFAULT_ENCODING, channels=[], tls=False, tls_verify=False, source_address=None): + async def _connect(self, hostname, port, reconnect=False, password=None, encoding=pydle.protocol.DEFAULT_ENCODING, channels=[], tls=False, tls_verify=False, source_address=None): """ Connect to IRC server, optionally over TLS. """ self.password = password @@ -54,22 +51,20 @@ class TLSSupport(rfc1459.RFC1459Support): self.encoding = encoding # Connect. - yield from self.connection.connect() + await self.connection.connect() ## API. - @async.coroutine - def whois(self, nickname): - info = yield from super().whois(nickname) + async def whois(self, nickname): + info = await super().whois(nickname) info.setdefault('secure', False) return info ## Message callbacks. - @async.coroutine - def on_raw_671(self, message): + async def on_raw_671(self, message): """ WHOIS: user is connected securely. """ target, nickname = message.params[:2] info = { diff --git a/pydle/features/whox.py b/pydle/features/whox.py index 41530c5..8a6ad35 100644 --- a/pydle/features/whox.py +++ b/pydle/features/whox.py @@ -1,6 +1,5 @@ ## whox.py # WHOX support. -from pydle import async from pydle.features import isupport, account NO_ACCOUNT = '0' @@ -11,10 +10,9 @@ class WHOXSupport(isupport.ISUPPORTSupport, account.AccountSupport): ## Overrides. - @async.coroutine - def on_raw_join(self, message): + async def on_raw_join(self, message): """ Override JOIN to send WHOX. """ - yield from super().on_raw_join(message) + await super().on_raw_join(message) nick, metadata = self._parse_user(message.source) channels = message.params[0].split(',') @@ -22,7 +20,7 @@ class WHOXSupport(isupport.ISUPPORTSupport, account.AccountSupport): # We joined. if 'WHOX' in self._isupport and self._isupport['WHOX']: # Get more relevant channel info thanks to WHOX. - yield from self.rawmsg('WHO', ','.join(channels), '%tnurha,{id}'.format(id=WHOX_IDENTIFIER)) + await self.rawmsg('WHO', ','.join(channels), '%tnurha,{id}'.format(id=WHOX_IDENTIFIER)) else: # Find account name of person. pass @@ -32,8 +30,7 @@ class WHOXSupport(isupport.ISUPPORTSupport, account.AccountSupport): if self.registered and 'WHOX' not in self._isupport: self.whois(nickname) - @async.coroutine - def on_raw_354(self, message): + async def on_raw_354(self, message): """ WHOX results have arrived. """ # Is the message for us? target, identifier = message.params[:2] diff --git a/pydle/utils/irccat.py b/pydle/utils/irccat.py index 84ebc9c..47a857c 100644 --- a/pydle/utils/irccat.py +++ b/pydle/utils/irccat.py @@ -8,9 +8,9 @@ import logging import asyncio from asyncio.streams import FlowControlMixin -from .. import async, Client, __version__ +from .. import Client, __version__ from . import _args - +import asyncio class IRCCat(Client): """ irccat. Takes raw messages on stdin, dumps raw messages to stdout. Life has never been easier. """ @@ -18,12 +18,12 @@ class IRCCat(Client): super().__init__(*args, **kwargs) self.async_stdin = None - @async.coroutine + @asyncio.coroutine def _send(self, data): sys.stdout.write(data) yield from super()._send(data) - @async.coroutine + @asyncio.coroutine def process_stdin(self): """ Yes. """ loop = self.eventloop.loop @@ -40,12 +40,12 @@ class IRCCat(Client): yield from self.quit('EOF') - @async.coroutine + @asyncio.coroutine def on_raw(self, message): print(message._raw) yield from super().on_raw(message) - @async.coroutine + @asyncio.coroutine def on_ctcp_version(self, source, target, contents): self.ctcp_reply(source, 'VERSION', 'pydle-irccat v{}'.format(__version__)) diff --git a/setup.py b/setup.py index c0b6603..ce55338 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name='pydle', - version='0.8.4', + version='0.8.5', packages=[ 'pydle', 'pydle.features',