numberoverzero / bottom Goto Github PK
View Code? Open in Web Editor NEWasyncio-based rfc2812-compliant IRC Client
Home Page: http://bottom-docs.readthedocs.io
License: MIT License
asyncio-based rfc2812-compliant IRC Client
Home Page: http://bottom-docs.readthedocs.io
License: MIT License
As noted in the README and pack.py, there are multiple commands which take a parameter, which is not consumed by the current packer.
What does this do? Why would I use it? How do I use it?
Implement additional IRC commands and events as requested by users.
After unpacking, the reading loop will yield from
the event dispatch, which means it waits until all handlers run for the event, before continuing.
Instead, events.trigger
should be a non-blocking call. That is, it should push the event into a queue, then return execution to the caller.
I'm working on accordian to fix this - however, in trying to use the 3.5 syntax (await
and async
as keywords) I've run into other problems. Once I've finished testing the library, I'll integrate it into bottom. This will require 3.5+ to use bottom, but the migration from 3.4 (current requirement) to 3.5 is virtually painless (I haven't seen any reports of problems migrating).
Document each command's parameters, including defaults and raw irc line equivalent
I've decided to revert the change that made connect/disconnect synchronous but non-blocking, and will again make them coroutines. I expected this to be a tough decision, but after writing out the examples below it seems rather straightforward.
I think the examples are valuable enough for future discussions about the API to keep the issue around, even though I intend to implement the coroutine version immediately.
connect()
The coroutine form is the clear winner here, with an explicit "wait until this happens before executing the rest of the body". Contrasting, the other form requires setting an asyncio.Event
in a different function and awaiting it in the place we care about it. If any other function can clear/set that event, things become spaghetti quickly.
@client.on("client_disconnect")
def reconnect(**kwargs):
await asyncio.sleep(3, loop=client.loop)
await client.connect()
client.trigger("reconnect.complete")
There are a few ways to solve this, but let's use an asyncio.Event
for now.
conn_established = asyncio.Event(loop=client.loop)
@client.on("client_connect")
def _mark_connect_complete(**kwargs):
conn_established.set()
@client.on("client_disconnect")
def reconnect(**kwargs):
await asyncio.sleep(3, loop=client.loop)
conn_established.clear()
client.connect()
await conn_established.wait()
client.trigger("reconnect.complete")
The non-coroutine form wins here, but I don't think it's by much. Because it's so easy to schedule coroutines for an event loop, and Client
exposes that loop, it's barely more code for the coroutine version.
@client.on("client_disconnect")
def reconnect(**kwargs):
client.loop.create_task(client.connect())
print("I print immediately")
@client.on("client_disconnect")
def reconnect(**kwargs):
client.connect()
print("I print immediately")
It appears that the asyncio API has changed in python 3.10, causing Bottom to exception.
Python 3.10.1 (main, Dec 11 2021, 17:22:55) [GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import bottom
>>> import asyncio
>>> host = 'chat.freenode.net'
>>> port = 6697
>>> ssl = True
>>> NICK = "bottom-bot"
>>> CHANNEL = "#bottom-dev"
>>> bot = bottom.Client(host=host, port=port, ssl=ssl)
>>> bot.loop.create_task(bot.connect())
<Task pending name='Task-1' coro=<RawClient.connect() running at /home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py:69>>
>>> bot.loop.run_forever()
Task exception was never retrieved
future: <Task finished name='Task-2' coro=<process() done, defined at /home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py:12> exception=TypeError('As of 3.10, the *loop* parameter was removed from Event() since it is no longer necessary')>
Traceback (most recent call last):
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 28, in process
await next_handler(message)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 26, in next_handler
await handler(next_handler, message)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 191, in handler
client.trigger(event, **kwargs)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 102, in trigger
async_event = self._events[event]
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 53, in <lambda>
lambda: asyncio.Event(loop=self.loop))
File "/usr/lib/python3.10/asyncio/locks.py", line 167, in __init__
super().__init__(loop=loop)
File "/usr/lib/python3.10/asyncio/mixins.py", line 17, in __init__
raise TypeError(
TypeError: As of 3.10, the *loop* parameter was removed from Event() since it is no longer necessary
Task exception was never retrieved
future: <Task finished name='Task-3' coro=<process() done, defined at /home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py:12> exception=TypeError('As of 3.10, the *loop* parameter was removed from Event() since it is no longer necessary')>
Traceback (most recent call last):
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 28, in process
await next_handler(message)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 26, in next_handler
await handler(next_handler, message)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 191, in handler
client.trigger(event, **kwargs)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 102, in trigger
async_event = self._events[event]
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 53, in <lambda>
lambda: asyncio.Event(loop=self.loop))
File "/usr/lib/python3.10/asyncio/locks.py", line 167, in __init__
super().__init__(loop=loop)
File "/usr/lib/python3.10/asyncio/mixins.py", line 17, in __init__
raise TypeError(
TypeError: As of 3.10, the *loop* parameter was removed from Event() since it is no longer necessary
Task exception was never retrieved
future: <Task finished name='Task-4' coro=<process() done, defined at /home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py:12> exception=TypeError('As of 3.10, the *loop* parameter was removed from Event() since it is no longer necessary')>
Traceback (most recent call last):
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 28, in process
await next_handler(message)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 26, in next_handler
await handler(next_handler, message)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 191, in handler
client.trigger(event, **kwargs)
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 102, in trigger
async_event = self._events[event]
File "/home/uriah/git/urwbot/.venv/lib/python3.10/site-packages/bottom/client.py", line 53, in <lambda>
lambda: asyncio.Event(loop=self.loop))
File "/usr/lib/python3.10/asyncio/locks.py", line 167, in __init__
super().__init__(loop=loop)
File "/usr/lib/python3.10/asyncio/mixins.py", line 17, in __init__
raise TypeError(
TypeError: As of 3.10, the *loop* parameter was removed from Event() since it is no longer necessary
^CTraceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.10/asyncio/base_events.py", line 595, in run_forever
self._run_once()
File "/usr/lib/python3.10/asyncio/base_events.py", line 1845, in _run_once
event_list = self._selector.select(timeout)
File "/usr/lib/python3.10/selectors.py", line 469, in select
fd_event_list = self._selector.poll(timeout, max_ev)
KeyboardInterrupt
http://bottom-docs.readthedocs.io/en/latest/user/installation.html
bottom no longer depends on simplex, and the docs should be updated to explain that.
Install instructions should mention:
Hello,
The doc at http://bottomdocs.readthedocs.io/en/latest/user/api.html mentioned in the README is unavailable.
partial_bind
is failing to preserve self
when handed a bound method, probably because the signature is inspected after wrapping non-coroutines. Inspecting first should take care of the problem.
minimal repro:
class Object:
def handle(self, host, port):
print("Connected: {} {}".format(host, port))
obj = Object()
bound_method = obj.handle
import asnycio
import bottom
client = bottom.Client("localhost", "8080")
client.on("client_connect")(bound_method)
# Throws when connecting because Object.handle gets params "host" and "port" but not "self"
asyncio.get_event_loop().run_until_complete(client.run())
Figure out why certain files are included in local runs, but not in the coveralls report.
collections.abc was moved to a separate module in 3.3 rather than being the abc attribute of the collections module. Therefore it needs to be imported in pack.py else an AttributeError is raised when packing iterables.
Add a "raw" command type. such as:
bot.send('RAW', message='<server specific command / non-rfc2812 command here>')
Have it sort of a "use at your own peril" sort of thing (meaning there would be no error handling, just dump the raw error message out).
IE: pack.py:
elif command == "RAW":
return "{}".format(f("message", kwargs))
I totally understand the concept that this would circumvent the whole "doing commands as routes",, but this could make it simpler for those of use that have extended upon the original library for functionality of non-standardized operations within a specific IRC server.
From #53, consider an SASL extension or baking functionality into bottom directly.
Option to get all users in channel with NAMES.
https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands#NAMES
This request is from an external source, not sure if they have a github account. It should be possible either with the existing event handlers, or with the addition of a raw handler (see #32). I haven't personally used Blowfish with IRC, but it looks neat. Extract of some comments that could get this started:
most every channel i am in however uses blowfish. for mIRC people use FISH10. [...] is there any easy way to implement decryption of blowfish messages in channels? these channels have fixed blowfish keys..
[...] each channel has it's own blowfish key. to know if the message is encrypted it starts with: +OK which signifies any text following is encrypted with the fish key for the channel, the same key must be used to decrypt the encrypted text. so encrypt/decrypt is always the same value/key for a particular channel.
[...] an implementation for irc and blowfish in python. the only important piece for me is: "blowcrypt, Fish etc" [...].
http://www.bjrn.se/code/irccrypt/irccrypt.py
you have to decrypt whatever comes after "+OK " which is the first 4 bytes of the PRIVMSG with the fish key for the channel. there are unique keys for nicknames which are automatically derived with the DH1080 key exchange between the bot and the user.
It would be nice to be able to @client.on('MODE')
for catching user and channel mode changes.
EDIT: it looks like TOPIC is also not an event.
EDIT 2: It looks like a lot of the RFC Non-numeric events are being missed, (KICK is on the list as well)
(first proposed in #29)
Following the rfc2812 spec leaves some particular warts, especially when it comes to the historical command names like RPL_WHOREPLY
and RPL_NAMREPLY
.
There should be an easy way to specify a set of aliases when hooking into the rfc commands, so that reading the code is more natural.
This isn't the intended implementation, just a quick example of how the bot.on
would hook up for some alias:
@bot.on("who_response")
def handle(...):
...
bot.aliases["RPL_WHOREPLY"].add("who_response")
I don't know if there's a risk of confusing a custom event for an irc command, but I think that's always been possible since there's no restriction on what the client can trigger. Worth considering but probably not an issue.
Similar to accordian we should be creating a new event loop per test.
This will be a larger part of the loop refactor that needs to occur for the main components: Client and Connection.
The client is not connecting to freenode. Here's my code:
import asyncio
import bottom
client = bottom.Client(host="chat.freenode.net", port=6697, ssl=True))
@client.on("ping")
def handle(message=None, **kwargs):
message = message or ""
client.send("pong", message=message)
@client.on("client_disconnect")
async def reconnect(**kwargs):
await asyncio.sleep(2, loop=client.loop)
client.loop.create_task(client.connect())
def waiter(client):
async def wait_for(*events, return_when=asyncio.FIRST_COMPLETED):
if not events:
return
done, pending = await asyncio.wait(
[client.wait(event) for event in events],
loop=client.loop,
return_when=return_when,
)
# Get the result(s) of the completed task(s).
ret = [future.result() for future in done]
# Cancel any events that didn't come in.
for future in pending:
future.cancel()
# Return list of completed event names.
return ret
return wait_for
# taken from :ref:`Patterns`
wait_for = waiter(client)
@client.on("client_connect")
async def connect(**kwargs):
client.default_name = "qpowieurtyturiewqop"
client.send("nick", nick=client.default_name)
client.send("user", user=client.default_name, realname=client.default_name)
events = await wait_for("rpl_endofmotd", "err_nomotd") # it gets stuck at this line
print("Connected")
client.send("join", channel="#channel")
client.loop.create_task(client.connect())
client.loop.run_forever()
I tried this on another PC and it worked.
Hey!
Just wanted to discuss if you would be willing to support Python 2 on a different branch? One of the projects I'm working on has a hard dependency on a project which is Python 2 only and there are few alternatives to this library.
And I don't want to replace Bottom with another library either as the terseness of it is very appealing.
I would be willing to maintain it as on a "best effort basis" as you make changes to the Python 3 branch. Or if you prefer, I could maintain it as a separate project (called "Top" :P).
Bottom's protocol makes an assertion that the transport is an instance of asyncio.WriteTransport (see https://github.com/numberoverzero/bottom/blob/master/bottom/protocol.py#L24 )
This assertion isn't compatible with uvloop library - an alternative event loop with better performance ( https://github.com/MagicStack/uvloop )
Is there a good reason to keep the assertion? I tried to remove it and my program works as expected. Thanks in advance for your help.
Python 3.5.1 (v3.5.1:37a07cee5969, Dec 6 2015, 01:54:25) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import bottom
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\Admin\AppData\Local\Programs\Python\Python35\lib\site-packages\bottom\__init__.py", line 2, in <module>
from bottom.client import Client
File "C:\Users\Admin\AppData\Local\Programs\Python\Python35\lib\site-packages\bottom\client.py", line 5, in <module>
from bottom.protocol import Protocol
File "C:\Users\Admin\AppData\Local\Programs\Python\Python35\lib\site-packages\bottom\protocol.py", line 4, in <module>
from typing import Optional, TYPE_CHECKING
ImportError: cannot import name 'TYPE_CHECKING'
Just installed it, typed in import bottom
and it just errors out.
Running on Python version 3.5.1 (v3.5.1:37a07cee5969, Dec 6 2015, 01:54:25) [MSC v.1900 64 bit (AMD64)]
, and on a Windows-7-6.1.7601-SP1
machine.
Is the project still alive or is it dead? The last release was over a year ago.
Both: mock out events to track invokations and args.
Connection
: need a not-terrible way to mock up asyncio.open_connection
to return fake StreamReader
, StreamWriter
instances.
Client
: mock out connection
Currently, bot.send()
interprets a list of pre-coded IRC commands and extends them to raw. It is not possible to send raw command directly to a IRC server, rendering the library less useful to work with different non-standard commands without modifying or monkey-patching bottom
.
For example, a vast majority of IRCv3 commands is an extension to RFC-2812.
Could you please add a way to send raw commands?
the example posted on the README:
import bottom
NICK = 'bottom-bot'
CHANNEL = '#python'
bot = bottom.Client('localhost', 6697)
@bot.on('CLIENT_CONNECT')
def connect(**kwargs):
bot.send('NICK', nick=NICK)
bot.send('USER', user=NICK, realname='Bot using bottom.py')
bot.send('JOIN', channel=CHANNEL)
@bot.on('PING')
def keepalive(message, **kwargs):
bot.send('PONG', message=message)
@bot.on('PRIVMSG')
def message(nick, target, message, **kwargs):
""" Echo all messages """
# Don't echo ourselves
if nick == NICK:
return
# Respond directly to direct messages
if target == NICK:
bot.send("PRIVMSG", target=nick, message=message)
# Channel message
else:
bot.send("PRIVMSG", target=target, message=message)
# This schedules a connection to be created when the bot's event loop
# is run. Nothing will happen until the loop starts running to clear
# the pending coroutines.
bot.loop.create_task(bot.connect())
# Ctrl + C to quit
bot.loop.run_forever()
is giving me this error:
Traceback (most recent call last):
File "bottom_test.py3", line 38, in <module>
bot.loop.create_task(bot.connect())
AttributeError: 'Client' object has no attribute 'loop'
Running python 3.5.1, bottom==0.9.13.
Hello, I am making a Discord bridge for my IRC server. Since I have only 1 bot, I thought I could change the bot's nick to the person on Discord who sent that message. Though, I can't, because it will show MyIRCBot is now known as Name
, and that's a big problem. Is there a way to change the nickname without showing that message and then send the message?
It is entirely valid for a server to use a trailing delimiter in the response parameters when one is not required. (source)
That means the following are functionally equivalent:
:nick!user@host JOIN #channel
:nick!user@host JOIN :#channel
I'm using Python 3.5.1, and trying to use the example echo.py
on a variety of efnet servers. I set ssl to False
. I tested on both OSX and Debian. I added some debug statements like this:
@bot.on('CLIENT_CONNECT')
def connect(**kwargs):
print(kwargs)
print('in connect sending NICK')
bot.send('NICK', nick=config.nick)
print('sending USER')
bot.send('USER', user=config.nick, realname=config.real_name)
print('sending JOIN')
bot.send('JOIN', channel=config.channel)
print('sent JOIN')
And the output is:
in connect sending NICK
sending USER
sending JOIN
sent JOIN
However, the bot appears to hang at that point. I don't see it join my channel nor do I see it connected to the server at all. Let me know if there is any other info I can provide to help debug. Thanks!
Is there anyway to connect to multiple servers?.
Something similar to a main function that would launch each client instance that are in multiple files?
Ex: lets i have , Freenode.py for freenoed and libera.py for libera.chat.
Currently I run each one of them on their own screen session?
Is there anyway I could just run a single main.py to run both the clients? (without something like subprocess)
Sometimes a server might change a user's nickname. Right now I have no way of telling if the nickname was changed.
I've used this to print each and every event I get:
def handle_logging(session):
for event in [
'CLIENT_CONNECT',
'JOIN',
'NOTICE',
'PART',
'PING',
'PRIVMSG',
'RPL_BOUNCE',
'RPL_CREATED',
'RPL_ENDOFMOTD',
'RPL_LUSERCHANNELS',
'RPL_LUSERCLIENT',
'RPL_LUSERME',
'RPL_LUSEROP',
'RPL_LUSERUNKNOWN',
'RPL_MOTD',
'RPL_MOTDSTART',
'RPL_MYINFO',
'RPL_WELCOME',
'RPL_YOURHOST',
]:
session.bot.on(
event,
lambda **kwargs: print(
'{}: {} {}'.format(
arrow.utcnow(),
event,
kwargs,
)))
None of the events were a nickname change event, just a RPL_YOURHOST warning me that I have 30 seconds to change my nickname and 30 seconds later RPL_YOURHOST telling me that it'll change my nickname. Nothing standard.
Many thanks!
Lines 70 to 71 in e6727f7
It's worth checking out asyncio.open_connection
to see what ssl=True
does, and what additional settings are available (eg. certificate validation) to harden against common attacks.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.