pydle_0.9.4.orig/.coveragerc0000644000000000000000000000017512311154422014167 0ustar0000000000000000[run] branch = True source = pydle omit = pydle/utils/* [report] exclude_lines = raise NotImplementedError pass pydle_0.9.4.orig/.gitignore0000644000000000000000000000012513407746146014052 0ustar0000000000000000__pycache__ *.pyc *.pyo *.swp *.swo /env /docs/_build /.coverage /.tox /*.egg-info pydle_0.9.4.orig/LICENSE.md0000644000000000000000000000273213345556615013475 0ustar0000000000000000Copyright (c) 2014-2016, Shiz All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pydle_0.9.4.orig/README.md0000644000000000000000000001116513521722173013336 0ustar0000000000000000pydle ===== Python IRC library. ------------------- pydle is a compact, flexible and standards-abiding IRC library for Python 3.5+. Features -------- * Well-organized: Thanks to the modularized feature system, it's not hard to find what you're looking for in the well-organised source code. * Standards-abiding: Based on [RFC1459](https://tools.ietf.org/html/rfc1459.html) with some small extension tweaks, with full support of optional extension standards: - [TLS](http://tools.ietf.org/html/rfc5246) - [CTCP](http://www.irchelp.org/irchelp/rfc/ctcpspec.html) - (coming soon) [DCC](http://www.irchelp.org/irchelp/rfc/dccspec.html) and extensions - [ISUPPORT/PROTOCTL](http://tools.ietf.org/html/draft-hardy-irc-isupport-00) - [IRCv3.1](http://ircv3.net) (full) - [IRCv3.2](http://ircv3.net) (base complete, most optional extensions) - [IRCv3.3](http://ircv3.net) (base in progress) * Asynchronous: IRC is an asynchronous protocol and so should be a library that implements it. Coroutines are used to process events from the server asynchronously. * Modularised and extensible: Features on top of RFC1459 are implemented as separate modules for a user to pick and choose, and write their own. Broad features are written to be as extensible as possible. * Liberally licensed: The 3-clause BSD license ensures you can use it everywhere. Basic Usage ----------- `pip install pydle` From there, you can `import pydle` and subclass `pydle.Client` for your own functionality. Setting a nickname and starting a connection over TLS: ```python import pydle # Simple echo bot. class MyOwnBot(pydle.Client): async def on_connect(self): await self.join('#bottest') async def on_message(self, target, source, message): # don't respond to our own messages, as this leads to a positive feedback loop if source != self.nickname: await self.message(target, message) client = MyOwnBot('MyBot', realname='My Bot') client.run('irc.rizon.net', tls=True, tls_verify=False) ``` *But wait, I want to handle multiple clients!* No worries! Use `pydle.ClientPool` like such: ```python pool = pydle.ClientPool() for i in range(10): client = MyOwnBot('MyBot' + str(i)) pool.connect(client, 'irc.rizon.net', 6697, tls=True, tls_verify=False) # This will make sure all clients are treated in a fair way priority-wise. pool.handle_forever() ``` Furthermore, since pydle is simply `asyncio`-based, you can run the client in your own event loop, like this: ```python import asyncio client = MyOwnBot('MyBot') loop = asyncio.get_event_loop() asyncio.ensure_future(client.connect('irc.rizon.net', tls=True, tls_verify=False), loop=loop) loop.run_forever() ``` Customization ------------- If you want to customize bot features, you can subclass `pydle.BasicClient` and one or more features from `pydle.features` or your own feature classes, like such: ```python # Only support RFC1459 (+small features), CTCP and our own ACME extension to IRC. class MyFeaturedBot(pydle.features.ctcp.CTCPSupport, acme.ACMESupport, rfc1459.RFC1459Support): pass ``` To create your own features, just subclass from `pydle.BasicClient` and start adding callbacks for IRC messages: ```python # Support custom ACME extension. class ACMESupport(pydle.BasicClient): async def on_raw_999(self, source, params): """ ACME's custom 999 numeric tells us to change our nickname. """ nickname = params[0] await self.set_nickname(nickname) ``` FAQ --- **Q: When constructing my own client class from several base classes, I get the following error: _TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y, Z_. What causes this and how can I solve it?** Pydle's use of class inheritance as a feature model may cause method resolution order conflicts if a feature inherits from a different feature, while a class inherits from both the original feature and the inheriting feature. To solve such problem, pydle offers a `featurize` function that will automatically put all classes in the right order and create an appropriate base class: ```python # Purposely mis-ordered base classes, as SASLSupport inherits from CapabilityNegotiationSupport, but everything works fine. MyBase = pydle.featurize(pydle.features.CapabilityNegotiationSupport, pydle.features.SASLSupport) class Client(MyBase): pass ``` **Q: How do I...?** Stop! Read the [documentation](http://pydle.readthedocs.org) first. If you're still in need of support, join us on IRC! We hang at `#kochira` on `irc.freenode.net`. If someone is around, they'll most likely gladly help you. License ------- Pydle is licensed under the 3-clause BSD license. See LICENSE.md for details. pydle_0.9.4.orig/docs/0000755000000000000000000000000012270071763013005 5ustar0000000000000000pydle_0.9.4.orig/pydle/0000755000000000000000000000000012270071763013172 5ustar0000000000000000pydle_0.9.4.orig/requirements.txt0000644000000000000000000000002213345556615015343 0ustar0000000000000000pure-sasl >=0.1.6 pydle_0.9.4.orig/setup.py0000644000000000000000000000202413702220363013556 0ustar0000000000000000from setuptools import setup setup( name='pydle', version='0.9.4', python_requires=">=3.5", packages=[ 'pydle', 'pydle.features', 'pydle.features.rpl_whoishost', 'pydle.features.rfc1459', 'pydle.features.ircv3', 'pydle.utils' ], extras_require={ 'sasl': 'pure-sasl >=0.1.6', # for pydle.features.sasl 'docs': 'sphinx_rtd_theme', # the Sphinx theme we use 'tests': 'pytest', # collect and run tests 'coverage': 'pytest-cov' # get test case coverage }, entry_points={ 'console_scripts': [ 'pydle = pydle.utils.run:main', 'pydle-irccat = pydle.utils.irccat:main' ] }, author='Shiz', author_email='hi@shiz.me', url='https://github.com/Shizmob/pydle', keywords='irc library python3 compact flexible', description='A compact, flexible and standards-abiding IRC library for Python 3.', license='BSD', zip_safe=True, test_suite='tests' ) pydle_0.9.4.orig/tests/0000755000000000000000000000000012310351621013204 5ustar0000000000000000pydle_0.9.4.orig/tox.ini0000644000000000000000000000072413702220363013364 0ustar0000000000000000[tox] envlist = py32,py33 [testenv] deps = coverage pytest pytest-cov commands = pip install -r requirements.txt py.test --cov pydle --cov-config .coveragerc --cov-report term-missing . [pytest] markers = slow: may take several seconds or more to complete. meta: tests the test suite itself. real: tests pydle against a real server. Requires PYDLE_TESTS_REAL_HOST and PYDLE_TESTS_REAL_PORT environment variables. unit: unit tests pydle_0.9.4.orig/docs/api/0000755000000000000000000000000012306653624013560 5ustar0000000000000000pydle_0.9.4.orig/docs/conf.py0000644000000000000000000000453213345556615014320 0ustar0000000000000000#!/usr/bin/env python3 import sys import os import os.path as path import datetime ### -- General options -- ### # Make autodoc and import work. if path.exists(path.join('..', 'pydle')): sys.path.insert(0, os.path.abspath('..')) import pydle # General information about the project. project = pydle.__name__ copyright = '2013-{current}, Shiz'.format(current=datetime.date.today().year) version = release = pydle.__version__ # Sphinx extensions to use. extensions = [ # Generate API description from code. 'sphinx.ext.autodoc', # Generate unit tests from docstrings. 'sphinx.ext.doctest', # Link to Sphinx documentation for related projects. 'sphinx.ext.intersphinx', # Generate TODO descriptions from docstrings. 'sphinx.ext.todo', # Conditional operator for documentation. 'sphinx.ext.ifconfig', # Include full source code with documentation. 'sphinx.ext.viewcode' ] # Documentation links for projects we link to. intersphinx_mapping = { 'python': ('http://docs.python.org/3', None) } ### -- Build locations -- ### templates_path = ['_templates'] exclude_patterns = ['_build'] source_suffix = '.rst' master_doc = 'index' ### -- General build settings -- ### pygments_style = 'trac' ### -- HTML output -- ## # Only set RTD theme if we're building locally. if os.environ.get('READTHEDOCS', None) != 'True': import sphinx_rtd_theme html_theme = "sphinx_rtd_theme" html_theme_path = [ sphinx_rtd_theme.get_html_theme_path() ] html_show_sphinx = False htmlhelp_basename = 'pydledoc' ### -- LaTeX output -- ## latex_documents = [ ('index', 'pydle.tex', 'pydle Documentation', 'Shiz', 'manual'), ] ### -- Manpage output -- ### man_pages = [ ('index', 'pydle', 'pydle Documentation', ['Shiz'], 1) ] ### -- Sphinx customization code -- ## def skip(app, what, name, obj, skip, options): if skip: return True if name.startswith('_') and name != '__init__': return True if name.startswith('on_data'): return True if name.startswith('on_raw_'): return True if name.startswith('on_ctcp') and name not in ('on_ctcp', 'on_ctcp_reply'): return True if name.startswith('on_isupport_'): return True if name.startswith('on_capability_'): return True return False def setup(app): app.connect('autodoc-skip-member', skip) pydle_0.9.4.orig/docs/features/0000755000000000000000000000000012366266346014634 5ustar0000000000000000pydle_0.9.4.orig/docs/index.rst0000644000000000000000000000532013345556615014656 0ustar0000000000000000===================================================== pydle - a Pythonic, extensible, compliant IRC library ===================================================== pydle is a compact, flexible and standards-abiding IRC library for Python 3, written out of frustration with existing solutions. Features -------- - **Well-organized, easily extensible** Thanks to the modular setup, pydle's functionality is seperated in modules according to the standard they were defined in. This makes specific functionality trivial to find and decreases unwanted coupling, as well as allowing users to pick-and-choose the functionality they need. No spaghetti code. - **Compliant** pydle contains modules, or "features" in pydle terminology, for almost every relevant IRC standard: * RFC1459_: The standard that defines the basic functionality of IRC - no client could live without. * TLS_: Support for chatting securely using TLS encryption. * CTCP_: The IRC Client-to-Client Protocol, allowing clients to query eachother for data. * ISUPPORT_: A method for the server to indicate non-standard or extended functionality to a client, and for clients to activate said functionality if needed. * WHOX_: Easily query status information for a lot of users at once. * IRCv3.1_: An ongoing effort to bring the IRC protocol to the twenty-first century, featuring enhancements such as extended capability negotiation and SASL authentication. * IRCv3.2_ *(in progress)*: The next, in-development iteration of IRCv3. Features among others advanced message tagging, a generalized metadata system, and online status monitoring. No half-assing functionality. - **Asynchronous** IRC is an asychronous protocol; it only makes sense a clients that implements it is asynchronous as well. Built on top of the wonderful asyncio_ library, pydle relies on proven technologies to deliver proper high-performance asynchronous functionality and primitives. pydle allows using Futures to make asynchronous programming just as intuitive as doing regular blocking operations. No callback spaghetti. - **Liberally licensed** The 3-clause BSD license ensures you can use pydle whenever, for what purpose you want. No arbitrary restrictions. .. _RFC1459: https://tools.ietf.org/html/rfc1459.html .. _TLS: https://tools.ietf.org/html/rfc5246 .. _CTCP: http://www.irchelp.org/irchelp/rfc/ctcpspec.html .. _ISUPPORT: https://tools.ietf.org/html/draft-hardy-irc-isupport-00 .. _WHOX: https://hg.quakenet.org/snircd/file/tip/doc/readme.who .. _IRCv3.1: http://ircv3.org/ .. _IRCv3.2: http://ircv3.org/ .. _asyncio: https://docs.python.org/3/library/asyncio.html Contents -------- .. toctree:: :maxdepth: 2 intro usage features/index api/index licensing pydle_0.9.4.orig/docs/intro.rst0000644000000000000000000000402413425444770014677 0ustar0000000000000000===================== Introduction to pydle ===================== What is pydle? -------------- pydle is an IRC library for Python 3.5 and up. Although old and dated on some fronts, IRC is still used by a variety of communities as the real-time communication method of choice, and the most popular IRC networks can still count on tens of thousands of users at any point during the day. pydle was created out of perceived lack of a good, Pythonic, IRC library solution that also worked with Python 3. It attempts to follow the standards properly, while also having functionality for the various extensions to the protocol that have been made over the many years. What isn't pydle? ----------------- pydle is not an end-user IRC client. Although a simple client may be trivial to implement using pydle, pydle itself does not seek out to be a full-fledged client. It does, however, provide the building blocks to which you can delegate most, if not all, of your IRC protocol headaches. pydle also isn't production-ready: while the maintainers try their utmost best to keep the API stable, pydle is still in heavy development, and APIs are prone to change or removal at least until version 1.0 has been reached. Requirements ------------ Most of pydle is written in pure, portable Python that only relies on the standard library. Optionally, if you plan to use pydle's SASL functionality for authentication, the excellent pure-sasl_ library is required. All dependencies can be installed using the standard package manager for Python, pip, and the included requirements file: .. code:: bash pip install -r requirements.txt .. _pure-sasl: https://github.com/thobbs/pure-sasl Compatibility ------------- pydle works in any interpreter that implements Python 3.5 or higher. Although mainly tested in CPython_, the standard Python implementation, there is no reason why pydle itself should not work in alternative implementations like PyPy_, as long as they support the Python 3.5 language requirements. .. _CPython: https://python.org .. _PyPy: http://pypy.org pydle_0.9.4.orig/docs/licensing.rst0000644000000000000000000000603213345556615015523 0ustar0000000000000000========= Licensing ========= pydle license ------------- :: Copyright (c) 2014-2016, Shiz All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SHIZ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------ pydle optionally relies on pure-sasl_ to provide SASL authentication methods; its license is printed in verbatim below. .. _pure-sasl: https://github.com/thobbs/pure-sasl pure-sasl license ----------------- :: http://www.opensource.org/licenses/mit-license.php Copyright 2007-2011 David Alan Cridland Copyright 2011 Lance Stout Copyright 2012 Tyler L Hobbs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pydle_0.9.4.orig/docs/usage.rst0000644000000000000000000002402313521722173014642 0ustar0000000000000000=========== Using pydle =========== .. note:: This section covers basic use of pydle. To see the full spectrum of what pydle is capable of, refer to the :doc:`API reference `. A simple client --------------- The most basic way to use pydle is instantiating a :class:`pydle.Client` object, connecting it to a server and having it handle messages forever using :meth:`pydle.Client.handle_forever`. pydle will automatically take care of ensuring that the connection persists, and will reconnect if for some reason disconnected unexpectedly. .. code:: python import pydle client = pydle.Client('MyBot') # Client.connect() is a coroutine. await client.connect('irc.freenode.net', tls=True) client.handle_forever() Adding functionality -------------------- Of course, the above client doesn't really do much, except idling and error recovery. To truly start adding functionality to the client, subclass :class:`pydle.Client` and override one or more of the IRC callbacks. .. code:: python import pydle class MyClient(pydle.Client): """ This is a simple bot that will greet people as they join the channel. """ async def on_connect(self): await super().on_connect() # Can't greet many people without joining a channel. await self.join('#kochira') async def on_join(self, channel, user): await super().on_join(channel, user) await self.message(channel, 'Hey there, {user}!', user=user) client = MyClient('MyBot') await client.connect('irc.freenode.net', tls=True) client.handle_forever() This trivial example shows a few things: 1. :meth:`pydle.Client.on_connect` is a callback that gets invoked as soon as the client is fully connected to the server. 2. :meth:`pydle.Client.on_join` is a callback that gets invoked whenever a user joins a channel. 3. Trivially enough, we can use :meth:`pydle.Client.join` to instruct the client to join a channel. 4. Finally, we can use :meth:`pydle.Client.message` to send a message to a channel or to a user; it will even format the message for us according to `advanced string formatting`_. .. hint:: It is recommended to call the callbacks of the parent class using ``super()``, to make sure whatever functionality implemented by your parent classes gets called too: pydle will gracefully handle the call even if no functionality was implemented or no callbacks overridden. .. _`advanced string formatting`: http://legacy.python.org/dev/peps/pep-3101/ Authentication ----------------- Pydle can also handle authenticating against IRC services by default, all you need to do is tell it what its credentials are. .. note:: the server must support SASL based authentication. ----------- SASL(Username + password) ----------- To authenticate, pydle simply needs to be provided with a set of credentials to present during the connection process, the most common type being a username+password pair .. code:: python import pydle client = pydle.Client( nickname="my_irc_bot[bot]", sasl_username = "username", sasl_password = "my_secret_bot_password", sasl_identity = "account_to_identify_against", ) ----------- External authentication (Certificate) ----------- As an alternative to using passwords for credentials, certificates can also be used via the SASL (External) authentication method. All you need to do is tell pydle where it can find the certificate, which it will then present during the TLS handshake when connecting to the server. .. code:: python import pydle client = pydle.Client( nickname="my_irc_bot[bot]", sasl_mechanism = "EXTERNAL", tls_client_cert = "/path/to/client_certificate" ) .. note:: this authentication mode only works over TLS connections Multiple servers, multiple clients ---------------------------------- Any pydle client instance can only be connected to a single server. That doesn't mean that you are restricted to only being active on a single server at once, though. Using a :class:`pydle.ClientPool`, you can instantiate multiple clients, connect them to different servers using :meth:`pydle.ClientPool.connect`, and handle them within a single loop. .. code:: python import pydle class MyClient(pydle.Client): """ This is a simple bot that will greet people as they join the channel. """ async def on_connect(self): await super().on_connect() # Can't greet many people without joining a channel. await self.join('#kochira') async def on_join(self, channel, user): await super().on_join(channel, user) await self.message(channel, 'Hey there, {user}!', user=user) # Setup pool and connect clients. pool = pydle.ClientPool() servers = [ 'irc.freenode.net', 'irc.rizon.net', 'irc.esper.net' ] for server in servers: client = MyClient('MyBot') pool.connect(client, server, tls=True) # Handle all clients in the pool at once. pool.handle_forever() .. warning:: While multiple :class:`pydle.ClientPool` instances can be created and ran, you should ensure a client is only active in a single :class:`pydle.ClientPool` at once. Being active in multiple pools can lead to strange things like receiving messages twice, or interleaved outgoing messages. Mixing and matching ------------------- Thanks to pydle's modular "feature" system, you don't have to support everything you want to support. You can choose just to select the options you think you need for your client by using :func:`pydle.featurize` to create a base class out of the featured you need. .. code:: python import pydle # Create a client that just supports the base RFC1459 spec, CTCP and an IRC services-style account system. MyBaseClient = pydle.featurize(pydle.features.RFC1459Support, pydle.features.CTCPSupport, pydle.features.AccountSupport) class MyClient(MyBaseClient): ... A list of all available built-in features and their use can be found at the :doc:`API reference `. In addition to this, you can of course also write your own features. Feature writing is discussed thoroughly in the :doc:`feature section `. Once you have written a feature, you can just featurize it on top of an existing client class. .. code:: python import pydle import vendor # Add vendor feature on top of the base client. MyBaseClient = pydle.featurize(pydle.Client, vendor.VendorExtensionSupport) class MyClient(MyBaseClient): ... Asynchronous functionality -------------------------- Some actions inevitably require blocking and waiting for a result. Since pydle is an asynchronous library where a client runs in a single thread, doing this blindly could lead to issues like the operation blocking the handling of messages entirely. Fortunately, pydle utilizes asyncio coroutines_ which allow you to handle a blocking operation almost as if it were a regular operation, while still retaining the benefits of asynchronous program flow. Coroutines allow pydle to be notified when a blocking operation is done, and then resume execution of the calling function appropriately. That way, blocking operations do not block the entire program flow. In order for a function to be declared as a coroutine, it has to be declared as an ``async def`` function or decorated with the :meth:`asyncio.coroutine` decorator. It can then call functions that would normally block using Python's ``await`` operator. Since a function that calls a blocking function is itself blocking too, it has to be declared a coroutine as well. .. hint:: As with a lot of things, documentation is key. Documenting that your function does blocking operations lets the caller know how to call the function, and to include the fact that it calls blocking operations in its own documentation for its own callers. For example, if you are implementing an administrative system that works based off nicknames, you might want to check if the users are identified to ``NickServ``. However, WHOISing a user using :meth:`pydle.Client.whois` would be a blocking operation. Thanks to coroutines and :meth:`pydle.Client.whois` being a blocking operation compatible with coroutines, the act of WHOISing will not block the entire program flow of the client. .. code:: python import pydle ADMIN_NICKNAMES = [ 'Shiz', 'rfw' ] class MyClient(pydle.Client): """ This is a simple bot that will tell you if you're an administrator or not. A real bot with administrative-like capabilities would probably be better off maintaining a cache that would be invalidated upon parting, quitting or changing nicknames. """ async def on_connect(self): await super().on_connect() self.join('#kochira') await def is_admin(self, nickname): """ Check whether or not a user has administrative rights for this bot. This is a blocking function: use a coroutine to call it. See pydle's documentation on blocking functionality for details. """ admin = False # Check the WHOIS info to see if the source has identified with NickServ. # This is a blocking operation, so use yield. if source in ADMIN_NICKNAMES: info = await self.whois(source) admin = info['identified'] return admin async def on_message(self, target, source, message): await super().on_message(target, source, message) # Tell a user if they are an administrator for this bot. if message.startswith('!adminstatus'): admin = await self.is_admin(source) if admin: self.message(target, '{source}: You are an administrator.', source=source) else: self.message(target, '{source}: You are not an administrator.', source=source) Writing your own blocking operation that can work with coroutines is trivial: Simply use the existing asyncio apis: https://docs.python.org/3.7/library/asyncio-task.html#coroutines-and-tasks .. _coroutines: https://en.wikipedia.org/wiki/Coroutine pydle_0.9.4.orig/docs/api/client.rst0000644000000000000000000000330212306653624015566 0ustar0000000000000000========== Client API ========== .. module:: pydle.client .. class:: pydle.Client :class:`pydle.Client` implements the featureset of :class:`pydle.BasicClient` with all the features in the :mod:`pydle.features` namespace added. For the full reference, check the :class:`pydle.BasicClient` documentation and the :doc:`Feature API reference `. .. class:: pydle.MinimalClient :class:`pydle.MinimalClient` implements the featureset of :class:`pydle.BasicClient` with some vital features in the :mod:`pydle.features` namespace added, namely: * :class:`pydle.features.RFC1459Support` * :class:`pydle.features.TLSSupport` * :class:`pydle.features.CTCPSupport` * :class:`pydle.features.ISUPPORTSupport` * :class:`pydle.features.WHOXSupport` For the full reference, check the :class:`pydle.BasicClient` documentation and the :doc:`Feature API reference `. ----- .. autoclass:: pydle.ClientPool :members: ----- .. autofunction:: pydle.featurize .. autoclass:: pydle.BasicClient :members: :attr:`users` A :class:`dict` mapping a username to a :class:`dict` with general information about that user. Available keys in the information dict: * ``nickname``: The user's nickname. * ``username``: The user's reported username on their source device. * ``realname``: The user's reported real name (GECOS). * ``hostname``: The hostname where the user is connecting from. :attr:`channels` A :class:`dict` mapping a joined channel name to a :class:`dict` with information about that channel. Available keys in the information dict: * ``users``: A :class:`set` of all users currently in the channel. pydle_0.9.4.orig/docs/api/features.rst0000644000000000000000000000114412306653624016130 0ustar0000000000000000============ Features API ============ .. module:: pydle.features RFC1459 ------- .. autoclass:: pydle.features.RFC1459Support :members: ----- Transport Layer Security ------------------------ .. autoclass:: pydle.features.TLSSupport :members: ----- Client-to-Client Protocol ------------------------- .. autoclass:: pydle.features.CTCPSupport :members: ----- Account ------- .. autoclass:: pydle.features.AccountSupport :members: ----- ISUPPORT -------- .. autoclass:: pydle.features.ISUPPORTSupport :members: ----- Extended WHO ------------ .. autoclass:: pydle.features.WHOXSupport pydle_0.9.4.orig/docs/api/index.rst0000644000000000000000000000013713425444770015425 0ustar0000000000000000============= API reference ============= .. toctree:: :maxdepth: 2 client features pydle_0.9.4.orig/docs/features/index.rst0000644000000000000000000000056113345556615016476 0ustar0000000000000000======== Features ======== pydle's main IRC functionality is divided into separate modular components called "features". These features allow you to mix and match your client to fit exactly to your requirements, as well as provide an easy way to extend pydle yourself, without having to dive into the source code. .. toctree:: :maxdepth: 2 overview writing pydle_0.9.4.orig/docs/features/overview.rst0000644000000000000000000003052013702220363017214 0ustar0000000000000000================= Built-in features ================= The following features are packaged with pydle and live in the :mod:`pydle.features` namespace. RFC1459 ======= *API:* :class:`pydle.features.RFC1459Support` RFC1459_ is the bread and butter of IRC: it is the standard that defines the very base concepts of the IRC protocol, ranging from what a channel is to the basic commands to channel limits. If you want your client to have actually any useful IRC functionality, it is recommend to include this feature. .. _RFC1459: https://tools.ietf.org/html/rfc1459.html Transport Layer Security (TLS) ============================== *API:* :class:`pydle.features.TLSSupport` Support for secure connections to the IRC server using `Transport Layer Security`_. This allows, if the server supports it, for encrypted connections between the server and the client, to prevent snooping and provide two-way authentication: both for the server to ensure its identity to the client, and for the client to ensure its identity to the server. The latter can also be used in certain service packages to automatically identify to the user account. In order to connect to a TLS-enabled server, supply ``tls=True`` to :meth:`pydle.features.TLSSupport.connect`. .. hint:: pydle does not verify server-side TLS certificates by default; to enable certificate verification, supply ``tls_verify=True`` to :meth:`pydle.features.TLSSupport.connect` as well. In order to supply a client certificate, :class:`pydle.features.TLSSupport` takes 3 additional constructor parameters: * ``tls_client_cert``: A path to the TLS client certificate. * ``tls_client_cert_key``: A path to the TLS client certificate key. * ``tls_client_cert_password``: The optional password for the certificate key. .. _`Transport Layer Security`: https://tools.ietf.org/html/rfc5246 Client-to-Client Protocol (CTCP) ================================ *API:* :class:`pydle.features.CTCPSupport` Support for encapsulation of out-of-band features into standard IRC messages using the `Client-to-Client Protocol`_. This allows you to send meta-messages to other users, requesting e.g. their local time, client version, and more, and respond to such requests. It adds `pydle.Client.ctcp(target, query, contents=None)`, which allows you to send a CTCP request to a target, and `pydle.Client.ctcp_reply(target, query, contents=None)`, which allows you to respond to CTCP requests. In addition, it registers the `pydle.Client.on_ctcp(from, query, contents)` hook, which allows you to act upon *any* CTCP request, and a per-type hook in the form of `pydle.Client.on_ctcp_(from, contents)`, which allows you to act upon CTCP requests of type `type`. `type` will always be lowercased. A few examples of `type` can be: `action`, `time`, `version`. Finally, it registers the `pydle.Client.on_ctcp_reply(from, queyr, contents)` hook, which acts similar to the above hook, except it is triggered when the client receives a CTCP response. It also registers `pydle.Client.on_ctcp__reply`, which works similar to the per-type hook described above. .. _`Client-to-Client Protocol`: http://www.irchelp.org/irchelp/rfc/ctcpspec.html Server-side Extension Support (ISUPPORT) ======================================== *API:* :class:`pydle.features.ISUPPORTSupport` Support for IRC protocol extensions using the `ISUPPORT`_ message. This feature allows pydle to support protocol extensions which are defined using the non-standard `ISUPPORT` (005) message. It includes built-in support for a number of popular `ISUPPORT`-based extensions, like `CASEMAPPING`, `CHANMODES`, `NETWORK` and `PREFIX`. It also provides the generic `pydle.Client.on_isupport_type(value)` hook, where `type` is the type of `ISUPPORT`-based extension that the server indicated support for, and `value` is the optional value of said extension, or `None` if no value was present. .. _`ISUPPORT`: http://tools.ietf.org/html/draft-hardy-irc-isupport-00 Account System ============== *API:* :class:`pydle.features.AccountSupport` Support for a generic IRC account system. Most IRC networks have some kind of account system that allows users to register and manage their nicknames and personas. This feature provides additional support in pydle for this idea and its integration into the networks. Currently, all it does is set the `identified` and `account` fields when doing a `WHOIS` query (`pydle.Client.whois(user)`) on someone, which indicate if the target user has identified to their account, and if such, their account name, if available. Extended User Tracking ====================== *API:* :class:`pydle.features.WHOXSupport` Support for better user tracking using `WHOX`_. This feature allows pydle to perform more accurate tracking of usernames, idents and account names, using the `WHOX`_ IRC extension. This allows pydle's internal user database to be more accurate and up-to-date. .. _`WHOX`: http://hg.quakenet.org/snircd/file/tip/doc/readme.who IRCv3 ===== *API:* :class:`pydle.features.IRCv3Support` A shortcut for IRCv3.1 and IRCv3.2 support; see below. IRCv3.1 ======= *API:* :class:`pydle.features.IRCv3_1Support` IRCv3.1 support. The `IRCv3 Working Group`_ is a working group organized by several network, server author, and client author representatives with the intention to standardize current non-standard IRC practices better, and modernize certain parts of the IRC protocol. The IRCv3 standards are specified as a bunch of extension specifications on top of the last widely-used IRC version, IRC v2.7, also known as `RFC1459`_. The `IRCv3.1 specification`_ adds useful features to IRC from a client perspective, including `SASL authentication`_, support for `indicating when a user identified to their account`_, and `indicating when a user went away from their PC`_. Including this feature entirely will activate all IRCv3.1 functionality for pydle. You can also opt-in to only select the two major features of IRCv3.1, the capability negotiation framework and SASL authentication support, as described below, by only including their features. .. _`IRCv3 Working Group`: http://ircv3.org .. _`IRCv3.1 specification`: http://ircv3.org .. _`SASL authentication`: http://ircv3.org/extensions/sasl-3.1 .. _`indicating when a user identified to their account`: http://ircv3.org/extensions/account-notify-3.1 .. _`indicating when a user went away from their PC`: http://ircv3.org/extensions/away-notify-3.1 Capability Negotiation Support ------------------------------ *API:* :class:`pydle.features.ircv3.CapabilityNegotiationSupport` Support for `capability negotiation` for IRC protocol extensions. This feature enables support for a generic framework for negotiating IRC protocol extension support between the client and the server. It was quickly found that `ISUPPORT` alone wasn't sufficient, as it only advertises support from the server side instead of allowing the server and client to negotiate. This is a generic base feature: enabling it on its own won't do much, instead other features like the IRCv3.1 support feature, or the SASL authentication feature will rely on it to work. This feature adds three generic hooks for feature authors whose features makes use of capability negotiation: * ``pydle.Client.on_capability__available(value)``: Called when the server indicates capability `cap` is available. Is passed a value as given by the IRC server, or `None` if no value was given Should return either a boolean indicating whether or not to request the capability, or a string indicating to request the capability with the returned value. * ``pydle.Client.on_capability__enabled()``: Called when the server has acknowledged the request of capability `cap`, and it has been enabled. Should return one of three values: `pydle.CAPABILITY_NEGOTIATING` when the capability will be further negotiated, `pydle.CAPABILITY_NEGOTIATED` when the capability has been negotiated successfully, or `pydle.CAPABILITY_FAILED` when negotiation of the capability has failed. If the function returned `pydle.CAPABILITY_NEGOTIATING`, it has to call `pydle.Client.capability_negotiated(cap, success=True)` when negotiating is finished. * ``pydle.Client.on_capability__disabled()``: Called when a previously-enabled capability `cap` has been disabled. .. _`capability negotiation`: http://ircv3.org/specification/capability-negotiation-3.1 User Authentication Support (SASL) ---------------------------------- *API:* :class:`pydle.features.ircv3.SASLSupport` Support for user authentication using `SASL`_. This feature enables users to identify to their network account using the SASL protocol and practices. Three extra arguments are added to the `pydle.Client` constructor: * ``sasl_username``: The SASL username. * ``sasl_password``: The SASL password. * ``sasl_identity``: The identity to use. Default, and most common, is ``''``. * ``sasl_mechanism``: The SASL mechanism to force. Default involves auto-selection from server-supported mechanism, or a `PLAIN`` fallback. These arguments are also set as attributes. Currently, pydle's SASL support requires on the Python `pure-sasl`_ package and is thus limited to the mechanisms it supports. The ``EXTERNAL`` mechanism is also supported without, however. .. _`SASL`: https://tools.ietf.org/html/rfc4422 .. _`pure-sasl`: https://github.com/thobbs/pure-sasl IRCv3.2 ======= *API:* :class:`pydle.features.IRCv3_2Support` Support for the IRCv3.2 specification. The `IRCv3.2 specification`_ is the second iteration of specifications from the `IRCv3 Working Group`_. This set of specification is still under development, and may change at any time. pydle's support is conservative, likely incomplete and to-be considered experimental. pydle currently supports the following IRCv3.2 extensions: * IRCv3.2 `improved capability negotiation`_. * Indication of changed ident/host using `CHGHOST`_. * Indication of `ident and host` in RFC1459's /NAMES command response. * Monitoring of a user's online status using `MONITOR`_. * `Message tags`_ to add metadata to messages. * Arbitrary key/value storage using `METADATA`_. .. _`IRCv3 Working Group`: http://ircv3.net .. _`IRCv3.2 specification`: http://ircv3.net .. _`improved capability negotiation`: http://ircv3.net/specs/core/capability-negotiation-3.2.html .. _`CHGHOST`: http://ircv3.net/specs/extensions/chghost-3.2.html .. _`MONITOR`: http://ircv3.net/specs/core/monitor-3.2.html .. _`ident and host`: http://ircv3.net/specs/extensions/userhost-in-names-3.2.html .. _`Message tags`: http://ircv3.net/specs/core/message-tags-3.2.html .. _`METADATA`: http://ircv3.net/specs/core/metadata-3.2.html As with the IRCv3.1 features, using this feature enables all of pydle's IRCv3.2 support. A user can also opt to only use individual large IRCv3.2 features by using the features below. Online Status Monitoring ------------------------ *API:* :class:`pydle.features.ircv3.MonitoringSupport` Support for monitoring a user's online status. This feature allows a client to monitor the online status of certain nicknames. It adds the `pydle.Client.monitor(nickname)` and `pydle.Client.unmonitor(nickname)` APIs to add and remove nicknames from the monitor list. If a monitored user comes online, `pydle.Client.on_user_online(nickname)` will be called. Similarly, if a user disappears offline, `pydle.Client.on_user_offline(nickname)` will be called. Tagged Messages --------------- *API:* :class:`pydle.features.ircv3.TaggedMessageSupport` Support for message metadata using tags. This feature allows pydle to parse message metadata that is transmitted using 'tags'. Currently, this has no impact on any APIs or hooks for client developers. Metadata -------- *API:* :class:`pydle.features.ircv3.MetadataSupport` Support for user and channel metadata. This allows you to set and unset arbitrary key-value information on yourself and on channels, as well as retrieve such values from other users and channels. ============== IRCd implementation-specific features ============== Optional features that for IRCds that have non-standard messages. UnrealIRCd ========== Features implementation-specific to UnrealIRCd servers. RPL_WHOIS_HOST -------------- *API:* :class:`pydle.features.rpl_whoishost.RplWhoisHostSupport` Support For `RPL_WHOIS_HOST` messages, this allows pydle to expose an IRC users real IP address and host, if the bot has access to that information. This information will fill in the `real_ip_address` and `real_hostname` fields of an :class:`pydle.Client.whois()` response.pydle_0.9.4.orig/docs/features/writing.rst0000644000000000000000000000006312366266346017050 0ustar0000000000000000================ Writing features ================ pydle_0.9.4.orig/pydle/__init__.py0000644000000000000000000000224513702220363015277 0ustar0000000000000000from . import connection, protocol, client, features 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 # noinspection PyUnresolvedReferences from asyncio import coroutine, Future __name__ = 'pydle' __version__ = '0.9.4rc1' __version_info__ = (0, 9, 4) __license__ = 'BSD' def featurize(*features): """ Put features into proper MRO order. """ from functools import cmp_to_key def compare_subclass(left, right): if issubclass(left, right): return -1 elif issubclass(right, left): return 1 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)) 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 pydle_0.9.4.orig/pydle/client.py0000644000000000000000000004105313702220363015016 0ustar0000000000000000## client.py # Basic IRC client implementation. import asyncio import logging from asyncio import new_event_loop, gather, get_event_loop, sleep from . import connection, protocol import warnings __all__ = ['Error', 'AlreadyInChannel', 'NotInChannel', 'BasicClient', 'ClientPool'] DEFAULT_NICKNAME = '' 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)) self.channel = channel class BasicClient: """ Base IRC client class. This class on its own is not complete: in order to be able to run properly, _has_message, _parse_message and _create_message have to be overloaded. """ READ_TIMEOUT = 300 RECONNECT_ON_ERROR = True RECONNECT_MAX_ATTEMPTS = 3 RECONNECT_DELAYED = True RECONNECT_DELAYS = [5, 5, 10, 30, 120, 600] @property def PING_TIMEOUT(self): warnings.warn( "PING_TIMEOUT has been moved to READ_TIMEOUT and may be removed in a future version. " "Please migrate to READ_TIMEOUT.", DeprecationWarning ) return self.READ_TIMEOUT @PING_TIMEOUT.setter def PING_TIMEOUT(self, value): warnings.warn( "PING_TIMEOUT has been moved to READ_TIMEOUT and may be removed in a future version", DeprecationWarning ) self.READ_TIMEOUT = value 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() self.realname = realname or nickname if eventloop: self.eventloop = eventloop else: self.eventloop = get_event_loop() self.own_eventloop = not eventloop self._reset_connection_attributes() self._reset_attributes() if kwargs: self.logger.warning('Unused arguments: %s', ', '.join(kwargs.keys())) def _reset_attributes(self): """ Reset attributes. """ # Record-keeping. self.channels = {} self.users = {} # Low-level data stuff. self._receive_buffer = b'' self._pending = {} self._handler_top_level = False # Misc. self.logger = logging.getLogger(__name__) # Public connection attributes. self.nickname = DEFAULT_NICKNAME self.network = None def _reset_connection_attributes(self): """ Reset connection attributes. """ self.connection = None self.encoding = None self._autojoin_channels = [] self._reconnect_attempts = 0 ## Connection. def run(self, *args, **kwargs): """ Connect and run bot in event loop. """ self.eventloop.run_until_complete(self.connect(*args, **kwargs)) try: self.eventloop.run_forever() finally: self.eventloop.stop() 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.') # Disconnect from current connection. if self.connected: await self.disconnect(expected=True) # Reset attributes and connect. if not reconnect: self._reset_connection_attributes() 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.create_task(self.handle_forever()) async def disconnect(self, expected=True): """ Disconnect from server. """ if self.connected: # Schedule disconnect. await self._disconnect(expected) async def _disconnect(self, expected): # Shutdown connection. await self.connection.disconnect() # Reset any attributes. self._reset_attributes() # Callback. await self.on_disconnect(expected) # Shut down event loop. if expected and self.own_eventloop: self.connection.stop() 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.encoding = encoding # Connect. await self.connection.connect() def _reconnect_delay(self): """ Calculate reconnection delay. """ if self.RECONNECT_ON_ERROR and self.RECONNECT_DELAYED: if self._reconnect_attempts >= len(self.RECONNECT_DELAYS): return self.RECONNECT_DELAYS[-1] else: return self.RECONNECT_DELAYS[self._reconnect_attempts] else: return 0 ## Internal database management. def _create_channel(self, channel): self.channels[channel] = { 'users': set(), } def _destroy_channel(self, channel): # Copy set to prevent a runtime error when destroying the user. for user in set(self.channels[channel]['users']): self._destroy_user(user, channel) del self.channels[channel] def _create_user(self, nickname): # Servers are NOT users. if not nickname or '.' in nickname: return self.users[nickname] = { 'nickname': nickname, 'username': None, 'realname': None, 'hostname': None } def _sync_user(self, nick, metadata): # Create user in database. if nick not in self.users: self._create_user(nick) if nick not in self.users: return self.users[nick].update(metadata) def _rename_user(self, user, new): if user in self.users: self.users[new] = self.users[user] self.users[new]['nickname'] = new del self.users[user] else: self._create_user(new) if new not in self.users: return for ch in self.channels.values(): # Rename user in channel list. if user in ch['users']: ch['users'].discard(user) ch['users'].add(new) def _destroy_user(self, nickname, channel=None): if channel: channels = [self.channels[channel]] else: channels = self.channels.values() for ch in channels: # Remove from nicklist. ch['users'].discard(nickname) # If we're not in any common channels with the user anymore, we have no reliable way to keep their info up-to-date. # Remove the user. if not channel or not any(nickname in ch['users'] for ch in self.channels.values()): del self.users[nickname] def _parse_user(self, data): """ Parse user and return nickname, metadata tuple. """ 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 '*') 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): """ Check if given argument is a channel name or not. """ return True def in_channel(self, channel): """ Check if we are currently in the given channel. """ return channel in self.channels.keys() def is_same_nick(self, left, right): """ Check if given nicknames are equal. """ return left == right def is_same_channel(self, left, right): """ Check if given channel names are equal. """ return left == right ## IRC attributes. @property def connected(self): """ Whether or not we are connected. """ return self.connection and self.connection.connected @property def server_tag(self): if self.connected and self.connection.hostname: if self.network: tag = self.network.lower() else: tag = self.connection.hostname.lower() # Remove hostname prefix. if tag.startswith('irc.'): tag = tag[4:] # Check if host is either an FQDN or IPv4. if '.' in tag: # Attempt to cut off TLD. host, suffix = tag.rsplit('.', 1) # Make sure we aren't cutting off the last octet of an IPv4. try: int(suffix) except ValueError: tag = host return tag else: return None ## IRC API. async def raw(self, message): """ Send raw command. """ await self._send(message) async def rawmsg(self, command, *args, **kwargs): """ Send raw message. """ message = str(self._create_message(command, *args, **kwargs)) await self._send(message) ## Overloadable callbacks. async def on_connect(self): """ Callback called when the client has connected successfully. """ # Reset reconnect attempts. self._reconnect_attempts = 0 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): # 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) else: self.logger.error('Unexpected disconnect. Attempting to reconnect.') # Wait and reconnect. await sleep(delay) await self.connect(reconnect=True) else: self.logger.error('Unexpected disconnect. Giving up.') ## Message dispatch. def _has_message(self): """ Whether or not we have messages available for processing. """ raise NotImplementedError() def _create_message(self, command, *params, **kwargs): raise NotImplementedError() def _parse_message(self): raise NotImplementedError() 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)) await self.connection.send(input) async def handle_forever(self): """ Handle data forever. """ while self.connected: try: data = await self.connection.recv(timeout=self.READ_TIMEOUT) except asyncio.TimeoutError: self.logger.warning( '>> Receive timeout reached, sending ping to check connection state...') try: await self.rawmsg("PING", self.server_tag) data = await self.connection.recv(timeout=self.READ_TIMEOUT) except (asyncio.TimeoutError, ConnectionResetError) as e: data = None if not data: if self.connected: await self.disconnect(expected=False) break await self.on_data(data) ## Raw message handlers. async def on_data(self, data): """ Handle received data. """ self._receive_buffer += data while self._has_message(): message = self._parse_message() self.eventloop.create_task(self.on_raw(message)) async def on_data_error(self, exception): """ Handle error. """ self.logger.error('Encountered error on socket.', exc_info=(type(exception), exception, None)) await self.disconnect(expected=False) 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) if isinstance(message.command, int): cmd = str(message.command).zfill(3) else: cmd = message.command # Invoke dispatcher, if we have one. method = 'on_raw_' + cmd.lower() try: # Set _top_level so __getattr__() can decide whether to return on_unknown or _ignored for unknown handlers. # The reason for this is that features can always call super().on_raw_* safely and thus don't need to care for other features, # while unknown messages for which no handlers exist at all are still logged. self._handler_top_level = True handler = getattr(self, method) self._handler_top_level = False await handler(message) except: self.logger.exception('Failed to execute %s handler.', method) async def on_unknown(self, message): """ Unknown command. """ self.logger.warning('Unknown command: [%s] %s %s', message.source, message.command, message.params) async def _ignored(self, message): """ Ignore message. """ pass def __getattr__(self, attr): """ Return on_unknown or _ignored for unknown handlers, depending on the invocation type. """ # Is this a raw handler? if attr.startswith('on_raw_'): # Are we in on_raw() trying to find any message handler? if self._handler_top_level: # In that case, return the method that logs and possibly acts on unknown messages. return self.on_unknown # Are we in an existing handler calling super()? else: # Just ignore it, then. return self._ignored # This isn't a handler, just raise an error. raise AttributeError(attr) class ClientPool: """ A pool of clients that are ran and handled in parallel. """ def __init__(self, clients=None, eventloop=None): self.eventloop = eventloop if eventloop else new_event_loop() self.clients = set(clients or []) self.connect_args = {} 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. """ self.clients.remove(client) del self.connect_args[client] asyncio.run_coroutine_threadsafe(client.disconnect(expected=True), self.eventloop) 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. """ # 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) # run the connections self.eventloop.run_until_complete(connections) # run the clients self.eventloop.run_forever() pydle_0.9.4.orig/pydle/connection.py0000644000000000000000000001022213702220363015671 0ustar0000000000000000import asyncio import os.path as path import ssl import sys __all__ = ['Connection'] DEFAULT_CA_PATHS = { 'linux': '/etc/ssl/certs', 'linux2': '/etc/ssl/certs', 'freebsd': '/etc/ssl/certs' } MESSAGE_THROTTLE_TRESHOLD = 3 MESSAGE_THROTTLE_DELAY = 2 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): self.hostname = hostname self.port = port self.source_address = source_address self.ping_timeout = ping_timeout self.tls = tls self.tls_context = None self.tls_verify = tls_verify self.tls_certificate_file = tls_certificate_file self.tls_certificate_keyfile = tls_certificate_keyfile self.tls_certificate_password = tls_certificate_password self.reader = None self.writer = None self.eventloop = eventloop or asyncio.new_event_loop() 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) = 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. """ # Create context manually, as we're going to set our own options. tls_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) # 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) # Set some relevant options: # - No server should use SSLv2 or SSLv3 any more, they are outdated and full of security holes. (RFC6176, RFC7568) # - Disable compression in order to counter the CRIME attack. (https://en.wikipedia.org/wiki/CRIME_%28security_exploit%29) # - Disable session resumption to maintain perfect forward secrecy. (https://timtaubert.de/blog/2014/11/the-sad-state-of-server-side-tls-session-resumption-implementations/) for opt in ['NO_SSLv2', 'NO_SSLv3', 'NO_COMPRESSION', 'NO_TICKET']: if hasattr(ssl, 'OP_' + opt): tls_context.options |= getattr(ssl, 'OP_' + opt) # Set TLS verification options. if self.tls_verify: # Load certificate verification paths. tls_context.set_default_verify_paths() if sys.platform in DEFAULT_CA_PATHS and path.isdir(DEFAULT_CA_PATHS[sys.platform]): tls_context.load_verify_locations(capath=DEFAULT_CA_PATHS[sys.platform]) # If we want to verify the TLS connection, we first need a certicate. tls_context.verify_mode = ssl.CERT_REQUIRED # And have python call match_hostname in do_handshake tls_context.check_hostname = True # We don't check for revocation, because that's impractical still (https://www.imperialviolet.org/2012/02/05/crlsets.html) return tls_context async def disconnect(self): """ Disconnect from target. """ if not self.connected: return self.writer.close() self.reader = None self.writer = None @property def connected(self): """ 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.call_soon(self.eventloop.stop) async def send(self, data): """ Add data to send queue. """ self.writer.write(data) await self.writer.drain() async def recv(self, *, timeout=None): return await asyncio.wait_for(self.reader.readline(), timeout=timeout) pydle_0.9.4.orig/pydle/features/0000755000000000000000000000000012270071763015010 5ustar0000000000000000pydle_0.9.4.orig/pydle/protocol.py0000644000000000000000000000225613345556615015422 0ustar0000000000000000## protocol.py # IRC implementation-agnostic constants/helpers. import re from abc import abstractmethod DEFAULT_ENCODING = 'utf-8' FALLBACK_ENCODING = 'iso-8859-1' ## Errors. class ProtocolViolation(Exception): """ An error that occurred while parsing or constructing an IRC message that violates the IRC protocol. """ def __init__(self, msg, message): super().__init__(msg) self.irc_message = message ## Bases. class Message: """ Abstract message class. Messages must inherit from this class. """ @classmethod @abstractmethod def parse(cls, line, encoding=DEFAULT_ENCODING): """ Parse data into IRC message. Return a Message instance or raise an error. """ raise NotImplementedError() @abstractmethod def construct(self, force=False): """ Convert message into raw IRC command. If `force` is True, don't attempt to check message validity. """ raise NotImplementedError() def __str__(self): return self.construct() ## Misc. def identifierify(name): """ Clean up name so it works for a Python identifier. """ name = name.lower() name = re.sub('[^a-z0-9]', '_', name) return name pydle_0.9.4.orig/pydle/utils/0000755000000000000000000000000012270071763014332 5ustar0000000000000000pydle_0.9.4.orig/pydle/features/__init__.py0000644000000000000000000000101012521204137017101 0ustar0000000000000000from . import rfc1459, account, ctcp, tls, isupport, whox, ircv3 from .rfc1459 import RFC1459Support from .account import AccountSupport from .ctcp import CTCPSupport from .tls import TLSSupport from .isupport import ISUPPORTSupport from .whox import WHOXSupport from .ircv3 import IRCv3Support, IRCv3_1Support, IRCv3_2Support ALL = [ IRCv3Support, WHOXSupport, ISUPPORTSupport, CTCPSupport, AccountSupport, TLSSupport, RFC1459Support ] LITE = [ WHOXSupport, ISUPPORTSupport, CTCPSupport, TLSSupport, RFC1459Support ] pydle_0.9.4.orig/pydle/features/account.py0000644000000000000000000000324213407746146017026 0ustar0000000000000000## account.py # Account system support. from pydle.features import rfc1459 import asyncio class AccountSupport(rfc1459.RFC1459Support): ## Internal. def _create_user(self, nickname): super()._create_user(nickname) if nickname in self.users: self.users[nickname].update({ 'account': None, 'identified': False }) 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.whois(new) ## IRC API. @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 def on_raw_307(self, message): """ WHOIS: User has identified for this nickname. (Anope) """ target, nickname = message.params[:2] info = { 'identified': True } if nickname in self.users: self._sync_user(nickname, info) if nickname in self._pending['whois']: self._whois_info[nickname].update(info) async def on_raw_330(self, message): """ WHOIS account name (Atheme). """ target, nickname, account = message.params[:3] info = { 'account': account, 'identified': True } if nickname in self.users: self._sync_user(nickname, info) if nickname in self._pending['whois']: self._whois_info[nickname].update(info) pydle_0.9.4.orig/pydle/features/ctcp.py0000644000000000000000000001033613521722173016314 0ustar0000000000000000## ctcp.py # Client-to-Client-Protocol (CTCP) support. import pydle.protocol from pydle.features import rfc1459 from pydle import client __all__ = [ 'CTCPSupport' ] CTCP_DELIMITER = '\x01' CTCP_ESCAPE_CHAR = '\x16' class CTCPSupport(rfc1459.RFC1459Support): """ Support for CTCP messages. """ ## Callbacks. async def on_ctcp(self, by, target, what, contents): """ Callback called when the user received a CTCP message. Client subclasses can override on_ctcp_ to be called when receiving a message of that specific CTCP type, in addition to this callback. """ pass async def on_ctcp_reply(self, by, target, what, response): """ Callback called when the user received a CTCP response. Client subclasses can override on_ctcp__reply to be called when receiving a reply of that specific CTCP type, in addition to this callback. """ pass async def on_ctcp_version(self, by, target, contents): """ Built-in CTCP version as some networks seem to require it. """ import pydle version = '{name} v{ver}'.format(name=pydle.__name__, ver=pydle.__version__) await self.ctcp_reply(by, 'VERSION', version) ## IRC API. 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) await self.message(target, construct_ctcp(query, contents)) 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) await self.notice(target, construct_ctcp(query, response)) ## Handler overrides. async def on_raw_privmsg(self, message): """ Modify PRIVMSG to redirect CTCP messages. """ nick, metadata = self._parse_user(message.source) target, msg = message.params if is_ctcp(msg): self._sync_user(nick, metadata) type, contents = parse_ctcp(msg) # Find dedicated handler if it exists. attr = 'on_ctcp_' + pydle.protocol.identifierify(type) if hasattr(self, attr): await getattr(self, attr)(nick, target, contents) # Invoke global handler. await self.on_ctcp(nick, target, type, contents) else: await super().on_raw_privmsg(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 if is_ctcp(msg): self._sync_user(nick, metadata) _type, response = parse_ctcp(msg) # Find dedicated handler if it exists. attr = 'on_ctcp_' + pydle.protocol.identifierify(_type) + '_reply' if hasattr(self, attr): await getattr(self, attr)(nick, target, response) # Invoke global handler. await self.on_ctcp_reply(nick, target, _type, response) else: await super().on_raw_notice(message) ## Helpers. def is_ctcp(message): """ Check if message follows the CTCP format. """ return message.startswith(CTCP_DELIMITER) and message.endswith(CTCP_DELIMITER) def construct_ctcp(*parts): """ Construct CTCP message. """ message = ' '.join(parts) message = message.replace('\0', CTCP_ESCAPE_CHAR + '0') message = message.replace('\n', CTCP_ESCAPE_CHAR + 'n') message = message.replace('\r', CTCP_ESCAPE_CHAR + 'r') message = message.replace(CTCP_ESCAPE_CHAR, CTCP_ESCAPE_CHAR + CTCP_ESCAPE_CHAR) return CTCP_DELIMITER + message + CTCP_DELIMITER def parse_ctcp(query): """ Strip and de-quote CTCP messages. """ query = query.strip(CTCP_DELIMITER) query = query.replace(CTCP_ESCAPE_CHAR + '0', '\0') query = query.replace(CTCP_ESCAPE_CHAR + 'n', '\n') query = query.replace(CTCP_ESCAPE_CHAR + 'r', '\r') query = query.replace(CTCP_ESCAPE_CHAR + CTCP_ESCAPE_CHAR, CTCP_ESCAPE_CHAR) if ' ' in query: return query.split(' ', 1) return query, None pydle_0.9.4.orig/pydle/features/ircv3/0000755000000000000000000000000012521204137016026 5ustar0000000000000000pydle_0.9.4.orig/pydle/features/isupport.py0000644000000000000000000002344213407746146017263 0ustar0000000000000000## isupport.py # ISUPPORT (server-side IRC extension indication) support. # See: http://tools.ietf.org/html/draft-hardy-irc-isupport-00 import collections import pydle.protocol from pydle.features import rfc1459 __all__ = [ 'ISUPPORTSupport' ] FEATURE_DISABLED_PREFIX = '-' BAN_EXCEPT_MODE = 'e' INVITE_EXCEPT_MODE = 'I' class ISUPPORTSupport(rfc1459.RFC1459Support): """ ISUPPORT support. """ ## Internal overrides. def _reset_attributes(self): super()._reset_attributes() self._isupport = {} self._extban_types = [] self._extban_prefix = None def _create_channel(self, channel): """ Create channel with optional ban and invite exception lists. """ super()._create_channel(channel) if 'EXCEPTS' in self._isupport: self.channels[channel]['exceptlist'] = None if 'INVEX' in self._isupport: self.channels[channel]['inviteexceptlist'] = None ## Command handlers. async def on_raw_005(self, message): """ ISUPPORT indication. """ isupport = {} # Parse response. # Strip target (first argument) and 'are supported by this server' (last argument). for feature in message.params[1:-1]: if feature.startswith(FEATURE_DISABLED_PREFIX): value = False elif '=' in feature: feature, value = feature.split('=', 1) else: value = True isupport[feature.upper()] = value # Update internal dict first. self._isupport.update(isupport) # And have callbacks update other internals. for entry, value in isupport.items(): if value != False: # A value of True technically means there was no value supplied; correct this for callbacks. if value == True: value = None method = 'on_isupport_' + pydle.protocol.identifierify(entry) if hasattr(self, method): await getattr(self, method)(value) ## ISUPPORT handlers. async def on_isupport_awaylen(self, value): """ Away message length limit. """ self._away_message_length_limit = int(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 def on_isupport_channellen(self, value): """ Channel name length limit. """ self._channel_length_limit = int(value) async def on_isupport_chanlimit(self, value): """ Simultaneous channel limits for user. """ self._channel_limits = {} for entry in value.split(','): types, limit = entry.split(':') # Assign limit to channel type group and add lookup entry for type. self._channel_limits[frozenset(types)] = int(limit) for prefix in types: self._channel_limit_groups[prefix] = frozenset(types) 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(',', ''))) # The reason we have to do it like this is because other ISUPPORTs (e.g. PREFIX) may update these values as well. if not rfc1459.protocol.BEHAVIOUR_LIST in self._channel_modes_behaviour: self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_LIST] = set() self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_LIST].update(list) if not rfc1459.protocol.BEHAVIOUR_PARAMETER in self._channel_modes_behaviour: self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_PARAMETER] = set() self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_PARAMETER].update(param) if not rfc1459.protocol.BEHAVIOUR_PARAMETER_ON_SET in self._channel_modes_behaviour: self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_PARAMETER_ON_SET] = set() self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_PARAMETER_ON_SET].update(param_set) if not rfc1459.protocol.BEHAVIOUR_NO_PARAMETER in self._channel_modes_behaviour: self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_NO_PARAMETER] = set() self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_NO_PARAMETER].update(noparams) async def on_isupport_chantypes(self, value): """ Channel name prefix symbols. """ if not value: value = '' self._channel_prefixes = set(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 def on_isupport_extban(self, value): """ Extended ban prefixes. """ self._extban_prefix, types = value.split(',') self._extban_types = set(types) 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 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 def on_isupport_maxchannels(self, value): """ Old version of CHANLIMIT. """ if 'CHANTYPES' in self._isupport and 'CHANLIMIT' not in self._isupport: self._channel_limits = {} prefixes = self._isupport['CHANTYPES'] # Assume the limit is for all types of channels. Make a single group for all types. self._channel_limits[frozenset(prefixes)] = int(value) for prefix in prefixes: self._channel_limit_groups[prefix] = frozenset(prefixes) async def on_isupport_maxlist(self, value): """ Limits on channel modes involving lists. """ self._list_limits = {} for entry in value.split(','): modes, limit = entry.split(':') # Assign limit to mode group and add lookup entry for mode. self._list_limits[frozenset(modes)] = int(limit) for mode in modes: self._list_limit_groups[mode] = frozenset(modes) async def on_isupport_maxpara(self, value): """ Limits to parameters given to command. """ self._command_parameter_limit = int(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 def on_isupport_namesx(self, value): """ Let the server know we do in fact support NAMESX. Effectively the same as CAP multi-prefix. """ await self.rawmsg('PROTOCTL', 'NAMESX') async def on_isupport_network(self, value): """ IRC network name. """ self.network = value async def on_isupport_nicklen(self, value): """ Nickname length limit. """ self._nickname_length_limit = int(value) async def on_isupport_prefix(self, value): """ Nickname prefixes on channels and their associated modes. """ if not value: # No prefixes support. self._nickname_prefixes = collections.OrderedDict() return modes, prefixes = value.lstrip('(').split(')', 1) # Update valid channel modes and their behaviour as CHANMODES doesn't include PREFIX modes. self._channel_modes.update(set(modes)) if not rfc1459.protocol.BEHAVIOUR_PARAMETER in self._channel_modes_behaviour: self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_PARAMETER] = set() self._channel_modes_behaviour[rfc1459.protocol.BEHAVIOUR_PARAMETER].update(set(modes)) self._nickname_prefixes = collections.OrderedDict() for mode, prefix in zip(modes, prefixes): self._nickname_prefixes[prefix] = mode 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 def on_isupport_targmax(self, value): """ The maximum number of targets certain types of commands can affect. """ if not value: return for entry in value.split(','): command, limit = entry.split(':', 1) if not limit: continue self._target_limits[command] = int(limit) async def on_isupport_topiclen(self, value): """ Channel topic length limit. """ self._topic_length_limit = int(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': break else: prefix = '@' self._status_message_prefixes.add(prefix) 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': break else: prefix = '+' self._status_message_prefixes.add(prefix) pydle_0.9.4.orig/pydle/features/rfc1459/0000755000000000000000000000000012276027660016110 5ustar0000000000000000pydle_0.9.4.orig/pydle/features/rpl_whoishost/0000755000000000000000000000000013671517400017713 5ustar0000000000000000pydle_0.9.4.orig/pydle/features/tls.py0000644000000000000000000000475613407746146016207 0ustar0000000000000000## tls.py # TLS support. import pydle.protocol from pydle.features import rfc1459 from .. import connection __all__ = ['TLSSupport'] DEFAULT_TLS_PORT = 6697 class TLSSupport(rfc1459.RFC1459Support): """ TLS support. Pass tls_client_cert, tls_client_cert_key and optionally tls_client_cert_password to have pydle send a client certificate upon TLS connections. """ ## Internal overrides. def __init__(self, *args, tls_client_cert=None, tls_client_cert_key=None, tls_client_cert_password=None, **kwargs): super().__init__(*args, **kwargs) self.tls_client_cert = tls_client_cert self.tls_client_cert_key = tls_client_cert_key self.tls_client_cert_password = tls_client_cert_password 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 await super().connect(hostname, port, tls=tls, **kwargs) 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 # Create connection if we can't reuse it. if not reconnect: self._autojoin_channels = channels self.connection = connection.Connection(hostname, port, source_address=source_address, tls=tls, tls_verify=tls_verify, tls_certificate_file=self.tls_client_cert, tls_certificate_keyfile=self.tls_client_cert_key, tls_certificate_password=self.tls_client_cert_password, eventloop=self.eventloop) self.encoding = encoding # Connect. await self.connection.connect() ## API. async def whois(self, nickname): info = await super().whois(nickname) info.setdefault('secure', False) return info ## Message callbacks. async def on_raw_671(self, message): """ WHOIS: user is connected securely. """ target, nickname = message.params[:2] info = { 'secure': True } if nickname in self._whois_info: self._whois_info[nickname].update(info) pydle_0.9.4.orig/pydle/features/whox.py0000644000000000000000000000335713407746146016366 0ustar0000000000000000## whox.py # WHOX support. from pydle.features import isupport, account NO_ACCOUNT = '0' # Maximum of 3 characters because Charybdis stupidity. The ASCII values of 'pydle' added together. WHOX_IDENTIFIER = '542' class WHOXSupport(isupport.ISUPPORTSupport, account.AccountSupport): ## Overrides. async def on_raw_join(self, message): """ Override JOIN to send WHOX. """ await super().on_raw_join(message) nick, metadata = self._parse_user(message.source) channels = message.params[0].split(',') if self.is_same_nick(self.nickname, nick): # We joined. if 'WHOX' in self._isupport and self._isupport['WHOX']: # Get more relevant channel info thanks to WHOX. await self.rawmsg('WHO', ','.join(channels), '%tnurha,{id}'.format(id=WHOX_IDENTIFIER)) else: # Find account name of person. pass def _create_user(self, nickname): super()._create_user(nickname) if self.registered and 'WHOX' not in self._isupport: self.whois(nickname) async def on_raw_354(self, message): """ WHOX results have arrived. """ # Is the message for us? target, identifier = message.params[:2] if identifier != WHOX_IDENTIFIER: return # Great. Extract relevant information. metadata = { 'nickname': message.params[4], 'username': message.params[2], 'realname': message.params[6], 'hostname': message.params[3], } if message.params[5] != NO_ACCOUNT: metadata['identified'] = True metadata['account'] = message.params[5] self._sync_user(metadata['nickname'], metadata) pydle_0.9.4.orig/pydle/features/ircv3/__init__.py0000644000000000000000000000103213345556615020153 0ustar0000000000000000## IRCv3.1 support. from . import cap, sasl, ircv3_1 from .cap import CapabilityNegotiationSupport from .sasl import SASLSupport from .ircv3_1 import IRCv3_1Support ## IRCv3.2 support. from . import monitor, tags, ircv3_2 from .monitor import MonitoringSupport from .tags import TaggedMessageSupport from .metadata import MetadataSupport from .ircv3_2 import IRCv3_2Support ## IRCv3.3 support. from . import ircv3_3 from .ircv3_3 import IRCv3_3Support class IRCv3Support(IRCv3_3Support, IRCv3_2Support, IRCv3_1Support): pass pydle_0.9.4.orig/pydle/features/ircv3/cap.py0000644000000000000000000001643613413743551017166 0ustar0000000000000000## cap.py # Server <-> client optional extension indication support. # See also: http://ircv3.atheme.org/specification/capability-negotiation-3.1 import pydle.protocol from pydle.features import rfc1459 __all__ = [ 'CapabilityNegotiationSupport', 'NEGOTIATED', 'NEGOTIATING', 'FAILED' ] DISABLED_PREFIX = '-' ACKNOWLEDGEMENT_REQUIRED_PREFIX = '~' STICKY_PREFIX = '=' PREFIXES = '-~=' CAPABILITY_VALUE_DIVIDER = '=' NEGOTIATING = True NEGOTIATED = None FAILED = False class CapabilityNegotiationSupport(rfc1459.RFC1459Support): """ CAP command support. """ ## Internal overrides. def _reset_attributes(self): super()._reset_attributes() self._capabilities = {} self._capabilities_requested = set() self._capabilities_negotiating = set() async def _register(self): """ Hijack registration to send a CAP LS first. """ if self.registered: self.logger.debug("skipping cap registration, already registered!") return # Ask server to list capabilities. await self.rawmsg('CAP', 'LS', '302') # Register as usual. await super()._register() def _capability_normalize(self, cap): cap = cap.lstrip(PREFIXES).lower() if CAPABILITY_VALUE_DIVIDER in cap: cap, _, value = cap.partition(CAPABILITY_VALUE_DIVIDER) else: value = None return cap, value ## API. 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: await self.rawmsg('CAP', 'END') ## Message handlers. async def on_raw_cap(self, message): """ Handle CAP message. """ target, subcommand = message.params[:2] params = message.params[2:] # Call handler. attr = 'on_raw_cap_' + pydle.protocol.identifierify(subcommand) if hasattr(self, attr): await getattr(self, attr)(params) else: self.logger.warning('Unknown CAP subcommand sent from server: %s', subcommand) async def on_raw_cap_ls(self, params): """ Update capability mapping. Request capabilities. """ to_request = set() for capab in params[0].split(): capab, value = self._capability_normalize(capab) # Only process new capabilities. if capab in self._capabilities: continue # Check if we support the capability. attr = 'on_capability_' + pydle.protocol.identifierify(capab) + '_available' supported = (await getattr(self, attr)(value)) if hasattr(self, attr) else False if supported: if isinstance(supported, str): to_request.add(capab + CAPABILITY_VALUE_DIVIDER + supported) else: to_request.add(capab) else: self._capabilities[capab] = False if to_request: # Request some capabilities. self._capabilities_requested.update(x.split(CAPABILITY_VALUE_DIVIDER, 1)[0] for x in to_request) await self.rawmsg('CAP', 'REQ', ' '.join(to_request)) else: # No capabilities requested, end negotiation. await self.rawmsg('CAP', 'END') async def on_raw_cap_list(self, params): """ Update active capabilities. """ self._capabilities = { capab: False for capab in self._capabilities } for capab in params[0].split(): capab, value = self._capability_normalize(capab) self._capabilities[capab] = value if value else True 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) self._capabilities_requested.discard(cp) # Determine capability type and callback. if capab.startswith(DISABLED_PREFIX): self._capabilities[cp] = False attr = 'on_capability_' + pydle.protocol.identifierify(cp) + '_disabled' elif capab.startswith(STICKY_PREFIX): # Can't disable it. Do nothing. self.logger.error('Could not disable capability %s.', cp) continue else: self._capabilities[cp] = value if value else True attr = 'on_capability_' + pydle.protocol.identifierify(cp) + '_enabled' # Indicate we're gonna use this capability if needed. if capab.startswith(ACKNOWLEDGEMENT_REQUIRED_PREFIX): await self.rawmsg('CAP', 'ACK', cp) # Run callback. if hasattr(self, attr): status = await getattr(self, attr)() else: status = NEGOTIATED # If the process needs more time, add it to the database and end later. if status == NEGOTIATING: self._capabilities_negotiating.add(cp) elif status == FAILED: # Ruh-roh, negotiation failed. Disable the capability. self.logger.warning('Capability negotiation for %s failed. Attempting to disable capability again.', 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: await self.rawmsg('CAP', 'END') 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) self._capabilities[capab] = False self._capabilities_requested.discard(capab) # If we have no capabilities left to process, end it. if not self._capabilities_requested and not self._capabilities_negotiating: await self.rawmsg('CAP', 'END') 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): await getattr(self, attr)() await self.on_raw_cap_nak(params) async def on_raw_cap_new(self, params): await self.on_raw_cap_ls(params) 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() await self.rawmsg('CAP', 'END') async def on_raw_421(self, message): """ Hijack to ignore the absence of a CAP command. """ if message.params[0] == 'CAP': return await super().on_raw_421(message) async def on_raw_451(self, message): """ Hijack to ignore the absence of a CAP command. """ if message.params[0] == 'CAP': return await super().on_raw_451(message) pydle_0.9.4.orig/pydle/features/ircv3/ircv3_1.py0000644000000000000000000000700713407746146017671 0ustar0000000000000000## ircv3_1.py # IRCv3.1 full spec support. from pydle.features import account, tls from . import cap from . import sasl __all__ = [ 'IRCv3_1Support' ] NO_ACCOUNT = '*' class IRCv3_1Support(sasl.SASLSupport, cap.CapabilityNegotiationSupport, account.AccountSupport, tls.TLSSupport): """ Support for IRCv3.1's base and optional extensions. """ def _rename_user(self, user, new): # If the server supports account-notify, we will be told about the registration status changing. # As such, we can skip the song and dance pydle.features.account does. if self._capabilities.get('account-notify', False): account = self.users.get(user, {}).get('account', None) identified = self.users.get(user, {}).get('identified', False) super()._rename_user(user, new) if self._capabilities.get('account-notify', False): self._sync_user(new, {'account': account, 'identified': identified}) ## IRC callbacks. async def on_capability_account_notify_available(self, value): """ Take note of user account changes. """ return True async def on_capability_away_notify_available(self, value): """ Take note of AWAY messages. """ return True async def on_capability_extended_join_available(self, value): """ Take note of user account and realname on JOIN. """ return True async def on_capability_multi_prefix_available(self, value): """ Thanks to how underlying client code works we already support multiple prefixes. """ return True async def on_capability_tls_available(self, value): """ We never need to request this explicitly. """ return False ## Message handlers. async def on_raw_account(self, message): """ Changes in the associated account for a nickname. """ if not self._capabilities.get('account-notify', False): return nick, metadata = self._parse_user(message.source) account = message.params[0] if nick not in self.users: return self._sync_user(nick, metadata) if account == NO_ACCOUNT: self._sync_user(nick, { 'account': None, 'identified': False }) else: self._sync_user(nick, { 'account': account, 'identified': True }) async def on_raw_away(self, message): """ Process AWAY messages. """ if 'away-notify' not in self._capabilities or not self._capabilities['away-notify']: return nick, metadata = self._parse_user(message.source) if nick not in self.users: return self._sync_user(nick, metadata) self.users[nick]['away'] = len(message.params) > 0 self.users[nick]['away_message'] = message.params[0] if len(message.params) > 0 else None 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) channels, account, realname = message.params self._sync_user(nick, metadata) # Emit a fake join message. fakemsg = self._create_message('JOIN', channels, source=message.source) await super().on_raw_join(fakemsg) if account == NO_ACCOUNT: account = None self.users[nick]['account'] = account self.users[nick]['realname'] = realname else: await super().on_raw_join(message) pydle_0.9.4.orig/pydle/features/ircv3/ircv3_2.py0000644000000000000000000000670613413743551017671 0ustar0000000000000000## ircv3_2.py # IRCv3.2 support (in progress). from . import ircv3_1 from . import tags from . import monitor from . import metadata __all__ = [ 'IRCv3_2Support' ] class IRCv3_2Support(metadata.MetadataSupport, monitor.MonitoringSupport, tags.TaggedMessageSupport, ircv3_1.IRCv3_1Support): """ Support for some of IRCv3.2's extensions. """ ## IRC callbacks. async def on_capability_account_tag_available(self, value): """ Add an account message tag to user messages. """ return True async def on_capability_cap_notify_available(self, value): """ Take note of new or removed capabilities. """ return True 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 def on_capability_echo_message_available(self, value): """ Echo PRIVMSG and NOTICEs back to client. """ return True async def on_capability_invite_notify_available(self, value): """ Broadcast invite messages to certain other clients. """ return True 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 def on_capability_uhnames_available(self, value): """ Possibly outdated alias for userhost-in-names. """ return await self.on_capability_userhost_in_names_available(value) async def on_isupport_uhnames(self, value): """ Let the server know that we support UHNAMES using the old ISUPPORT method, for legacy support. """ await self.rawmsg('PROTOCTL', 'UHNAMES') ## API overrides. async def message(self, target, message): await super().message(target, message) if not self._capabilities.get('echo-message'): await self.on_message(target, self.nickname, message) if self.is_channel(target): await self.on_channel_message(target, self.nickname, message) else: await self.on_private_message(target, self.nickname, message) async def notice(self, target, message): await super().notice(target, message) if not self._capabilities.get('echo-message'): await self.on_notice(target, self.nickname, message) if self.is_channel(target): await self.on_channel_notice(target, self.nickname, message) else: await self.on_private_notice(target, self.nickname, message) ## Message handlers. async def on_raw(self, message): if 'account' in message.tags: nick, _ = self._parse_user(message.source) if nick in self.users: metadata = { 'identified': True, 'account': message.tags['account'] } self._sync_user(nick, metadata) await super().on_raw(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 nick, _ = self._parse_user(message.source) if nick not in self.users: return # Update user and host. metadata = { 'username': message.params[0], 'hostname': message.params[1] } self._sync_user(nick, metadata) pydle_0.9.4.orig/pydle/features/ircv3/ircv3_3.py0000644000000000000000000000057313407746146017674 0ustar0000000000000000## ircv3_3.py # IRCv3.3 support (in progress). from . import ircv3_2 __all__ = [ 'IRCv3_3Support' ] class IRCv3_3Support(ircv3_2.IRCv3_2Support): """ Support for some of IRCv3.3's extensions. """ ## IRC callbacks. async def on_capability_message_tags_available(self, value): """ Indicate that we can in fact parse arbitrary tags. """ return True pydle_0.9.4.orig/pydle/features/ircv3/metadata.py0000644000000000000000000001010313407746146020172 0ustar0000000000000000from . import cap VISIBLITY_ALL = '*' class MetadataSupport(cap.CapabilityNegotiationSupport): ## Internals. def _reset_attributes(self): super()._reset_attributes() self._pending['metadata'] = {} self._metadata_info = {} self._metadata_queue = [] ## IRC API. 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 = await self.get_metadata('#foo') """ if target not in self._pending['metadata']: await self.rawmsg('METADATA', target, 'LIST') self._metadata_queue.append(target) self._metadata_info[target] = {} self._pending['metadata'][target] = self.eventloop.create_future() return self._pending['metadata'][target] async def set_metadata(self, target, key, value): await self.rawmsg('METADATA', target, 'SET', key, value) async def unset_metadata(self, target, key): await self.rawmsg('METADATA', target, 'SET', key) async def clear_metadata(self, target): await self.rawmsg('METADATA', target, 'CLEAR') ## Callbacks. async def on_metadata(self, target, key, value, visibility=None): pass ## Message handlers. async def on_capability_metadata_notify_available(self, value): return True async def on_raw_metadata(self, message): """ Metadata event. """ target, targetmeta = self._parse_user(message.params[0]) key, visibility, value = message.params[1:4] if visibility == VISIBLITY_ALL: visibility = None if target in self.users: self._sync_user(target, targetmeta) await self.on_metadata(target, key, value, visibility=visibility) 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] if target not in self._pending['whois']: return if target in self.users: self._sync_user(target, targetmeta) self._whois_info[target].setdefault('metadata', {}) self._whois_info[target]['metadata'][key] = value async def on_raw_761(self, message): """ Metadata key/value. """ target, targetmeta = self._parse_user(message.params[0]) key, visibility = message.params[1:3] value = message.params[3] if len(message.params) > 3 else None if target not in self._pending['metadata']: return if target in self.users: self._sync_user(target, targetmeta) self._metadata_info[target][key] = value 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. if not self._metadata_queue: return nickname = self._metadata_queue.pop() future = self._pending['metadata'].pop(nickname) future.set_result(self._metadata_info.pop(nickname)) async def on_raw_764(self, message): """ Metadata limit reached. """ pass async def on_raw_765(self, message): """ Invalid metadata target. """ target, targetmeta = self._parse_user(message.params[0]) if target not in self._pending['metadata']: return if target in self.users: self._sync_user(target, targetmeta) self._metadata_queue.remove(target) del self._metadata_info[target] future = self._pending['metadata'].pop(target) future.set_result(None) async def on_raw_766(self, message): """ Unknown metadata key. """ pass async def on_raw_767(self, message): """ Invalid metadata key. """ pass async def on_raw_768(self, message): """ Metadata key not set. """ pass async def on_raw_769(self, message): """ Metadata permission denied. """ pass pydle_0.9.4.orig/pydle/features/ircv3/monitor.py0000644000000000000000000000707113407746146020113 0ustar0000000000000000## monitor.py # Online status monitoring support. from . import cap class MonitoringSupport(cap.CapabilityNegotiationSupport): """ Support for monitoring the online/offline status of certain targets. """ ## Internals. def _reset_attributes(self): super()._reset_attributes() self._monitoring = set() def _destroy_user(self, nickname, channel=None, monitor_override=False): # Override _destroy_user to not remove user if they are being monitored by us. if channel: channels = [ self.channels[channel] ] else: channels = self.channels.values() for ch in channels: # Remove from nicklist. ch['users'].discard(nickname) # Remove from statuses. for status in self._nickname_prefixes.values(): if status in ch['modes'] and nickname in ch['modes'][status]: ch['modes'][status].remove(nickname) # If we're not in any common channels with the user anymore, we have no reliable way to keep their info up-to-date. # Remove the user. if (monitor_override or not self.is_monitoring(nickname)) and (not channel or not any(nickname in ch['users'] for ch in self.channels.values())): del self.users[nickname] ## API. def monitor(self, target): """ Start monitoring the online status of a user. Returns whether or not the server supports monitoring. """ if 'monitor-notify' in self._capabilities and not self.is_monitoring(target): yield from self.rawmsg('MONITOR', '+', target) self._monitoring.add(target) return True else: return False def unmonitor(self, target): """ Stop monitoring the online status of a user. Returns whether or not the server supports monitoring. """ if 'monitor-notify' in self._capabilities and self.is_monitoring(target): yield from self.rawmsg('MONITOR', '-', target) self._monitoring.remove(target) return True else: return False def is_monitoring(self, target): """ Return whether or not we are monitoring the target's online status. """ return target in self._monitoring ## Callbacks. async def on_user_online(self, nickname): """ Callback called when a monitored user appears online. """ pass async def on_user_offline(self, nickname): """ Callback called when a monitored users goes offline. """ pass ## Message handlers. async def on_capability_monitor_notify_available(self, value): return True 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) await self.on_user_online(nickname) 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) await self.on_user_offline(nickname) 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 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(',')) pydle_0.9.4.orig/pydle/features/ircv3/sasl.py0000644000000000000000000001530313407746146017363 0ustar0000000000000000## 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 . import cap __all__ = [ 'SASLSupport' ] RESPONSE_LIMIT = 400 EMPTY_MESSAGE = '+' ABORT_MESSAGE = '*' class SASLSupport(cap.CapabilityNegotiationSupport): """ SASL authentication support. Currently limited to the PLAIN mechanism. """ SASL_TIMEOUT = 10 ## Internal overrides. def __init__(self, *args, sasl_identity='', sasl_username=None, sasl_password=None, sasl_mechanism=None, **kwargs): super().__init__(*args, **kwargs) self.sasl_identity = sasl_identity self.sasl_username = sasl_username self.sasl_password = sasl_password self.sasl_mechanism = sasl_mechanism def _reset_attributes(self): super()._reset_attributes() self._sasl_client = None self._sasl_timer = None self._sasl_challenge = b'' self._sasl_mechanisms = None ## SASL functionality. async def _sasl_start(self, mechanism): """ Initiate SASL authentication. """ # The rest will be handled in on_raw_authenticate()/_sasl_respond(). 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 def _sasl_abort(self, timeout=False): """ Abort SASL authentication. """ if timeout: self.logger.error('SASL authentication timed out: aborting.') else: self.logger.error('SASL authentication aborted.') if self._sasl_timer: self._sasl_timer.cancel() self._sasl_timer = None # We're done here. await self.rawmsg('AUTHENTICATE', ABORT_MESSAGE) await self._capability_negotiated('sasl') async def _sasl_end(self): """ Finalize SASL authentication. """ if self._sasl_timer: self._sasl_timer.cancel() self._sasl_timer = None await self._capability_negotiated('sasl') async def _sasl_respond(self): """ Respond to SASL challenge with response. """ # Formulate a response. if self._sasl_client: try: response = self._sasl_client.process(self._sasl_challenge) except puresasl.SASLError: response = None if response is None: self.logger.warning('SASL challenge processing failed: aborting SASL authentication.') await self._sasl_abort() else: response = b'' response = base64.b64encode(response).decode(self.encoding) to_send = len(response) self._sasl_challenge = b'' # Send response in chunks. while to_send > 0: 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: await self.rawmsg('AUTHENTICATE', EMPTY_MESSAGE) ## Capability callbacks. async def on_capability_sasl_available(self, value): """ Check whether or not SASL is available. """ if value: self._sasl_mechanisms = value.upper().split(',') else: self._sasl_mechanisms = None if self.sasl_mechanism == 'EXTERNAL' or (self.sasl_username and self.sasl_password): if self.sasl_mechanism == 'EXTERNAL' or puresasl: return True self.logger.warning('SASL credentials set but puresasl module not found: not initiating SASL authentication.') return False 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: self.logger.warning('Requested SASL mechanism is not in server mechanism list: aborting SASL authentication.') return cap.failed mechanisms = [self.sasl_mechanism] else: mechanisms = self._sasl_mechanisms or ['PLAIN'] if mechanisms == ['EXTERNAL']: mechanism = 'EXTERNAL' else: self._sasl_client = puresasl.client.SASLClient(self.connection.hostname, 'irc', username=self.sasl_username, password=self.sasl_password, identity=self.sasl_identity ) try: self._sasl_client.choose_mechanism(mechanisms, allow_anonymous=False) except puresasl.SASLError: self.logger.exception('SASL mechanism choice failed: aborting SASL authentication.') return cap.FAILED mechanism = self._sasl_client.mechanism.upper() # Initialize SASL. 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 def on_raw_authenticate(self, message): """ Received part of the authentication challenge. """ # Cancel timeout timer. if self._sasl_timer: self._sasl_timer.cancel() self._sasl_timer = None # Add response data. response = ' '.join(message.params) if response != EMPTY_MESSAGE: self._sasl_challenge += base64.b64decode(response) # If the response ain't exactly SASL_RESPONSE_LIMIT bytes long, it's the end. Process. if len(response) % RESPONSE_LIMIT > 0: await self._sasl_respond() else: # Response not done yet. Restart timer. 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 def on_raw_903(self, message): """ SASL authentication successful. """ await self._sasl_end() async def on_raw_904(self, message): """ Invalid mechanism or authentication failed. Abort SASL. """ await self._sasl_abort() async def on_raw_905(self, message): """ Authentication failed. Abort SASL. """ 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. pydle_0.9.4.orig/pydle/features/ircv3/tags.py0000644000000000000000000001014513702220363017340 0ustar0000000000000000## tags.py # Tagged message support. import pydle.client import pydle.protocol from pydle.features import rfc1459 import re TAG_INDICATOR = '@' TAG_SEPARATOR = ';' TAG_VALUE_SEPARATOR = '=' TAGGED_MESSAGE_LENGTH_LIMIT = 1024 TAG_CONVERSIONS = { r"\:": ';', r"\s": ' ', r"\\": '\\', r"\r": '\r', r"\n": '\n' } class TaggedMessage(rfc1459.RFC1459Message): def __init__(self, tags=None, **kw): super().__init__(**kw) self._kw['tags'] = tags self.__dict__.update(self._kw) @classmethod def parse(cls, line, encoding=pydle.protocol.DEFAULT_ENCODING): """ Parse given line into IRC message structure. Returns a TaggedMessage. """ valid = True # Decode message. try: message = line.decode(encoding) except UnicodeDecodeError: # Try our fallback encoding. message = line.decode(pydle.protocol.FALLBACK_ENCODING) # Sanity check for message length. if len(message) > TAGGED_MESSAGE_LENGTH_LIMIT: valid = False # Strip message separator. if message.endswith(rfc1459.protocol.LINE_SEPARATOR): message = message[:-len(rfc1459.protocol.LINE_SEPARATOR)] elif message.endswith(rfc1459.protocol.MINIMAL_LINE_SEPARATOR): message = message[:-len(rfc1459.protocol.MINIMAL_LINE_SEPARATOR)] raw = message # Parse tags. tags = {} if message.startswith(TAG_INDICATOR): message = message[len(TAG_INDICATOR):] raw_tags, message = message.split(' ', 1) for raw_tag in raw_tags.split(TAG_SEPARATOR): if TAG_VALUE_SEPARATOR in raw_tag: tag, value = raw_tag.split(TAG_VALUE_SEPARATOR, 1) else: tag = raw_tag value = True # Parse escape sequences since IRC escapes != python escapes # convert known escapes first for escape, replacement in TAG_CONVERSIONS.items(): value = value.replace(escape, replacement) # convert other escape sequences based on the spec pattern =re.compile(r"(\\[\s\S])+") for match in pattern.finditer(value): escape = match.group() value = value.replace(escape, escape[1]) # Finally: add constructed tag to the output object. tags[tag] = value # Parse rest of message. message = super().parse(message.lstrip().encode(encoding), encoding=encoding) return TaggedMessage(_raw=raw, _valid=message._valid and valid, tags=tags, **message._kw) def construct(self, force=False): """ Construct raw IRC message and return it. """ message = super().construct(force=force) # Add tags. if self.tags: raw_tags = [] for tag, value in self.tags.items(): if value == True: raw_tags.append(tag) else: raw_tags.append(tag + TAG_VALUE_SEPARATOR + value) message = TAG_INDICATOR + TAG_SEPARATOR.join(raw_tags) + ' ' + message if len(message) > TAGGED_MESSAGE_LENGTH_LIMIT and not force: raise protocol.ProtocolViolation( 'The constructed message is too long. ({len} > {maxlen})'.format(len=len(message), maxlen=TAGGED_MESSAGE_LENGTH_LIMIT), message=message) return message class TaggedMessageSupport(rfc1459.RFC1459Support): def _create_message(self, command, *params, tags={}, **kwargs): message = super()._create_message(command, *params, **kwargs) return TaggedMessage(tags=tags, **message._kw) def _parse_message(self): sep = rfc1459.protocol.MINIMAL_LINE_SEPARATOR.encode(self.encoding) message, _, data = self._receive_buffer.partition(sep) self._receive_buffer = data return TaggedMessage.parse(message + sep, encoding=self.encoding) pydle_0.9.4.orig/pydle/features/rfc1459/__init__.py0000644000000000000000000000016012276030007020203 0ustar0000000000000000from . import client, parsing, protocol from .client import RFC1459Support from .parsing import RFC1459Message pydle_0.9.4.orig/pydle/features/rfc1459/client.py0000644000000000000000000011044713702220363017735 0ustar0000000000000000## rfc1459.py # Basic RFC1459 stuff. import copy import datetime import ipaddress import itertools from pydle.client import BasicClient, NotInChannel, AlreadyInChannel from . import parsing, protocol class RFC1459Support(BasicClient): """ Basic RFC1459 client. """ DEFAULT_QUIT_MESSAGE = 'Quitting' ## Internals. def _reset_attributes(self): super()._reset_attributes() # Casemapping. self._case_mapping = protocol.DEFAULT_CASE_MAPPING # Limitations. self._away_message_length_limit = None self._channel_length_limit = protocol.CHANNEL_LENGTH_LIMIT self._channel_limit_groups = copy.deepcopy(protocol.CHANNEL_LIMITS_GROUPS) self._channel_limits = copy.deepcopy(protocol.CHANNEL_LIMITS) self._command_parameter_limit = protocol.PARAMETER_LIMIT self._list_limit_groups = copy.deepcopy(protocol.LIST_LIMITS_GROUPS) self._list_limits = copy.deepcopy(protocol.LIST_LIMITS) self._mode_limit = None self._nickname_length_limit = protocol.NICKNAME_LENGTH_LIMIT self._target_limits = {} self._topic_length_limit = protocol.TOPIC_LENGTH_LIMIT # Modes, prefixes. self._mode = {} self._channel_modes = set(protocol.CHANNEL_MODES) self._channel_modes_behaviour = copy.deepcopy(protocol.CHANNEL_MODES_BEHAVIOUR) self._channel_prefixes = set(protocol.CHANNEL_PREFIXES) self._nickname_prefixes = protocol.NICKNAME_PREFIXES.copy() self._status_message_prefixes = set() self._user_modes = set(protocol.USER_MODES) self._user_modes_behaviour = copy.deepcopy(protocol.USER_MODES_BEHAVIOUR) # Registration. self.registered = False self._registration_attempts = 0 self._attempt_nicknames = self._nicknames[:] # Info. self._pending['whois'] = parsing.NormalizingDict(case_mapping=self._case_mapping) self._pending['whowas'] = parsing.NormalizingDict(case_mapping=self._case_mapping) self._whois_info = parsing.NormalizingDict(case_mapping=self._case_mapping) self._whowas_info = parsing.NormalizingDict(case_mapping=self._case_mapping) # Misc. self.motd = None self.channels = parsing.NormalizingDict(self.channels, case_mapping=self._case_mapping) self.users = parsing.NormalizingDict(self.users, case_mapping=self._case_mapping) def _reset_connection_attributes(self): super()._reset_connection_attributes() self.password = None def _create_channel(self, channel): super()._create_channel(channel) self.channels[channel].update({ 'modes': {}, 'topic': None, 'topic_by': None, 'topic_set': None, 'created': None, 'password': None, 'banlist': None, 'public': True }) def _create_user(self, nickname): super()._create_user(nickname) if nickname in self.users: self.users[nickname].update({ 'away': False, 'away_message': None, }) def _rename_user(self, user, new): super()._rename_user(user, new) # Rename in mode lists, too. for ch in self.channels.values(): for status in self._nickname_prefixes.values(): if status in ch['modes'] and user in ch['modes'][status]: ch['modes'][status].remove(user) ch['modes'][status].append(new) def _destroy_user(self, user, channel=None): if channel: channels = [self.channels[channel]] else: channels = self.channels.values() # Remove user from status list too. for ch in channels: for status in self._nickname_prefixes.values(): if status in ch['modes'] and user in ch['modes'][status]: ch['modes'][status].remove(user) def _parse_user(self, data): if data: nickname, username, host = parsing.parse_user(data) metadata = {} metadata['nickname'] = nickname if username: metadata['username'] = username if host: metadata['hostname'] = host else: return None, {} return nickname, metadata def _parse_user_modes(self, user, modes, current=None): if current is None: current = self.users[user]['modes'] return parsing.parse_modes(modes, current, behaviour=self._user_modes_behaviour) def _parse_channel_modes(self, channel, modes, current=None): if current is None: current = self.channels[channel]['modes'] return parsing.parse_modes(modes, current, behaviour=self._channel_modes_behaviour) def _format_host_range(self, host, range, allow_everything=False): # IPv4? try: addr = ipaddress.IPv4Network(host, strict=False) max = 4 if allow_everything else 3 # Round up subnet to nearest octet. subnet = addr.prefixlen + (8 - addr.prefixlen % 8) # Remove range mask. subnet -= min(range, max) * 8 rangeaddr = addr.supernet(new_prefix=subnet).exploded.split('/', 1)[0] return rangeaddr.replace('0', '*') except ValueError: pass # IPv6? try: addr = ipaddress.IPv6Network(host, strict=False) max = 4 if allow_everything else 3 # Round up subnet to nearest 32-et. subnet = addr.prefixlen + (32 - addr.prefixlen % 32) # Remove range mask. subnet -= min(range, max) * 32 rangeaddr = addr.supernet(new_prefix=subnet).exploded.split('/', 1)[0] return rangeaddr.replace(':0000', ':*') except ValueError: pass # Host? if '.' in host: # Split pieces. pieces = host.split('.') max = len(pieces) if not allow_everything: max -= 1 # Figure out how many to mask. to_mask = min(range, max) # Mask pieces. pieces[:to_mask] = '*' * to_mask return '.'.join(pieces) # Wat. if allow_everything and range >= 4: return '*' else: return host ## Connection. async def connect(self, hostname=None, port=None, password=None, **kwargs): port = port or protocol.DEFAULT_PORT # Connect... await super().connect(hostname, port, **kwargs) # Check if a password was provided and we don't already have one if password is not None and not self.password: # if so, set the password. self.password = password # And initiate the IRC connection. await self._register() async def _register(self): """ Perform IRC connection registration. """ if self.registered: return self._registration_attempts += 1 # Don't throttle during registration, most ircds don't care for flooding during registration, # and it might speed it up significantly. self.connection.throttle = False # Password first. if self.password: await self.rawmsg('PASS', self.password) # Then nickname... await self.set_nickname(self._attempt_nicknames.pop(0)) # And now for the rest of the user information. await self.rawmsg('USER', self.username, '0', '*', self.realname) 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. self.registered = True self.connection.throttle = True target = message.params[0] fakemsg = self._create_message('NICK', target, source=self.nickname) await self.on_raw_nick(fakemsg) ## Message handling. def _has_message(self): """ Whether or not we have messages available for processing. """ sep = protocol.MINIMAL_LINE_SEPARATOR.encode(self.encoding) return sep in self._receive_buffer def _create_message(self, command, *params, **kwargs): return parsing.RFC1459Message(command, params, **kwargs) def _parse_message(self): sep = protocol.MINIMAL_LINE_SEPARATOR.encode(self.encoding) message, _, data = self._receive_buffer.partition(sep) self._receive_buffer = data return parsing.RFC1459Message.parse(message + sep, encoding=self.encoding) ## IRC API. 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. """ await self.rawmsg('NICK', nickname) async def join(self, channel, password=None): """ Join channel, optionally with password. """ if self.in_channel(channel): raise AlreadyInChannel(channel) if password: await self.rawmsg('JOIN', channel, password) else: await self.rawmsg('JOIN', channel) 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: await self.rawmsg('PART', channel, message) else: await self.rawmsg('PART', channel) async def kick(self, channel, target, reason=None): """ Kick user from channel. """ if not self.in_channel(channel): raise NotInChannel(channel) if reason: await self.rawmsg('KICK', channel, target, reason) else: await self.rawmsg('KICK', channel, target) 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. range indicates the IP/host range to ban: 0 means ban only the IP/host, 1+ means ban that many 'degrees' (up to 3 for IP addresses) of the host for range bans. """ if target in self.users: host = self.users[target]['hostname'] else: host = target host = self._format_host_range(host, range) mask = self._format_host_mask('*', '*', host) await self.rawmsg('MODE', channel, '+b', mask) 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. """ if target in self.users: host = self.users[target]['hostname'] else: host = target host = self._format_host_range(host, range) mask = self._format_host_mask('*', '*', host) await self.rawmsg('MODE', channel, '-b', mask) async def kickban(self, channel, target, reason=None, range=0): """ Kick and ban user from channel. """ await self.ban(channel, target, range) await self.kick(channel, target, reason) async def quit(self, message=None): """ Quit network. """ if message is None: message = self.DEFAULT_QUIT_MESSAGE await self.rawmsg('QUIT', message) await self.disconnect(expected=True) async def cycle(self, channel): """ Rejoin channel. """ if not self.in_channel(channel): raise NotInChannel(channel) password = self.channels[channel]['password'] await self.part(channel) await self.join(channel, password) 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 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. await self.rawmsg('PRIVMSG', target, chunk or ' ') 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 for line in message.replace('\r', '').split('\n'): for chunk in chunkify(line, chunklen): await self.rawmsg('NOTICE', target, chunk) 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. """ if self.is_channel(target) and not self.in_channel(target): raise NotInChannel(target) await self.rawmsg('MODE', target, *modes) 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. """ if not self.is_channel(channel): raise ValueError('Not a channel: {}'.format(channel)) elif not self.in_channel(channel): raise NotInChannel(channel) await self.rawmsg('TOPIC', channel, topic) async def away(self, message): """ Mark self as away. """ await self.rawmsg('AWAY', message) async def back(self): """ Mark self as not away. """ await self.rawmsg('AWAY') 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 = 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, # then we immediately set the future's result to None and don't bother checking. if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None: result = self.eventloop.create_future() result.set_result(None) return result if nickname not in self._pending['whois']: await self.rawmsg('WHOIS', nickname) self._whois_info[nickname] = { 'oper': False, 'idle': 0, 'away': False, 'away_message': None } # Create a future for when the WHOIS requests succeeds. self._pending['whois'][nickname] = self.eventloop.create_future() return await self._pending['whois'][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 = await self.whowas('Nick') """ # Same treatment as nicknames in whois. if protocol.ARGUMENT_SEPARATOR.search(nickname) is not None: result = self.eventloop.create_future() result.set_result(None) return result if nickname not in self._pending['whowas']: 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 await self._pending['whowas'][nickname] ## IRC helpers. def normalize(self, input): return parsing.normalize(input, case_mapping=self._case_mapping) def is_channel(self, chan): return any(chan.startswith(prefix) for prefix in self._channel_prefixes) def is_same_nick(self, left, right): """ Check if given nicknames are equal in the server's case mapping. """ return self.normalize(left) == self.normalize(right) def is_same_channel(self, left, right): """ Check if given nicknames are equal in the server's case mapping. """ return self.normalize(left) == self.normalize(right) ## Overloadable callbacks. async def on_connect(self): # Auto-join channels. for channel in self._autojoin_channels: await self.join(channel) # super call await super().on_connect() async def on_invite(self, channel, by): """ Callback called when the client was invited into a channel by someone. """ pass async def on_user_invite(self, target, channel, by): """ Callback called when another user was invited into a channel by someone. """ pass async def on_join(self, channel, user): """ Callback called when a user, possibly the client, has joined the channel. """ pass async def on_kill(self, target, by, reason): """ Callback called when a user, possibly the client, was killed from the server. """ pass 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 def on_mode_change(self, channel, modes, by): """ Callback called when the mode on a channel was changed. """ pass async def on_user_mode_change(self, modes): """ Callback called when a user mode change occurred for the client. """ pass async def on_message(self, target, by, message): """ Callback called when the client received a message. """ pass async def on_channel_message(self, target, by, message): """ Callback received when the client received a message in a channel. """ pass async def on_private_message(self, target, by, message): """ Callback called when the client received a message in private. """ pass async def on_nick_change(self, old, new): """ Callback called when a user, possibly the client, changed their nickname. """ pass async def on_notice(self, target, by, message): """ Callback called when the client received a notice. """ pass async def on_channel_notice(self, target, by, message): """ Callback called when the client received a notice in a channel. """ pass async def on_private_notice(self, target, by, message): """ Callback called when the client received a notice in private. """ pass async def on_part(self, channel, user, message=None): """ Callback called when a user, possibly the client, left a channel. """ pass async def on_topic_change(self, channel, message, by): """ Callback called when the topic for a channel was changed. """ pass async def on_quit(self, user, message=None): """ Callback called when a user, possibly the client, left the network. """ pass ## Callback handlers. async def on_raw_error(self, message): """ Server encountered an error and will now close the connection. """ error = protocol.ServerError(' '.join(message.params)) await self.on_data_error(error) async def on_raw_pong(self, message): self.logger.debug('>> PONG received') async def on_raw_invite(self, message): """ INVITE command. """ nick, metadata = self._parse_user(message.source) self._sync_user(nick, metadata) target, channel = message.params target, metadata = self._parse_user(target) if self.is_same_nick(self.nickname, target): await self.on_invite(channel, nick) else: await self.on_user_invite(target, channel, nick) async def on_raw_join(self, message): """ JOIN command. """ nick, metadata = self._parse_user(message.source) self._sync_user(nick, metadata) channels = message.params[0].split(',') if self.is_same_nick(self.nickname, nick): # Add to our channel list, we joined here. for channel in channels: if not self.in_channel(channel): self._create_channel(channel) # Request channel mode from IRCd. await self.rawmsg('MODE', channel) else: # Add user to channel user list. for channel in channels: if self.in_channel(channel): self.channels[channel]['users'].add(nick) for channel in channels: await self.on_join(channel, nick) async def on_raw_kick(self, message): """ KICK command. """ kicker, kickermeta = self._parse_user(message.source) self._sync_user(kicker, kickermeta) if len(message.params) > 2: channels, targets, reason = message.params else: channels, targets = message.params reason = None channels = channels.split(',') targets = targets.split(',') for channel, target in itertools.product(channels, targets): target, targetmeta = self._parse_user(target) self._sync_user(target, targetmeta) if self.is_same_nick(target, self.nickname): self._destroy_channel(channel) else: # Update nick list on channel. if self.in_channel(channel): self._destroy_user(target, channel) await self.on_kick(channel, target, kicker, reason) async def on_raw_kill(self, message): """ KILL command. """ by, bymeta = self._parse_user(message.source) target, targetmeta = self._parse_user(message.params[0]) reason = message.params[1] self._sync_user(target, targetmeta) if by in self.users: self._sync_user(by, bymeta) await self.on_kill(target, by, reason) if self.is_same_nick(self.nickname, target): await self.disconnect(expected=False) else: self._destroy_user(target) async def on_raw_mode(self, message): """ MODE command. """ nick, metadata = self._parse_user(message.source) target, modes = message.params[0], message.params[1:] self._sync_user(nick, metadata) if self.is_channel(target): if self.in_channel(target): # Parse modes. self.channels[target]['modes'] = self._parse_channel_modes(target, modes) await self.on_mode_change(target, modes, nick) else: target, targetmeta = self._parse_user(target) self._sync_user(target, targetmeta) # Update own modes. if self.is_same_nick(self.nickname, nick): self._mode = self._parse_user_modes(nick, modes, current=self._mode) await self.on_user_mode_change(modes) async def on_raw_nick(self, message): """ NICK command. """ nick, metadata = self._parse_user(message.source) new = message.params[0] self._sync_user(nick, metadata) # Acknowledgement of nickname change: set it internally, too. # Alternatively, we were force nick-changed. Nothing much we can do about it. if self.is_same_nick(self.nickname, nick): self.nickname = new # Go through all user lists and replace. self._rename_user(nick, new) # Call handler. await self.on_nick_change(nick, new) 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) await self.on_notice(target, nick, message) if self.is_channel(target): await self.on_channel_notice(target, nick, message) else: await self.on_private_notice(target, nick, message) async def on_raw_part(self, message): """ PART command. """ nick, metadata = self._parse_user(message.source) channels = message.params[0].split(',') if len(message.params) > 1: reason = message.params[1] else: reason = None self._sync_user(nick, metadata) if self.is_same_nick(self.nickname, nick): # We left the channel. Remove from channel list. :( for channel in channels: if self.in_channel(channel): await self.on_part(channel, nick, reason) self._destroy_channel(channel) else: # Someone else left. Remove them. for channel in channels: await self.on_part(channel, nick, reason) self._destroy_user(nick, channel) async def on_raw_ping(self, message): """ PING command. """ # Respond with a pong. await self.rawmsg('PONG', *message.params) 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) await self.on_message(target, nick, message) if self.is_channel(target): await self.on_channel_message(target, nick, message) else: await self.on_private_message(target, nick, message) async def on_raw_quit(self, message): """ QUIT command. """ nick, metadata = self._parse_user(message.source) self._sync_user(nick, metadata) if message.params: reason = message.params[0] else: reason = None await self.on_quit(nick, reason) # Remove user from database. if not self.is_same_nick(self.nickname, nick): self._destroy_user(nick) # Else, we quit. elif self.connected: await self.disconnect(expected=True) async def on_raw_topic(self, message): """ TOPIC command. """ setter, settermeta = self._parse_user(message.source) target, topic = message.params self._sync_user(setter, settermeta) # Update topic in our own channel list. if self.in_channel(target): self.channels[target]['topic'] = topic self.channels[target]['topic_by'] = setter self.channels[target]['topic_set'] = datetime.datetime.now() 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. async def on_raw_004(self, message): """ Basic server information. """ target, hostname, ircd, user_modes, channel_modes = message.params[:5] # Set valid channel and user modes. 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. async def on_raw_301(self, message): """ User is away. """ target, nickname, message = message.params info = { 'away': True, 'away_message': message } if nickname in self.users: self._sync_user(nickname, info) if nickname in self._pending['whois']: self._whois_info[nickname].update(info) async def on_raw_311(self, message): """ WHOIS user info. """ target, nickname, username, hostname, _, realname = message.params info = { 'username': username, 'hostname': hostname, 'realname': realname } self._sync_user(nickname, info) if nickname in self._pending['whois']: self._whois_info[nickname].update(info) async def on_raw_312(self, message): """ WHOIS server info. """ target, nickname, server, serverinfo = message.params info = { 'server': server, 'server_info': serverinfo } if nickname in self._pending['whois']: self._whois_info[nickname].update(info) if nickname in self._pending['whowas']: self._whowas_info[nickname].update(info) async def on_raw_313(self, message): """ WHOIS operator info. """ target, nickname = message.params[:2] info = { 'oper': True } if nickname in self._pending['whois']: self._whois_info[nickname].update(info) async def on_raw_314(self, message): """ WHOWAS user info. """ target, nickname, username, hostname, _, realname = message.params info = { 'username': username, 'hostname': hostname, 'realname': realname } if nickname in self._pending['whowas']: self._whowas_info[nickname].update(info) on_raw_315 = BasicClient._ignored # End of /WHO list. async def on_raw_317(self, message): """ WHOIS idle time. """ target, nickname, idle_time = message.params[:3] info = { 'idle': int(idle_time), } if nickname in self._pending['whois']: self._whois_info[nickname].update(info) async def on_raw_318(self, message): """ End of /WHOIS list. """ 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 def on_raw_319(self, message): """ WHOIS active channels. """ target, nickname, channels = message.params[:3] channels = {channel.lstrip() for channel in channels.strip().split(' ')} info = { 'channels': channels } if nickname in self._pending['whois']: self._whois_info[nickname].update(info) async def on_raw_324(self, message): """ Channel mode. """ target, channel = message.params[:2] modes = message.params[2:] if not self.in_channel(channel): return self.channels[channel]['modes'] = self._parse_channel_modes(channel, modes) async def on_raw_329(self, message): """ Channel creation time. """ target, channel, timestamp = message.params if not self.in_channel(channel): return self.channels[channel]['created'] = datetime.datetime.fromtimestamp(int(timestamp)) async def on_raw_332(self, message): """ Current topic on channel join. """ target, channel, topic = message.params if not self.in_channel(channel): return self.channels[channel]['topic'] = topic 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): return # No need to sync user since this is most likely outdated info. self.channels[channel]['topic_by'] = self._parse_user(setter)[0] self.channels[channel]['topic_set'] = datetime.datetime.fromtimestamp(int(timestamp)) async def on_raw_353(self, message): """ Response to /NAMES. """ target, visibility, channel, names = message.params if not self.in_channel(channel): return # Set channel visibility. if visibility == protocol.PUBLIC_CHANNEL_SIGIL: self.channels[channel]['public'] = True elif visibility in (protocol.PRIVATE_CHANNEL_SIGIL, protocol.SECRET_CHANNEL_SIGIL): self.channels[channel]['public'] = False # Update channel user list. for entry in names.split(' '): statuses = [] # Make entry safe for _parse_user(). safe_entry = entry.lstrip(''.join(self._nickname_prefixes.keys())) # Parse entry and update database. nick, metadata = self._parse_user(safe_entry) if not nick: # nonsense nickname continue self._sync_user(nick, metadata) # Get prefixes. prefixes = set(entry.replace(safe_entry, '')) # Check, record and strip status prefixes. for prefix, status in self._nickname_prefixes.items(): # Add to list of statuses by user. if prefix in prefixes: statuses.append(status) # Add user to user list. self.channels[channel]['users'].add(nick) # And to channel modes.. for status in statuses: if status not in self.channels[channel]['modes']: self.channels[channel]['modes'][status] = [] self.channels[channel]['modes'][status].append(nick) on_raw_366 = BasicClient._ignored # End of /NAMES list. async def on_raw_375(self, message): """ Start message of the day. """ await self._registration_completed(message) self.motd = message.params[1] + '\n' async def on_raw_372(self, message): """ Append message of the day. """ self.motd += message.params[1] + '\n' 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. await self.on_connect() async def on_raw_401(self, message): """ No such nick/channel. """ nickname = message.params[1] # Remove nickname from whois requests if it involves one of ours. if nickname in self._pending['whois']: future = self._pending['whois'].pop(nickname) future.set_result(None) del self._whois_info[nickname] async def on_raw_402(self, message): """ No such server. """ return await self.on_raw_401(message) async def on_raw_422(self, message): """ MOTD is missing. """ await self._registration_completed(message) self.motd = None await self.on_connect() 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 def on_raw_432(self, message): """ Erroneous nickname. """ if not self.registered: # Nothing else we can do than try our next nickname. await self.on_raw_433(message) async def on_raw_433(self, message): """ Nickname in use. """ if not self.registered: self._registration_attempts += 1 # Attempt to set new nickname. if self._attempt_nicknames: await self.set_nickname(self._attempt_nicknames.pop(0)) else: await 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. 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. ## Helpers. def chunkify(message, chunksize): if not message: yield message else: while message: chunk = message[:chunksize] message = message[chunksize:] yield chunk pydle_0.9.4.orig/pydle/features/rfc1459/parsing.py0000644000000000000000000002346013413743551020130 0ustar0000000000000000## parsing.py # RFC1459 parsing and construction. import collections.abc import pydle.protocol from . import protocol class RFC1459Message(pydle.protocol.Message): def __init__(self, command, params, source=None, _raw=None, _valid=True, **kw): self._kw = kw self._kw['command'] = command self._kw['params'] = params self._kw['source'] = source self._valid = _valid self._raw = _raw self.__dict__.update(self._kw) @classmethod def parse(cls, line, encoding=pydle.protocol.DEFAULT_ENCODING): """ Parse given line into IRC message structure. Returns a Message. """ valid = True # Decode message. try: message = line.decode(encoding) except UnicodeDecodeError: # Try our fallback encoding. message = line.decode(pydle.protocol.FALLBACK_ENCODING) # Sanity check for message length. if len(message) > protocol.MESSAGE_LENGTH_LIMIT: valid = False # Strip message separator. if message.endswith(protocol.LINE_SEPARATOR): message = message[:-len(protocol.LINE_SEPARATOR)] elif message.endswith(protocol.MINIMAL_LINE_SEPARATOR): message = message[:-len(protocol.MINIMAL_LINE_SEPARATOR)] # Sanity check for forbidden characters. if any(ch in message for ch in protocol.FORBIDDEN_CHARACTERS): valid = False # Extract message sections. # Format: (:source)? command parameter* if message.startswith(':'): parts = protocol.ARGUMENT_SEPARATOR.split(message[1:], 2) else: parts = [ None ] + protocol.ARGUMENT_SEPARATOR.split(message, 1) if len(parts) == 3: source, command, raw_params = parts elif len(parts) == 2: source, command = parts raw_params = '' else: raise pydle.protocol.ProtocolViolation('Improper IRC message format: not enough elements.', message=message) # Sanity check for command. if not protocol.COMMAND_PATTERN.match(command): valid = False # Extract parameters properly. # Format: (word|:sentence)* # Only parameter is a 'trailing' sentence. if raw_params.startswith(protocol.TRAILING_PREFIX): params = [ raw_params[len(protocol.TRAILING_PREFIX):] ] # We have a sentence in our parameters. elif ' ' + protocol.TRAILING_PREFIX in raw_params: index = raw_params.find(' ' + protocol.TRAILING_PREFIX) # Get all single-word parameters. params = protocol.ARGUMENT_SEPARATOR.split(raw_params[:index].rstrip(' ')) # Extract last parameter as sentence params.append(raw_params[index + len(protocol.TRAILING_PREFIX) + 1:]) # We have some parameters, but no sentences. elif raw_params: params = protocol.ARGUMENT_SEPARATOR.split(raw_params) # No parameters. else: params = [] # Commands can be either [a-zA-Z]+ or [0-9]+. # In the former case, force it to uppercase. # In the latter case (a numeric command), try to represent it as such. try: command = int(command) except ValueError: command = command.upper() # Return parsed message. return RFC1459Message(command, params, source=source, _valid=valid, _raw=message) def construct(self, force=False): """ Construct a raw IRC message. """ # Sanity check for command. command = str(self.command) if not protocol.COMMAND_PATTERN.match(command) and not force: raise pydle.protocol.ProtocolViolation('The constructed command does not follow the command pattern ({pat})'.format(pat=protocol.COMMAND_PATTERN.pattern), message=command) message = command.upper() # Add parameters. if not self.params: message += ' ' for idx, param in enumerate(self.params): # Trailing parameter? if not param or ' ' in param or param[0] == ':': if idx + 1 < len(self.params) and not force: raise pydle.protocol.ProtocolViolation('Only the final parameter of an IRC message can be trailing and thus contain spaces, or start with a colon.', message=param) message += ' ' + protocol.TRAILING_PREFIX + param # Regular parameter. else: message += ' ' + param # Prepend source. if self.source: message = ':' + self.source + ' ' + message # Sanity check for characters. if any(ch in message for ch in protocol.FORBIDDEN_CHARACTERS) and not force: raise pydle.protocol.ProtocolViolation('The constructed message contains forbidden characters ({chs}).'.format(chs=', '.join(protocol.FORBIDDEN_CHARACTERS)), message=message) # Sanity check for length. message += protocol.LINE_SEPARATOR if len(message) > protocol.MESSAGE_LENGTH_LIMIT and not force: raise pydle.protocol.ProtocolViolation('The constructed message is too long. ({len} > {maxlen})'.format(len=len(message), maxlen=protocol.MESSAGE_LENGTH_LIMIT), message=message) return message def normalize(input, case_mapping=protocol.DEFAULT_CASE_MAPPING): """ Normalize input according to case mapping. """ if case_mapping not in protocol.CASE_MAPPINGS: raise pydle.protocol.ProtocolViolation('Unknown case mapping ({})'.format(case_mapping)) input = input.lower() if case_mapping in ('rfc1459', 'rfc1459-strict'): input = input.replace('{', '[').replace('}', ']').replace('|', '\\') if case_mapping == 'rfc1459': input = input.replace('~', '^') return input class NormalizingDict(collections.abc.MutableMapping): """ A dict that normalizes entries according to the given case mapping. """ def __init__(self, *args, case_mapping): self.storage = {} self.case_mapping = case_mapping self.update(dict(*args)) def __getitem__(self, key): if not isinstance(key, str): raise KeyError(key) return self.storage[normalize(key, case_mapping=self.case_mapping)] def __setitem__(self, key, value): if not isinstance(key, str): raise KeyError(key) self.storage[normalize(key, case_mapping=self.case_mapping)] = value def __delitem__(self, key): if not isinstance(key, str): raise KeyError(key) del self.storage[normalize(key, case_mapping=self.case_mapping)] def __iter__(self): return iter(self.storage) def __len__(self): return len(self.storage) def __repr__(self): return '{mod}.{cls}({dict}, case_mapping={cm})'.format( mod=__name__, cls=self.__class__.__name__, dict=self.storage, cm=self.case_mapping) # Parsing. def parse_user(raw): """ Parse nick(!user(@host)?)? structure. """ nick = raw user = None host = None # Attempt to extract host. if protocol.HOST_SEPARATOR in raw: raw, host = raw.split(protocol.HOST_SEPARATOR) # Attempt to extract user. if protocol.USER_SEPARATOR in raw: nick, user = raw.split(protocol.USER_SEPARATOR) return nick, user, host def parse_modes(modes, current, behaviour): """ Parse mode change string(s) and return updated dictionary. """ current = current.copy() modes = modes[:] # Iterate in a somewhat odd way over the list because we want to modify it during iteration. i = 0 while i < len(modes): piece = modes[i] add = True sigiled = False for mode in piece: # Set mode to addition or deletion of modes. if mode == '+': add = True sigiled = True continue if mode == '-': add = False sigiled = True continue # Find mode behaviour. for type, affected in behaviour.items(): if mode in affected: break else: # If we don't have a behaviour for this mode, assume it has no parameters... type = protocol.BEHAVIOUR_NO_PARAMETER # Don't parse modes that are meant for list retrieval. if type == protocol.BEHAVIOUR_LIST and not sigiled: continue # Do we require a parameter? if type in (protocol.BEHAVIOUR_PARAMETER, protocol.BEHAVIOUR_LIST) or (type == protocol.BEHAVIOUR_PARAMETER_ON_SET and add): # Do we _have_ a parameter? if i + 1 == len(modes): raise pydle.protocol.ProtocolViolation('Attempted to parse mode with parameter ({s}{mode}) but no parameters left in mode list.'.format( mode=mode, s='+' if add else '-'), ' '.join(modes)) param = modes.pop(i + 1) # Now update the actual mode dict with our new values. if type in (protocol.BEHAVIOUR_PARAMETER, protocol.BEHAVIOUR_LIST): # Add/remove parameter from list. if add: if mode not in current: current[mode] = [] current[mode].append(param) else: if mode in current and param in current[mode]: current[mode].remove(param) elif type == protocol.BEHAVIOUR_PARAMETER_ON_SET and add: # Simply set parameter. current[mode] = param else: # Simply add/remove option. if add: current[mode] = True else: if mode in current: del current[mode] i += 1 return current pydle_0.9.4.orig/pydle/features/rfc1459/protocol.py0000644000000000000000000000342212305546454020324 0ustar0000000000000000## protocol.py # RFC1459 protocol constants. import re import collections from pydle.client import Error class ServerError(Error): pass # While this *technically* is supposed to be 143, I've yet to see a server that actually uses those. DEFAULT_PORT = 6667 ## Limits. CHANNEL_LIMITS_GROUPS = { '#': frozenset('#&'), '&': frozenset('#&') } CHANNEL_LIMITS = { frozenset('#&'): 10 } LIST_LIMITS_GROUPS = { 'b': frozenset('b') } LIST_LIMITS = { frozenset('b'): None } PARAMETER_LIMIT = 15 MESSAGE_LENGTH_LIMIT = 512 CHANNEL_LENGTH_LIMIT = 200 NICKNAME_LENGTH_LIMIT = 8 TOPIC_LENGTH_LIMIT = 450 ## Defaults. BEHAVIOUR_NO_PARAMETER = 'noparam' BEHAVIOUR_PARAMETER = 'param' BEHAVIOUR_PARAMETER_ON_SET = 'param_set' BEHAVIOUR_LIST = 'list' CHANNEL_MODES = { 'o', 'p', 's', 'i', 't', 'n', 'b', 'v', 'm', 'r', 'k', 'l' } CHANNEL_MODES_BEHAVIOUR = { BEHAVIOUR_LIST: { 'b' }, BEHAVIOUR_PARAMETER: { 'o', 'v' }, BEHAVIOUR_PARAMETER_ON_SET: { 'k', 'l' }, BEHAVIOUR_NO_PARAMETER: { 'p', 's', 'i', 't', 'n', 'm', 'r' } } CHANNEL_PREFIXES = { '#', '&' } CASE_MAPPINGS = { 'ascii', 'rfc1459', 'strict-rfc1459' } DEFAULT_CASE_MAPPING = 'rfc1459' NICKNAME_PREFIXES = collections.OrderedDict([ ('@', 'o'), ('+', 'v') ]) USER_MODES = { 'i', 'w', 's', 'o' } # Maybe one day, user modes will have parameters... USER_MODES_BEHAVIOUR = { BEHAVIOUR_NO_PARAMETER: { 'i', 'w', 's', 'o' } } ## Message parsing. LINE_SEPARATOR = '\r\n' MINIMAL_LINE_SEPARATOR = '\n' FORBIDDEN_CHARACTERS = { '\r', '\n', '\0' } USER_SEPARATOR = '!' HOST_SEPARATOR = '@' PRIVATE_CHANNEL_SIGIL = '@' SECRET_CHANNEL_SIGIL = '*' PUBLIC_CHANNEL_SIGIL = '=' ARGUMENT_SEPARATOR = re.compile(' +', re.UNICODE) COMMAND_PATTERN = re.compile('^([a-zA-Z]+|[0-9]+)$', re.UNICODE) TRAILING_PREFIX = ':' pydle_0.9.4.orig/pydle/features/rpl_whoishost/__init__.py0000644000000000000000000000021213671517400022017 0ustar0000000000000000""" Adds support for RPL_WHOISHOST (reply type 378) """ from .rpl_whoishost import RplWhoisHostSupport __all__ = ["RplWhoisHostSupport"] pydle_0.9.4.orig/pydle/features/rpl_whoishost/rpl_whoishost.py0000644000000000000000000000155013671517400023172 0ustar0000000000000000from pydle.features.rfc1459 import RFC1459Support class RplWhoisHostSupport(RFC1459Support): """ Adds support for RPL_WHOISHOST messages (378) """ async def on_raw_378(self, message): """ handles a RPL_WHOISHOST message """ _, target, data = message.params data = data.split(" ") target = message.params[1] ip_addr = data[-1] host = data[-2] meta = {"real_ip_address": ip_addr, "real_hostname": host} self._sync_user(target, meta) if target in self._whois_info: self._whois_info[target]["real_ip_address"] = ip_addr self._whois_info[target]["real_hostname"] = host async def whois(self, nickname): info = await super().whois(nickname) info.setdefault("real_ip_address", None) info.setdefault("real_hostname", None) return info pydle_0.9.4.orig/pydle/utils/__init__.py0000644000000000000000000000003213345556615016446 0ustar0000000000000000from . import irccat, run pydle_0.9.4.orig/pydle/utils/_args.py0000644000000000000000000000772213345556615016017 0ustar0000000000000000## _args.py # Common argument parsing code. import argparse import functools import logging import pydle def client_from_args(name, description, default_nick='Bot', cls=pydle.Client): # Parse some arguments. parser = argparse.ArgumentParser(name, description=description, add_help=False, epilog='This program is part of {package}.'.format(package=pydle.__name__)) meta = parser.add_argument_group('Meta') meta.add_argument('-h', '--help', action='help', help='What you are reading right now.') meta.add_argument('-v', '--version', action='version', version='{package}/%(prog)s {ver}'.format(package=pydle.__name__, ver=pydle.__version__), help='Dump version number.') meta.add_argument('-V', '--verbose', help='Be verbose in warnings and errors.', action='store_true', default=False) meta.add_argument('-d', '--debug', help='Show debug output.', action='store_true', default=False) conn = parser.add_argument_group('Connection') conn.add_argument('server', help='The server to connect to.', metavar='SERVER') conn.add_argument('-p', '--port', help='The port to use. (default: 6667, 6697 (TLS))') conn.add_argument('-P', '--password', help='Server password.', metavar='PASS') conn.add_argument('--tls', help='Use TLS. (default: no)', action='store_true', default=False) conn.add_argument('--verify-tls', help='Verify TLS certificate sent by server. (default: no)', action='store_true', default=False) conn.add_argument('-e', '--encoding', help='Connection encoding. (default: UTF-8)', default='utf-8', metavar='ENCODING') init = parser.add_argument_group('Initialization') init.add_argument('-n', '--nickname', help='Nickname. Can be set multiple times to set fallback nicknames. (default: {})'.format(default_nick), action='append', dest='nicknames', default=[], metavar='NICK') init.add_argument('-u', '--username', help='Username. (default: derived from nickname)', metavar='USER') init.add_argument('-r', '--realname', help='Realname (GECOS). (default: derived from nickname)', metavar='REAL') init.add_argument('-c', '--channel', help='Channel to automatically join. Can be set multiple times for multiple channels.', action='append', dest='channels', default=[], metavar='CHANNEL') auth = parser.add_argument_group('Authentication') auth.add_argument('--sasl-identity', help='Identity to use for SASL authentication. (default: )', default='', metavar='SASLIDENT') auth.add_argument('--sasl-username', help='Username to use for SASL authentication.', metavar='SASLUSER') auth.add_argument('--sasl-password', help='Password to use for SASL authentication.', metavar='SASLPASS') auth.add_argument('--sasl-mechanism', help='Mechanism to use for SASL authentication.', metavar='SASLMECH') auth.add_argument('--tls-client-cert', help='TLS client certificate to use.', metavar='CERT') auth.add_argument('--tls-client-cert-keyfile', help='Keyfile to use for TLS client cert.', metavar='KEYFILE') args = parser.parse_args() # Set nicknames straight. if not args.nicknames: nick = default_nick fallback = [] else: nick = args.nicknames.pop(0) fallback = args.nicknames # Set log level. if args.debug: log_level = logging.DEBUG elif not args.verbose: log_level = logging.ERROR logging.basicConfig(level=log_level) # Setup client and connect. client = cls(nickname=nick, fallback_nicknames=fallback, username=args.username, realname=args.realname, sasl_identity=args.sasl_identity, sasl_username=args.sasl_username, sasl_password=args.sasl_password, sasl_mechanism=args.sasl_mechanism, tls_client_cert=args.tls_client_cert, tls_client_cert_key=args.tls_client_cert_keyfile) connect = functools.partial(client.connect, hostname=args.server, port=args.port, password=args.password, encoding=args.encoding, channels=args.channels, tls=args.tls, tls_verify=args.verify_tls ) return client, connect pydle_0.9.4.orig/pydle/utils/irccat.py0000644000000000000000000000355613407746146016171 0ustar0000000000000000#!/usr/bin/env python3 ## irccat.py # Simple threaded irccat implementation, using pydle. import sys import os import threading import logging import asyncio from asyncio.streams import FlowControlMixin 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. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.async_stdin = None @asyncio.coroutine def _send(self, data): sys.stdout.write(data) yield from super()._send(data) @asyncio.coroutine def process_stdin(self): """ Yes. """ loop = self.eventloop.loop self.async_stdin = asyncio.StreamReader() reader_protocol = asyncio.StreamReaderProtocol(self.async_stdin) yield from loop.connect_read_pipe(lambda: reader_protocol, sys.stdin) while True: line = yield from self.async_stdin.readline() if not line: break yield from self.raw(line.decode('utf-8')) yield from self.quit('EOF') @asyncio.coroutine def on_raw(self, message): print(message._raw) yield from super().on_raw(message) @asyncio.coroutine def on_ctcp_version(self, source, target, contents): self.ctcp_reply(source, 'VERSION', 'pydle-irccat v{}'.format(__version__)) def main(): # Setup logging. logging.basicConfig(format='!! %(levelname)s: %(message)s') # Create client. irccat, connect = _args.client_from_args('irccat', default_nick='irccat', description='Process raw IRC messages from stdin, dump received IRC messages to stdout.', cls=IRCCat) irccat.eventloop.schedule_async(connect()) irccat.eventloop.run_with(irccat.process_stdin()) if __name__ == '__main__': main() pydle_0.9.4.orig/pydle/utils/run.py0000644000000000000000000000047713345556615015530 0ustar0000000000000000## run.py # Run client. import asyncio import pydle from . import _args def main(): client, connect = _args.client_from_args('pydle', description='pydle IRC library.') loop = asyncio.get_event_loop() asyncio.ensure_future(connect(), loop=loop) loop.run_forever() if __name__ == '__main__': main() pydle_0.9.4.orig/tests/__init__.py0000644000000000000000000000000012310351621015303 0ustar0000000000000000pydle_0.9.4.orig/tests/conftest.py0000644000000000000000000000210512311334510015400 0ustar0000000000000000import os import pytest def pytest_addoption(parser): # Add option to skip meta (test suite-testing) tests. parser.addoption('--skip-meta', action='store_true', help='skip test suite-testing tests') # Add option to skip slow tests. parser.addoption('--skip-slow', action='store_true', help='skip slow tests') # Add option to skip real life tests. parser.addoption('--skip-real', action='store_true', help='skip real life tests') def pytest_runtest_setup(item): if 'meta' in item.keywords and item.config.getoption('--skip-meta'): pytest.skip('skipping meta test (--skip-meta given)') if 'slow' in item.keywords and item.config.getoption('--skip-slow'): pytest.skip('skipping slow test (--skip-slow given)') if 'real' in item.keywords: if item.config.getoption('--skip-real'): pytest.skip('skipping real life test (--skip-real given)') if (not os.getenv('PYDLE_TESTS_REAL_HOST') or not os.getenv('PYDLE_TESTS_REAL_PORT')): pytest.skip('skipping real life test (no real server given)') pydle_0.9.4.orig/tests/fixtures.py0000644000000000000000000000157612310710363015442 0ustar0000000000000000import pydle from .mocks import MockServer, MockClient, MockEventLoop def with_client(*features, connected=True, **options): if not features: features = (pydle.client.BasicClient,) if features not in with_client.classes: with_client.classes[features] = pydle.featurize(MockClient, *features) def inner(f): def run(): server = MockServer() client = with_client.classes[features]('TestcaseRunner', mock_server=server, **options) if connected: client.connect('mock://local', 1337, eventloop=MockEventLoop()) try: ret = f(client=client, server=server) return ret finally: if client.eventloop: client.eventloop.stop() run.__name__ = f.__name__ return run return inner with_client.classes = {} pydle_0.9.4.orig/tests/mocks.py0000644000000000000000000001414612311154525014705 0ustar0000000000000000import threading import datetime import json import pydle try: from unittest.mock import Mock except: from mock import Mock class MockServer: """ A mock server that will receive data and messages from the client, and can send its own data and messages. """ def __init__(self): self.connection = None self.recvbuffer = '' self.msgbuffer = [] def receive(self, *args, **kwargs): self.msgbuffer.append((args, kwargs)) def receivedata(self, data): self.recvbuffer += data def received(self, *args, **kwargs): if (args, kwargs) in self.msgbuffer: self.msgbuffer.remove((args, kwargs)) return True return False def receiveddata(self, data): if data in self.recvbuffer: self.recvbuffer.replace(data, '', 1) return True return False def send(self, *args, **kwargs): msg = self.connection._mock_client._create_message(*args, **kwargs) self.connection._mock_client.on_raw(msg) def sendraw(self, data): self.connection._mock_client.on_data(data) class MockClient(pydle.client.BasicClient): """ A client that subtitutes its own connection for a mock connection to MockServer. """ def __init__(self, *args, mock_server=None, **kwargs): self._mock_server = mock_server self._mock_logger = Mock() super().__init__(*args, **kwargs) @property def logger(self): return self._mock_logger @logger.setter def logger(self, val): pass def _connect(self, hostname, port, *args, **kwargs): self.connection = MockConnection(hostname, port, mock_client=self, mock_server=self._mock_server, eventloop=self.eventloop) self.connection.connect() self.on_connect() def raw(self, data): self.connection._mock_server.receivedata(data) def rawmsg(self, *args, **kwargs): self.connection._mock_server.receive(*args, **kwargs) def _create_message(self, *args, **kwargs): return MockMessage(*args, **kwargs) def _has_message(self): return b'\r\n' in self._receive_buffer def _parse_message(self): message, _, data = self._receive_buffer.partition(b'\r\n') self._receive_buffer = data return MockMessage.parse(message + b'\r\n', encoding=self.encoding) class MockConnection(pydle.connection.Connection): """ A mock connection between a client and a server. """ def __init__(self, *args, mock_client=None, mock_server=None, **kwargs): super().__init__(*args, **kwargs) self._mock_connected = False self._mock_server = mock_server self._mock_client = mock_client def on(self, *args, **kwargs): pass def off(self, *args, **kwargs): pass @property def connected(self): return self._mock_connected def connect(self, *args, **kwargs): self._mock_server.connection = self self._mock_connected = True def disconnect(self, *args, **kwargs): self._mock_server.connection = None self._mock_connected = False class MockEventLoop: """ A mock event loop for use in testing. """ def __init__(self, *args, **kwargs): self._mock_timers = {} self._mock_periodical_id = 0 self.running = False def __del__(self): pass def run(self): self.running = True def run_with(self, func): self.running = True func() self.stop() def run_until(self, future): self.running = True future.result() self.stop() def stop(self): self.running = False for timer in self._mock_timers.values(): timer.cancel() def schedule(self, f, *args, **kwargs): f(*args, **kwargs) def schedule_in(self, _delay, _f, *_args, **_kw): if isinstance(_delay, datetime.timedelta): _delay = _delay.total_seconds() timer = threading.Timer(_delay, _f, _args, _kw) timer.start() id = self._mock_periodical_id self._mock_timers[id] = timer self._mock_periodical_id += 1 return id def schedule_periodically(self, _delay, _f, *_args, **_kw): if isinstance(_delay, datetime.timedelta): _delay = _delay.total_seconds() id = self._mock_periodical_id timer = threading.Timer(_delay, self._do_schedule_periodically, (_f, _delay, id, _args, _kw)) timer.start() self._mock_timers[id] = timer self._mock_periodical_id += 1 return id def _do_schedule_periodically(self, f, delay, id, args, kw): if not self.is_scheduled(id): return timer = threading.Timer(delay, self._do_schedule_periodically, (f, delay, id, args, kw)) timer.start() self._mock_timers[id] = timer result = False try: result = f(*args, **kw) finally: if result == False: self.unschedule(id) def is_scheduled(self, handle): return handle in self._mock_timers def unschedule(self, handle): self._mock_timers[handle].cancel() del self._mock_timers[handle] class MockMessage(pydle.protocol.Message): def __init__(self, command, *params, source=None, **kw): self.command = command self.params = params self.source = source self.kw = kw self._valid = True @classmethod def parse(cls, line, encoding=pydle.protocol.DEFAULT_ENCODING): # Decode message. line = line.strip() try: message = line.decode(encoding) except UnicodeDecodeError: # Try our fallback encoding. message = line.decode(pydle.protocol.FALLBACK_ENCODING) try: val = json.loads(message) except: raise pydle.protocol.ProtocolViolation('Invalid JSON') return MockMessage(val['command'], *val['params'], source=val['source'], **val['kw']) def construct(self): return json.dumps({ 'command': self.command, 'params': self.params, 'source': self.source, 'kw': self.kw }) + '\r\n' pydle_0.9.4.orig/tests/test__fixtures.py0000644000000000000000000000242512311064777016646 0ustar0000000000000000import pydle from pytest import mark from .fixtures import with_client from .mocks import MockClient, MockServer, MockConnection, MockEventLoop @mark.meta @with_client(connected=False) def test_fixtures_with_client(server, client): assert isinstance(server, MockServer) assert isinstance(client, MockClient) assert client.__class__.__mro__[1] is MockClient, 'MockClient should be first in method resolution order' assert not client.connected @mark.meta @with_client(pydle.features.RFC1459Support, connected=False) def test_fixtures_with_client_features(server, client): assert isinstance(client, MockClient) assert client.__class__.__mro__[1] is MockClient, 'MockClient should be first in method resolution order' assert isinstance(client, pydle.features.RFC1459Support) @mark.meta @with_client(username='test_runner') def test_fixtures_with_client_options(server, client): assert client.username == 'test_runner' @mark.meta @with_client() def test_fixtures_with_client_connected(server, client): assert client.connected assert isinstance(client.eventloop, MockEventLoop) assert isinstance(client.connection, MockConnection) assert isinstance(client.connection.eventloop, MockEventLoop) assert client.eventloop is client.connection.eventloop pydle_0.9.4.orig/tests/test__mocks.py0000644000000000000000000000666312311334544016111 0ustar0000000000000000import time import datetime import pydle from pytest import mark from .fixtures import with_client from .mocks import Mock, MockEventLoop, MockConnection class Passed: def __init__(self): self._passed = False def __bool__(self): return self._passed def set(self): self._passed = True def reset(self): self._passed = False ## Client. @mark.meta @with_client(connected=False) def test_mock_client_connect(server, client): assert not client.connected client.on_connect = Mock() client.connect('mock://local', 1337, eventloop=MockEventLoop()) assert client.connected assert client.on_connect.called client.disconnect() assert not client.connected @mark.meta @with_client() def test_mock_client_send(server, client): client.raw('benis') assert server.receiveddata('benis') client.rawmsg('INSTALL', 'Gentoo') assert server.received('INSTALL', 'Gentoo') @mark.meta @with_client(pydle.features.RFC1459Support) def test_mock_client_receive(server, client): client.on_raw = Mock() server.send('PING', 'test') assert client.on_raw.called message = client.on_raw.call_args[0][0] assert isinstance(message, pydle.protocol.Message) assert message.source is None assert message.command == 'PING' assert message.params == ('test',) ## Connection. @mark.meta def test_mock_connection_connect(): serv = Mock() conn = MockConnection('mock.local', port=1337, mock_server=serv) conn.connect() assert conn.connected assert serv.connection is conn @mark.meta def test_mock_connection_disconnect(): serv = Mock() conn = MockConnection('mock.local', port=1337, mock_server=serv) conn.connect() conn.disconnect() assert not conn.connected ## Event loop. @mark.meta def test_mock_eventloop_schedule(): ev = MockEventLoop() passed = Passed() ev.schedule(lambda: passed.set()) assert passed ev.stop() @mark.meta @mark.slow def test_mock_eventloop_schedule_in(): ev = MockEventLoop() passed = Passed() ev.schedule_in(1, lambda: passed.set()) time.sleep(1.1) assert passed ev.stop() @mark.meta @mark.slow def test_mock_eventloop_schedule_in_timedelta(): ev = MockEventLoop() passed = Passed() ev.schedule_in(datetime.timedelta(seconds=1), lambda: passed.set()) time.sleep(1.1) assert passed @mark.meta @mark.slow def test_mock_eventloop_schedule_periodically(): ev = MockEventLoop() passed = Passed() ev.schedule_periodically(1, lambda: passed.set()) time.sleep(1.1) assert passed passed.reset() time.sleep(1) assert passed ev.stop() @mark.meta @mark.slow def test_mock_eventloop_unschedule_in(): ev = MockEventLoop() passed = Passed() handle = ev.schedule_in(1, lambda: passed.set()) ev.unschedule(handle) time.sleep(1.1) assert not passed @mark.meta @mark.slow def test_mock_eventloop_unschedule_periodically(): ev = MockEventLoop() passed = Passed() handle = ev.schedule_periodically(1, lambda: passed.set()) ev.unschedule(handle) time.sleep(1.1) assert not passed @mark.meta @mark.slow def test_mock_eventloop_unschedule_periodically_after(): ev = MockEventLoop() passed = Passed() handle = ev.schedule_periodically(1, lambda: passed.set()) time.sleep(1.1) assert passed passed.reset() ev.unschedule(handle) time.sleep(1.0) assert not passed pydle_0.9.4.orig/tests/test_client.py0000644000000000000000000000676512311154573016121 0ustar0000000000000000import time import pydle from pytest import raises, mark from .fixtures import with_client from .mocks import Mock, MockEventLoop pydle.client.PING_TIMEOUT = 10 ## Initialization. @with_client(invalid_kwarg=False) def test_client_superfluous_arguments(server, client): assert client.logger.warning.called ## Connection. @with_client() def test_client_reconnect(server, client): client.disconnect(expected=True) assert not client.connected client.connect(reconnect=True) assert client.connected @mark.slow @with_client() def test_client_unexpected_disconnect_reconnect(server, client): client._reconnect_delay = Mock(return_value=0) client.disconnect(expected=False) assert client._reconnect_delay.called time.sleep(0.1) assert client.connected @with_client() def test_client_unexpected_reconnect_give_up(server, client): client.RECONNECT_ON_ERROR = False client.disconnect(expected=False) assert not client.connected @mark.slow @with_client() def test_client_unexpected_disconnect_reconnect_delay(server, client): client._reconnect_delay = Mock(return_value=1) client.disconnect(expected=False) assert not client.connected time.sleep(1.1) assert client.connected @with_client() def test_client_reconnect_delay_calculation(server, client): client.RECONNECT_DELAYED = False assert client._reconnect_delay() == 0 client.RECONNECT_DELAYED = True for expected_delay in client.RECONNECT_DELAYS: delay = client._reconnect_delay() assert delay == expected_delay client._reconnect_attempts += 1 assert client._reconnect_delay() == client.RECONNECT_DELAYS[-1] @with_client() def test_client_disconnect_on_connect(server, client): client.disconnect = Mock() client.connect('mock://local', 1337) assert client.connected assert client.disconnect.called @with_client(connected=False) def test_client_connect_invalid_params(server, client): with raises(ValueError): client.connect() with raises(ValueError): client.connect(port=1337) @mark.slow @with_client() def test_client_timeout(server, client): client.on_data_error = Mock() time.sleep(pydle.client.PING_TIMEOUT + 1) assert client.on_data_error.called assert isinstance(client.on_data_error.call_args[0][0], TimeoutError) @with_client(connected=False) def test_client_server_tag(server, client): ev = MockEventLoop() assert client.server_tag is None client.connect('Mock.local', 1337, eventloop=ev) assert client.server_tag == 'mock' client.disconnect() client.connect('irc.mock.local', 1337, eventloop=ev) assert client.server_tag == 'mock' client.disconnect() client.connect('mock', 1337, eventloop=ev) assert client.server_tag == 'mock' client.disconnect() client.connect('127.0.0.1', 1337, eventloop=ev) assert client.server_tag == '127.0.0.1' client.network = 'MockNet' assert client.server_tag == 'mocknet' client.disconnect() ## Messages. @with_client() def test_client_message(server, client): client.on_raw_install = Mock() server.send('INSTALL', 'gentoo') assert client.on_raw_install.called message = client.on_raw_install.call_args[0][0] assert isinstance(message, pydle.protocol.Message) assert message.command == 'INSTALL' assert message.params == ('gentoo',) @with_client() def test_client_unknown(server, client): client.on_unknown = Mock() server.send('INSTALL', 'gentoo') assert client.on_unknown.called pydle_0.9.4.orig/tests/test_client_channels.py0000644000000000000000000000242112311074624017753 0ustar0000000000000000import pydle from .fixtures import with_client @with_client() def test_client_same_channel(server, client): assert client.is_same_channel('#lobby', '#lobby') assert not client.is_same_channel('#lobby', '#support') assert not client.is_same_channel('#lobby', 'jilles') @with_client() def test_client_in_channel(server, client): client._create_channel('#lobby') assert client.in_channel('#lobby') @with_client() def test_client_is_channel(server, client): # Test always true... assert client.is_channel('#lobby') assert client.is_channel('WiZ') assert client.is_channel('irc.fbi.gov') @with_client() def test_channel_creation(server, client): client._create_channel('#pydle') assert '#pydle' in client.channels assert client.channels['#pydle']['users'] == set() @with_client() def test_channel_destruction(server, client): client._create_channel('#pydle') client._destroy_channel('#pydle') assert '#pydle' not in client.channels @with_client() def test_channel_user_destruction(server, client): client._create_channel('#pydle') client._create_user('WiZ') client.channels['#pydle']['users'].add('WiZ') client._destroy_channel('#pydle') assert '#pydle' not in client.channels assert 'WiZ' not in client.users pydle_0.9.4.orig/tests/test_client_users.py0000644000000000000000000000663512311064261017330 0ustar0000000000000000import pydle from .fixtures import with_client @with_client() def test_client_same_nick(server, client): assert client.is_same_nick('WiZ', 'WiZ') assert not client.is_same_nick('WiZ', 'jilles') assert not client.is_same_nick('WiZ', 'wiz') @with_client() def test_user_creation(server, client): client._create_user('WiZ') assert 'WiZ' in client.users assert client.users['WiZ']['nickname'] == 'WiZ' @with_client() def test_user_invalid_creation(server, client): client._create_user('irc.fbi.gov') assert 'irc.fbi.gov' not in client.users @with_client() def test_user_renaming(server, client): client._create_user('WiZ') client._rename_user('WiZ', 'jilles') assert 'WiZ' not in client.users assert 'jilles' in client.users assert client.users['jilles']['nickname'] == 'jilles' @with_client() def test_user_renaming_creation(server, client): client._rename_user('null', 'WiZ') assert 'WiZ' in client.users assert 'null' not in client.users @with_client() def test_user_renaming_invalid_creation(server, client): client._rename_user('null', 'irc.fbi.gov') assert 'irc.fbi.gov' not in client.users assert 'null' not in client.users @with_client() def test_user_renaming_channel_users(server, client): client._create_user('WiZ') client._create_channel('#lobby') client.channels['#lobby']['users'].add('WiZ') client._rename_user('WiZ', 'jilles') assert 'WiZ' not in client.channels['#lobby']['users'] assert 'jilles' in client.channels['#lobby']['users'] @with_client() def test_user_deletion(server, client): client._create_user('WiZ') client._destroy_user('WiZ') assert 'WiZ' not in client.users @with_client() def test_user_channel_deletion(server, client): client._create_channel('#lobby') client._create_user('WiZ') client.channels['#lobby']['users'].add('WiZ') client._destroy_user('WiZ', '#lobby') assert 'WiZ' not in client.users assert client.channels['#lobby']['users'] == set() @with_client() def test_user_channel_incomplete_deletion(server, client): client._create_channel('#lobby') client._create_channel('#foo') client._create_user('WiZ') client.channels['#lobby']['users'].add('WiZ') client.channels['#foo']['users'].add('WiZ') client._destroy_user('WiZ', '#lobby') assert 'WiZ' in client.users assert client.channels['#lobby']['users'] == set() @with_client() def test_user_synchronization(server, client): client._create_user('WiZ') client._sync_user('WiZ', { 'hostname': 'og.irc.developer' }) assert client.users['WiZ']['hostname'] == 'og.irc.developer' @with_client() def test_user_synchronization_creation(server, client): client._sync_user('WiZ', {}) assert 'WiZ' in client.users @with_client() def test_user_invalid_synchronization(server, client): client._sync_user('irc.fbi.gov', {}) assert 'irc.fbi.gov' not in client.users @with_client() def test_user_mask_format(server, client): client._create_user('WiZ') assert client._format_user_mask('WiZ') == 'WiZ!*@*' client._sync_user('WiZ', { 'username': 'wiz' }) assert client._format_user_mask('WiZ') == 'WiZ!wiz@*' client._sync_user('WiZ', { 'hostname': 'og.irc.developer' }) assert client._format_user_mask('WiZ') == 'WiZ!wiz@og.irc.developer' client._sync_user('WiZ', { 'username': None }) assert client._format_user_mask('WiZ') == 'WiZ!*@og.irc.developer' pydle_0.9.4.orig/tests/test_featurize.py0000644000000000000000000000275512311334563016633 0ustar0000000000000000import pydle from .mocks import MockClient from .fixtures import with_client def with_errorcheck_client(*features): def inner(f): def run(): try: return with_client(*features, connected=False)(f)() except TypeError as e: assert False, e run.__name__ = f.__name__ return run return inner def assert_mro(client, *features): # Skip FeaturizedClient, MockClient, pydle.BasicClient and object classes. assert client.__class__.__mro__[2:-2] == features class FeatureClass(pydle.BasicClient): pass class SubFeatureClass(FeatureClass): pass class SubBFeatureClass(FeatureClass): pass class DiamondFeatureClass(SubBFeatureClass, SubFeatureClass): pass @with_errorcheck_client() def test_featurize_basic(server, client): assert_mro(client) @with_errorcheck_client(FeatureClass) def test_featurize_multiple(server, client): assert_mro(client, FeatureClass) @with_errorcheck_client(SubFeatureClass) def test_featurize_inheritance(server, client): assert_mro(client, SubFeatureClass, FeatureClass) @with_errorcheck_client(FeatureClass, SubFeatureClass) def test_featurize_inheritance_ordering(server, client): assert_mro(client, SubFeatureClass, FeatureClass) @with_errorcheck_client(SubBFeatureClass, SubFeatureClass, DiamondFeatureClass) def test_featurize_inheritance_diamond(server, client): assert_mro(client, DiamondFeatureClass, SubBFeatureClass, SubFeatureClass, FeatureClass) pydle_0.9.4.orig/tests/test_ircv3.py0000644000000000000000000000114213600004240015634 0ustar0000000000000000import pytest from pydle.features import ircv3 pytestmark = pytest.mark.unit @pytest.mark.parametrize( "payload, expected", [ ( rb"@+example=raw+:=,escaped\:\s\\ :irc.example.com NOTICE #channel :Message", {"+example": """raw+:=,escaped; \\"""} ), ( rb"@+example=\foo\bar :irc.example.com NOTICE #channel :Message", {"+example": "foobar"} ), ] ) def test_tagged_message_escape_sequences(payload, expected): message = ircv3.tags.TaggedMessage.parse(payload) assert message.tags == expected