Skip to content

Commit ee1d8a1

Browse files
author
Eviee Py
committed
Add various features to commands ext including guards and docs.
1 parent dd13832 commit ee1d8a1

File tree

5 files changed

+1199
-60
lines changed

5 files changed

+1199
-60
lines changed

twitchio/ext/commands/bot.py

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939
from models.eventsub_ import ChatMessage
4040
from types_.options import Prefix_T
4141

42+
from twitchio.eventsub.subscriptions import SubscriptionPayload
43+
from twitchio.types_.eventsub import SubscriptionResponse
4244
from twitchio.types_.options import ClientOptions
45+
from twitchio.user import PartialUser
4346

4447
from .components import Component
4548

@@ -48,6 +51,102 @@
4851

4952

5053
class Bot(Mixin[None], Client):
54+
"""The TwitchIO ``commands.Bot`` class.
55+
56+
The Bot is an extension of and inherits from :class:`twitchio.Client` and comes with additonal powerful features for
57+
creating and managing bots on Twitch.
58+
59+
Unlike :class:`twitchio.Client`, the :class:`~.Bot` class allows you to easily make use of built-in the commands ext.
60+
61+
The easiest way of creating and using a bot is via subclassing, some examples are provided below.
62+
63+
.. note::
64+
65+
Any examples contained in this class which use ``twitchio.Client`` can be changed to ``commands.Bot``.
66+
67+
68+
Parameters
69+
----------
70+
client_id: str
71+
The client ID of the application you registered on the Twitch Developer Portal.
72+
client_secret: str
73+
The client secret of the application you registered on the Twitch Developer Portal.
74+
This must be associated with the same ``client_id``.
75+
bot_id: str
76+
The User ID associated with the Bot Account.
77+
Unlike on :class:`~twitchio.Client` this is a required argument on :class:`~.Bot`.
78+
owner_id: str | None
79+
An optional ``str`` which is the User ID associated with the owner of this bot. This should be set to your own user
80+
accounts ID, but is not required. Defaults to ``None``.
81+
prefix: str | Iterabale[str] | Coroutine[Any, Any, str | Iterable[str]]
82+
The prefix(es) to listen to, to determine whether a message should be treated as a possible command.
83+
84+
This can be a ``str``, an iterable of ``str`` or a coroutine which returns either.
85+
86+
This is a required argument, common prefixes include: ``"!"`` or ``"?"``.
87+
88+
Example
89+
-------
90+
91+
.. code:: python3
92+
93+
import asyncio
94+
import logging
95+
96+
import twitchio
97+
from twitchio import eventsub
98+
from twitchio.ext import commands
99+
100+
LOGGER: logging.Logger = logging.getLogger("Bot")
101+
102+
class Bot(commands.Bot):
103+
104+
def __init__(self) -> None:
105+
super().__init__(client_id="...", client_secret="...", bot_id="...", owner_id="...", prefix="!")
106+
107+
# Do some async setup, as an example we will load a component and subscribe to some events...
108+
# Passing the bot to the component is completely optional...
109+
async def setup_hook(self) -> None:
110+
111+
# Listen for messages on our channel...
112+
# You need appropriate scopes, see the docs on authenticating for more info...
113+
payload = eventsub.ChatMessageSubscription(broadcaster_user_id=self.owner_id, user_id=self.bot_id)
114+
await self.subscribe_websocket(payload=payload)
115+
116+
await self.add_component(SimpleCommands(self))
117+
LOGGER.info("Finished setup hook!")
118+
119+
class SimpleCommands(commands.Component):
120+
121+
def __init__(self, bot: Bot) -> None:
122+
self.bot = bot
123+
124+
@commands.command()
125+
async def hi(self, ctx: commands.Context) -> None:
126+
'''Command which sends you a hello.'''
127+
await ctx.reply(f"Hello {ctx.chatter}!")
128+
129+
@commands.command()
130+
async def say(self, ctx: commands.Context, *, message: str) -> None:
131+
'''Command which repeats what you say: !say I am an apple...'''
132+
await ctx.send(message)
133+
134+
def main() -> None:
135+
# Setup logging, this is optional, however a nice to have...
136+
twitchio.utils.setup_logging(level=logging.INFO)
137+
138+
async def runner() -> None:
139+
async with Bot() as bot:
140+
await bot.start()
141+
142+
try:
143+
asyncio.run(runner())
144+
except KeyboardInterrupt:
145+
LOGGER.warning("Shutting down due to Keyboard Interrupt...")
146+
147+
main()
148+
"""
149+
51150
def __init__(
52151
self,
53152
*,
@@ -74,7 +173,7 @@ def __init__(
74173
def bot_id(self) -> str:
75174
"""Property returning the ID of the bot.
76175
77-
This **MUST** be set via the keyword argument ``bot_id="..."`` in the constructor of this class.
176+
You must ensure you set this via the keyword argument ``bot_id="..."`` in the constructor of this class.
78177
79178
Returns
80179
-------
@@ -199,7 +298,7 @@ async def invoke(self, ctx: Context) -> None:
199298
payload = CommandErrorPayload(context=ctx, exception=e)
200299
self.dispatch("command_error", payload=payload)
201300

202-
async def event_channel_chat_message(self, payload: ChatMessage) -> None:
301+
async def event_message(self, payload: ChatMessage) -> None:
203302
if payload.chatter.id == self.bot_id:
204303
return
205304

@@ -286,4 +385,67 @@ async def after_invoke(self, ctx: Context) -> None:
286385
The context associated with command invocation, after being passed through the command.
287386
"""
288387

289-
async def guard(self, ctx: Context) -> None: ...
388+
async def global_guard(self, ctx: Context, /) -> bool:
389+
"""|coro|
390+
391+
A global guard applied to all commmands added to the bot.
392+
393+
This coroutine function should take in one parameter :class:`~.commands.Context` the context surrounding
394+
command invocation, and return a bool indicating whether a command should be allowed to run.
395+
396+
If this function returns ``False``, the chatter will not be able to invoke the command and an error will be
397+
raised. If this function returns ``True`` the chatter will be able to invoke the command,
398+
assuming all the other guards also pass their predicate checks.
399+
400+
See: :func:`~.commands.guard` for more information on guards, what they do and how to use them.
401+
402+
.. note::
403+
404+
This is the first guard to run, and is applied to every command.
405+
406+
.. important::
407+
408+
Unlike command specific guards or :meth:`.commands.Component.guard`, this function must
409+
be always be a coroutine.
410+
411+
412+
This coroutine is intended to be overriden when needed and by default always returns ``True``.
413+
414+
Parameters
415+
----------
416+
ctx: commands.Context
417+
The context associated with command invocation.
418+
419+
Raises
420+
------
421+
GuardFailure
422+
The guard predicate returned ``False`` and prevented the chatter from using the command.
423+
"""
424+
return True
425+
426+
async def subscribe_webhook(
427+
self,
428+
*,
429+
payload: SubscriptionPayload,
430+
as_bot: bool = True,
431+
token_for: str | PartialUser | None,
432+
callback_url: str | None = None,
433+
eventsub_secret: str | None = None,
434+
) -> SubscriptionResponse | None:
435+
return await super().subscribe_webhook(
436+
payload=payload,
437+
as_bot=as_bot,
438+
token_for=token_for,
439+
callback_url=callback_url,
440+
eventsub_secret=eventsub_secret,
441+
)
442+
443+
async def subscribe_websocket(
444+
self,
445+
*,
446+
payload: SubscriptionPayload,
447+
as_bot: bool = True,
448+
token_for: str | PartialUser | None = None,
449+
socket_id: str | None = None,
450+
) -> SubscriptionResponse | None:
451+
return await super().subscribe_websocket(payload=payload, as_bot=as_bot, token_for=token_for, socket_id=socket_id)

0 commit comments

Comments
 (0)