1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
|
.. Copyright (C) 2016, 2021 taylor.fish <contact@taylor.fish>
.. This file is part of pyrcb2-docs, documentation for pyrcb2.
.. pyrcb2-docs is licensed under the GNU Lesser General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
.. As an additional permission under GNU GPL version 3 section 7, you
may distribute non-source forms of pyrcb2-docs without the copy of
the GNU GPL normally required by section 4, provided you include a
URL through which recipients can obtain a copy of the Corresponding
Source and the GPL at no charge.
.. pyrcb2-docs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
.. You should have received a copy of the GNU Lesser General Public License
along with pyrcb2-docs. If not, see <http://www.gnu.org/licenses/>.
.. currentmodule:: pyrcb2
Custom commands
===============
Sending
-------
Sending custom commands is straightforward---simply use `IRCBot.send_command`::
bot.send_command("INVITE", "nickname", "#channel")
Waiting for responses
~~~~~~~~~~~~~~~~~~~~~
If you send a command that expects a response, you can asynchronously wait for
the response. If you want to do this, it's recommended to create a method that
sends the command and returns a coroutine that waits for the response.
Use :meth:`IRCBot.wait_for` to wait for the expected messages and possible
errors.
Let's create a method for ``INVITE`` commands::
class MyBot:
# Initialization code here...
# self.bot is an IRCBot object.
def invite(self, nickname, channel):
future = self.bot.send_command("INVITE", nickname, channel)
return self.wait_for(
future,
Reply("RPL_INVITING", nickname, channel),
errors=[
Error("ERR_NOSUCHNICK", nickname, ANY),
Error("ERR_USERONCHANNEL", nickname, channel, ANY),
Error([
"ERR_USERONCHANNEL", "ERR_CHANOPRIVSNEEDED",
], channel, ANY),
]
)
:meth:`IRCBot.wait_for` returns an `OptionalCoroutine`, so the ``invite()``
method above can be called with or without being awaited.
**Note**: pyrcb2 automatically delays large volumes of messages to prevent
server throttling or disconnecting. :meth:`IRCBot.send_command` returns a
future that blocks until the message has actually been sent---*be sure to pass
this to* :meth:`IRCBot.wait_for` *so that timeouts are accurate*. The timeout
counter shouldn't start until the message has really been sent.
If you want to do some post-processing after the response is received (for
example, setting the ``value`` attribute of the `WaitResult` returned by
:meth:`IRCBot.wait_for`), you can put your asynchronous code in an inner
coroutine function and return it in an `OptionalCoroutine`::
class MyBot:
# Initialization code here...
def invite(self, nickname, channel):
future = self.bot.send_command("INVITE", nickname, channel)
async def coroutine():
# We can await `future` here instead of
# passing it to wait_for().
await future
# Same as the call to wait_for() above, except
# without the `future` argument.
result = self.bot.wait_for(...)
# Any post-processing can go here.
# For example, setting the `value` attribute.
if result.success:
result.value = {"invited_at": time.time()}
return result
# Return an optional coroutine.
return OptionalCoroutine(coroutine)
If you're writing a method that must be awaited, you can skip the inner
function and simply make the method a coroutine::
class MyBot:
# Initialization code here...
async def invite(self, nickname, channel):
await self.bot.send_command("INVITE", nickname, channel)
result = await self.bot.wait_for(...)
# Can do post-processing here; no need
# to use an inner function.
return result
Handling
--------
To add an event handler for an arbitrary IRC command, use
:func:`@Event.command <Event.command>`::
@Event.command("INVITE")
async def on_invite(sender, target: IStr, channel: IStr):
# Code here...
(Remember to include the ``self`` parameter if your event handler is a method.)
When using :func:`@Event.command <Event.command>` (or :func:`@Event.reply
<Event.reply>`), it is important to note that only the first parameter
(``sender``) is a case-insensitive string---other parameters must be converted
to `IStr` if they represent channels, nicknames, or other case-insensitive
values. As explained in :func:`@Event.command <Event.command>`, you can either
convert parameters manually or use parameter annotations like the example
above.
You can use :func:`@Event.reply <Event.reply>` to handle numeric replies::
@Event.reply("RPL_TOPIC")
async def on_rpl_topic(sender, target, channel, topic):
# Code here...
.. _custom-events:
Custom events
~~~~~~~~~~~~~
You can also add custom events that you call manually with :meth:`IRCBot.call`.
To do this, subclass `Event` and do one of two things:
Set the class attribute ``events`` to a list of the events you want to add. An
event decorator will be added for each string in the list, with both its name
and the event ID it uses equal to the string. ::
class Event(Event):
events = ["self_invited"]
class MyBot:
# Initialization code here...
# self.bot is an IRCBot object.
@Event.self_invited
async def on_self_invited(self, sender, channel):
print(sender, "has invited this bot to", channel)
@Event.command("INVITE")
async def on_invite(self, sender, target: IStr, channel: IStr):
if target == self.bot.nickname:
# Make sure `Event` is the subclass above.
await self.bot.call(
Event, "self_invited", sender, channel,
)
Or, to have control over the event ID your event decorator uses, use
:func:`@event_decorator <event_decorator>`. This also allows you to add
parameters to your decorator. ::
class Event(Event):
@event_decorator
def user_invited(nickname):
# Return an (event_class, event_id) tuple.
return (Event, ("user_invited", nickname))
class MyBot:
# Initialization code here...
# self.bot is an IRCBot object.
@Event.user_invited("user1")
async def on_user1_invited(self, sender, channel):
print(sender, "has invited user1 to", channel)
@Event.user_invited("user2")
async def on_user2_invited(self, sender, channel):
print(sender, "has invited user2 to", channel)
@Event.command("INVITE")
async def on_invite(self, sender, target: IStr, channel: IStr):
# Make sure `Event` is the subclass above.
await self.bot.call(
Event, ("user_invited", target), sender, channel,
)
Example
-------
Here is an example bot that sends and handles custom commands::
class MyBot:
def __init__(self):
self.bot = IRCBot()
self.bot.load_events(self)
@Event.command("INVITE")
async def on_invite(self, sender, target: IStr, channel: IStr):
print(sender, "has invited", target, "to", channel)
@Event.privmsg
async def on_privmsg(self, sender, channel, message):
if channel is None and message == "!invite":
self.bot.send_command("INVITE", sender, "#channel")
async def run(self):
with self.bot:
await self.bot.connect("irc.example.com", 6667)
await self.bot.register("mybot")
self.bot.join("#channel")
await self.bot.listen()
mybot = MyBot()
await mybot.run()
|