Merge branch 'asyncio'

This commit is contained in:
Shiz 2018-12-23 18:56:54 +01:00
commit a7053805cf
19 changed files with 476 additions and 875 deletions

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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 = {

View File

@ -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_<type> 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_<type>_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.

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(','))

View File

@ -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.

View File

@ -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':

View File

@ -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.

View File

@ -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 = {

View File

@ -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]

View File

@ -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__))

View File

@ -2,7 +2,7 @@ from setuptools import setup
setup(
name='pydle',
version='0.8.4',
version='0.8.5',
packages=[
'pydle',
'pydle.features',