[![Build Status] (https://travis-ci.org/numberoverzero/bottom.svg?branch=master)] (https://travis-ci.org/numberoverzero/bottom)[![Coverage Status] (https://coveralls.io/repos/numberoverzero/bottom/badge.png?branch=master)] (https://coveralls.io/r/numberoverzero/bottom?branch=master)
Downloads https://pypi.python.org/pypi/bottom
Source https://github.com/numberoverzero/bottom
asyncio-based rfc2812-compliant IRC Client
pip install bottom
bottom isn't a kitchen-sink library. Instead, it provides a consistent API with a small surface area, tuned for performance and ease of extension. Similar to the routing style of bottle.py, hooking into events is one line.
import bottom
import asyncio
NICK = 'bottom-bot'
CHANNEL = '#python'
bot = bottom.Client('localhost', 6697)
@bot.on('CLIENT_CONNECT')
def connect():
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):
bot.send('PONG', message=message)
@bot.on('PRIVMSG')
def message(nick, target, message):
''' Echo all messages '''
# Don't echo ourselves
if nick == NICK:
return
# Direct message to bot
if target == NICK:
bot.send("PRIVMSG", target=nick, message=message)
# Message in channel
else:
bot.send("PRIVMSG", target=target, message=message)
asyncio.get_event_loop().run_until_complete(bot.run())
-
Bottom follows semver for its public API.
- Currently,
Client
is the only public member of bottom. - IRC replies/codes which are not yet implemented may be added at any time, and will correspond to a patch - the function contract of
@on
method does not change. - You should not rely on the internal api staying the same between minor versions.
- Over time, private apis may be raised to become public. The reverse will never occur.
- Currently,
-
There are a number of changes from RFC2812 - none should noticeably change how you interact with a standard IRC server. For specific adjustments, see the notes section of each command in
Supported Commands
.
Contributions welcome! When reporting issues, please provide enough detail to reproduce the bug - sample code is ideal. When submitting a PR, please make sure tox
passes (including flake8).
bottom uses tox
, pytest
and flake8
. To get everything set up:
# RECOMMENDED: create a virtualenv with:
# mkvirtualenv bottom
git clone https://github.com/numberoverzero/bottom.git
pip install tox
tox
- Better
Client
docstrings - Add missing replies/errors to
unpack.py:unpack_command
- Add reply/error parameters to
unpack.py:parameters
- Document
Supported Events
- Add reply/error parameters to
This is a coroutine.
Start the magic. This will connect the client, and then read until it disconnects. The CLIENT_DISCONNECT
event will fire before the loop exits, allowing you to yield from Client.connect()
and keep the client running.
If you want to call this synchronously (block until it's complete) use the following:
import asyncio
# ... client is defined somewhere
loop = asyncio.get_event_loop()
task = client.run()
loop.run_until_complete(task)
This @decorator
is the main way you'll interact with a Client
. It takes a string, returning a function wrapper that validates the function and registers it for the given event. When that event occurs, the function will be called, mapping any arguments the function may expect from the set of available arguments for the event.
Not all available arguments need to be used. For instance, both of the following are valid:
@bot.on('PRIVMSG')
def event(nick, message, target):
''' Doesn't use user, host. argument order is different '''
# message sent to bot - echo message
if target == bot.nick:
bot.send('PRIVMSG', target, message=message)
# Some channel we're watching
elif target == bot.monitored_channel:
logger.info("{} -> {}: {}".format(nick, target, message))
@bot.on('PRIVMSG')
def func(message, target):
''' Just waiting for the signal '''
if message == codeword && target == secret_channel:
execute_heist()
VAR_KWARGS can be used, as long as the name doesn't mask an actual parameter. VAR_ARGS may not be used.
# OK - kwargs, no masking
@bot.on('PRIVMSG')
def event(message, **everything_else):
logger.log(everything_else['nick'] + " said " + message)
# NOT OK - kwargs, masking parameter <nick>
@bot.on('PRIVMSG')
def event(message, **nick):
logger.log(nick['target'])
# NOT OK - uses VAR_ARGS
@bot.on('PRIVMSG')
def event(message, *args):
logger.log(args)
Decorated functions will be invoked asynchronously, and may optionally use the yield from
syntax. Functions do not need to be wrapped with @ayncio.coroutine
- this is handled as part of the function caching process.
This is a coroutine.
Manually inject a command or reply as if it came from the server. This is useful for invoking other handlers.
# Manually trigger `PRIVMSG` handlers:
yield from bot.trigger('privmsg', nick="always_says_no", message="yes")
# Rename !commands to !help
@bot.on('privmsg')
def parse(nick, target, message):
if message == '!commands':
bot.send('privmsg', target=nick,
message="!commands was renamed to !help in 1.2")
# Don't make them retype it, just make it happen
yield from bot.trigger('privmsg', nick=nick,
target=target, message="!help")
# While testing the auto-reconnect module, simulate a disconnect:
def test_reconnect(bot):
loop = asyncio.get_event_loop()
loop.run_until_complete(bot.trigger("client_disconnect"))
assert bot.connected
This is a coroutine.
Attempt to reconnect using the client's host, port.
@bot.on('client_disconnect')
def reconnect():
# Wait a few seconds
yield from asyncio.sleep(3)
yield from bot.connect()
This is a coroutine.
Disconnect from the server if connected.
@bot.on('privmsg')
def suicide_pill(nick, message):
if nick == "spy_handler" and message == "last stop":
yield from bot.disconnect()
Send a command to the server. See Supported Commands
for a detailed breakdown of available commands and their parameters.
These commands can be sent to the server using Client.send
.
For incoming signals and messages, see Supported Events
below.
There are three parts to each command's documentation:
- Python syntax - sample calls using available parameters
- Normalized IRC wire format - the normalized translation from python keywords to a literal string that will be constructed by the client and sent to the server. The following syntax is used:
<parameter>
the location of theparameter
passed tosend
. Literal<>
are not transferred.[value]
an optional value, which may be excluded. In some cases, such asLINKS
, an optional value may only be provided if another dependant value is present. Literal[]
are not transferred.:
the start of a field which may contain spaces. This is always the last field of an IRC line."value"
literal value as printed. Literal""
are not transferred.
- Notes - additional options or restrictions on commands that do not fit a pre-defined convention. Common notes include keywords for ease of searching:
RFC_DELTA
- Some commands have different parameters from their RFC2812 definitions. Please pay attention to these notes, since they are the most likely to cause issues. These changes can include:- Addition of new required or optional parameters
- Default values for new or existing parameters
CONDITIONAL_OPTION
- there are some commands whose values depend on each other. For example,LINKS
,<mask>
REQUIRES<remote>
.MULTIPLE_VALUES
- Some commands can handle non-string iterables, such asWHOWAS
where<nick>
can handle both"WiZ"
and["WiZ", "WiZ-friend"]
.PARAM_RENAME
- Some commands have renamed parameters from their RFC2812 specification to improve comsistency.
(trigger only)
yield from client.trigger('CLIENT_CONNECT', host='localhost', port=6697)
yield from client.trigger('CLIENT_DISCONNECT', host='localhost', port=6697)
client.send('PASS', password='hunter2')
PASS <password>
client.send('nick', nick='WiZ')
NICK <nick>
- PARAM_RENAME
nickname -> nick
client.send('USER', user='WiZ-user', realname='Ronnie')
client.send('USER', user='WiZ-user', mode='8', realname='Ronnie')
USER <user> [<mode>] :<realname>
- RFC_DELTA
mode
is optional - default is0
client.send('OPER', user='WiZ', password='hunter2')
OPER <user> <password>
- PARAM_RENAME
name -> user
client.send('USERMODE', nick='WiZ')
client.send('USERMODE', nick='WiZ', modes='+io')
MODE <nick> [<modes>]
- RFC_DELTA rfc did not name
modes
parameter
client.send('SERVICE', nick='CHANSERV', distribution='*.en',
type='0', info='manages channels')
SERVICE <nick> <distribution> <type> :<info>
- PARAM_RENAME
nickname -> nick
client.send('QUIT')
client.send('QUIT', message='Gone to Lunch')
QUIT :[<message>]
- PARAM_RENAME
Quit Message -> message
client.send('SQUIT', server='tolsun.oulu.fi')
client.send('SQUIT', server='tolsun.oulu.fi', message='Bad Link')
SQUIT <server> :[<message>]
- PARAM_RENAME
Comment -> message
- RFC_DELTA
message
is optional - rfc says comment SHOULD be supplied; syntax shows required
client.send('JOIN', channel='0') # send PART to all joined channels
client.send('JOIN', channel='#foo-chan')
client.send('JOIN', channel='#foo-chan', key='foo-key')
client.send('JOIN', channel=['#foo-chan', '#other'], key='key-for-both')
client.send('JOIN', channel=['#foo-chan', '#other'], key=['foo-key', 'other-key'])
JOIN <channel> [<key>]
- MULTIPLE_VALUES
channel
andkey
- If
channel
has n > 1 values,key
MUST have 1 or n values
client.send('PART', channel='#foo-chan')
client.send('PART', channel=['#foo-chan', '#other'])
client.send('PART', channel='#foo-chan', message='I lost')
PART <channel> :[<message>]
- MULTIPLE_VALUES
channel
CHANNELMODE (renamed from MODE)
client.send('CHANNELMODE', channel='#foo-chan', modes='+b')
client.send('CHANNELMODE', channel='#foo-chan', modes='+l', params='10')
MODE <channel> <modes> [<params>]
- PARAM_RENAME
modeparams -> params
client.send('TOPIC', channel='#foo-chan')
client.send('TOPIC', channel='#foo-chan', message='') # Clear channel message
client.send('TOPIC', channel='#foo-chan', message='Yes, this is dog')
TOPIC <channel> :[<message>]
- PARAM_RENAME
topic -> message
client.send('NAMES')
client.send('NAMES', channel='#foo-chan')
client.send('NAMES', channel=['#foo-chan', '#other'])
client.send('NAMES', channel=['#foo-chan', '#other'], target='remote.*.edu')
NAMES [<channel>] [<target>]
- MULTIPLE_VALUES
channel
- CONDITIONAL_OPTION
target
requireschannel
client.send('LIST')
client.send('LIST', channel='#foo-chan')
client.send('LIST', channel=['#foo-chan', '#other'])
client.send('LIST', channel=['#foo-chan', '#other'], target='remote.*.edu')
LIST [<channel>] [<target>]
- MULTIPLE_VALUES
channel
- CONDITIONAL_OPTION
target
requireschannel
client.send('INVITE', nick='WiZ-friend', channel='#bar-chan')
INVITE <nick> <channel>
- PARAM_RENAME
nickname -> nick
client.send('KICK', channel='#foo-chan', nick='WiZ')
client.send('KICK', channel='#foo-chan', nick='WiZ', message='Spamming')
client.send('KICK', channel='#foo-chan', nick=['WiZ', 'WiZ-friend'])
client.send('KICK', channel=['#foo', '#bar'], nick=['WiZ', 'WiZ-friend'])
KICK <channel> <nick> :[<message>]
- PARAM_RENAME
nickname -> nick
- PARAM_RENAME
comment -> message
- MULTIPLE_VALUES
channel
andnick
- If
nick
has n > 1 values, channel MUST have 1 or n values channel
can have n > 1 values IFFnick
has n values
client.send('PRIVMSG', target='WiZ-friend', message='Hello, friend!')
PRIVMSG <target> :<message>
- PARAM_RENAME
msgtarget -> target
- PARAM_RENAME
text to be sent -> message
client.send('NOTICE', target='#foo-chan', message='Maintenance in 5 mins')
NOTICE <target> :<message>
- PARAM_RENAME
msgtarget -> target
- PARAM_RENAME
text -> message
client.send('MOTD')
client.send('MOTD', target='remote.*.edu')
MOTD [<target>]
client.send('LUSERS')
client.send('LUSERS', mask='*.edu')
client.send('LUSERS', mask='*.edu', target='remote.*.edu')
LUSERS [<mask>] [<target>]
- CONDITIONAL_OPTION
target
requiresmask
client.send('VERSION')
VERSION [<target>]
client.send('STATS')
client.send('STATS', query='m')
client.send('STATS', query='m', target='remote.*.edu')
STATS [<query>] [<target>]
- CONDITIONAL_OPTION
target
requiresquery
client.send('LINKS')
client.send('LINKS', mask='*.bu.edu')
client.send('LINKS', remote='*.edu', mask='*.bu.edu')
LINKS [<remote>] [<mask>]
- PARAM_RENAME
remote server -> remote
- PARAM_RENAME
server mask -> mask
- CONDITIONAL_OPTION
remote
requiresmask
client.send('TIME')
client.send('TIME', target='remote.*.edu')
TIME [<target>]
client.send('CONNECT', target='tolsun.oulu.fi', port=6667)
client.send('CONNECT', target='tolsun.oulu.fi', port=6667, remote='*.edu')
CONNECT <target> <port> [<remote>]
- PARAM_RENAME
target server -> target
- PARAM_RENAME
remote server -> remote
client.send('TRACE')
client.send('TRACE', target='remote.*.edu')
TRACE [<target>]
client.send('ADMIN')
client.send('ADMIN', target='remote.*.edu')
ADMIN [<target>]
client.send('INFO')
client.send('INFO', target='remote.*.edu')
INFO [<target>]
client.send('SERVLIST', mask='*SERV')
client.send('SERVLIST', mask='*SERV', type=3)
SERVLIST [<mask>] [<type>]
- CONDITIONAL_OPTION
type
requiresmask
client.send('SQUERY', target='irchelp', message='HELP privmsg')
SQUERY <target> :<message>
- PARAM_RENAME
servicename -> target
- PARAM_RENAME
text -> message
client.send('WHO')
client.send('WHO', mask='*.fi')
client.send('WHO', mask='*.fi', o=True)
WHO [<mask>] ["o"]
- Optional positional parameter "o" is included if the kwarg "o" is Truthy
client.send('WHOIS', mask='*.fi')
client.send('WHOIS', mask=['*.fi', '*.edu'], target='remote.*.edu')
WHOIS <mask> [<target>]
- MULTIPLE_VALUES
mask
client.send('WHOWAS', nick='WiZ')
client.send('WHOWAS', nick='WiZ', count=10)
client.send('WHOWAS', nick=['WiZ', 'WiZ-friend'], count=10)
client.send('WHOWAS', nick='WiZ', count=10, target='remote.*.edu')
WHOWAS <nick> [<count>] [<target>]
- PARAM_RENAME
nickname -> nick
- MULTIPLE_VALUES
nick
- CONDITIONAL_OPTION
target
requirescount
client.send('KILL', nick='WiZ', message='Spamming Joins')
KILL <nick> :<message>
- PARAM_RENAME
nickname -> nick
- PARAM_RENAME
comment -> message
client.send('PING', message='Test..')
client.send('PING', server2='tolsun.oulu.fi')
client.send('PING', server1='WiZ', server2='tolsun.oulu.fi')
PING [<server1>] [<server2>] :[<message>]
- RFC_DELTA
server1
is optional - RFC_DELTA
message
is new, and optional - CONDITIONAL_OPTION
server2
requiresserver1
client.send('PONG', message='Test..')
client.send('PONG', server2='tolsun.oulu.fi')
client.send('PONG', server1='WiZ', server2='tolsun.oulu.fi')
PONG [<server1>] [<server2>] :[<message>]
- RFC_DELTA
server1
is optional - RFC_DELTA
message
is new, and optional - CONDITIONAL_OPTION
server2
requiresserver1
client.send('AWAY')
client.send('AWAY', message='Gone to Lunch')
AWAY :[<message>]
- PARAM_RENAME
text -> message
client.send('REHASH')
REHASH
client.send('DIE')
DIE
client.send('RESTART')
RESTART
client.send('SUMMON', nick='WiZ')
client.send('SUMMON', nick='WiZ', target='remote.*.edu')
client.send('SUMMON', nick='WiZ', target='remote.*.edu', channel='#foo-chan')
SUMMON <nick> [<target>] [<channel>]
- PARAM_RENAME
user -> nick
- CONDITIONAL_OPTION
channel
requirestarget
client.send('USERS')
client.send('USERS', target='remote.*.edu')
USERS [<target>]
client.send('WALLOPS', message='Maintenance in 5 minutes')
WALLOPS :<message>
- PARAM_RENAME
Text to be sent -> message
client.send('USERHOST', nick='WiZ')
client.send('USERHOST', nick=['WiZ', 'WiZ-friend'])
USERHOST <nick>
- PARAM_RENAME
nickname -> nick
- MULTIPLE_VALUES
nick
client.send('ISON', nick='WiZ')
client.send('ISON', nick=['WiZ', 'WiZ-friend'])
ISON <nick>
- PARAM_RENAME
nickname -> nick
- MULTIPLE_VALUES
nick
These commands are received from the server, or dispatched using Client.trigger(...)
.
For sending commands, see Supported Commands
above.
- CLIENT_CONNECT (host, port)
- CLIENT_DISCONNECT (host, port)
- PING (message)
- JOIN (nick, user, host, channel)
- PART (nick, user, host, channel, message)
- QUIT (nick, user, host, message)
- PRIVMSG (nick, user, host, target, message)
- NOTICE (nick, user, host, target, message)
- RPL_WELCOME (001) (message)
- RPL_YOURHOST (002) (message)
- RPL_CREATED (003) (message)
- RPL_MYINFO (004) (info, message)
- RPL_BOUNCE (005) (info, message)
- RPL_USERHOST (302) (message)
- RPL_NOTOPIC (331) (channel, message)
- RPL_TOPIC (332) (channel, message)
- RPL_ENDOFNAMES (366) (channel, message)
- RPL_MOTDSTART (375) (message)
- RPL_MOTD (372) (message)
- RPL_ENDOFMOTD (376) (message)
- RPL_LUSERCLIENT (251) (message)
- RPL_LUSERME (255) (message)
- RPL_LUSEROP (252) (count, message)
- RPL_LUSERUNKNOWN (253) (count, message)
- RPL_LUSERCHANNELS (254) (count, message)
- ERR_NOMOTD (422) (message)