Skip to content

Commit 786b057

Browse files
committed
Add module example
1 parent 2a53b43 commit 786b057

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import twitchio
2+
from twitchio.ext import commands
3+
4+
class MyComponent(commands.Component):
5+
def __init__(self, bot: commands.Bot) -> None:
6+
# Passing args is not required...
7+
# We pass bot here as an example...
8+
self.bot = bot
9+
10+
@commands.command(aliases=["hello", "howdy", "hey"])
11+
async def hi(self, ctx: commands.Context) -> None:
12+
"""Simple command that says hello!
13+
14+
!hi, !hello, !howdy, !hey
15+
"""
16+
await ctx.reply(f"Hello {ctx.chatter.mention}!")
17+
18+
@commands.group(invoke_fallback=True)
19+
async def socials(self, ctx: commands.Context) -> None:
20+
"""Group command for our social links.
21+
22+
!socials
23+
"""
24+
await ctx.send("discord.gg/..., youtube.com/..., twitch.tv/...")
25+
26+
@socials.command(name="discord")
27+
async def socials_discord(self, ctx: commands.Context) -> None:
28+
"""Sub command of socials that sends only our discord invite.
29+
30+
!socials discord
31+
"""
32+
await ctx.send("discord.gg/...")
33+
34+
@commands.command(aliases=["repeat"])
35+
@commands.is_moderator()
36+
async def say(self, ctx: commands.Context, *, content: str) -> None:
37+
"""Moderator only command which repeats back what you say.
38+
39+
!say hello world, !repeat I am cool LUL
40+
"""
41+
await ctx.send(content)
42+
43+
@commands.Component.listener()
44+
async def event_stream_online(self, payload: twitchio.StreamOnline) -> None:
45+
# Event dispatched when a user goes live from the subscription we made above...
46+
47+
# Keep in mind we are assuming this is for ourselves
48+
# others may not want your bot randomly sending messages...
49+
await payload.broadcaster.send_message(
50+
sender=self.bot.bot_id,
51+
message=f"Hi... {payload.broadcaster}! You are live!!!",
52+
)
53+
54+
# This is our entry point for the module.
55+
async def setup(bot: commands.Bot) -> None:
56+
await bot.add_component(MyComponent(bot))
57+
58+
59+
# This is an optional teardown coroutine for miscellaneous clean-up if necessary.
60+
async def teardown(bot: commands.Bot) -> None:
61+
...
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from twitchio.ext import commands
2+
3+
# Custom Exception for our component guard.
4+
class NotOwnerError(commands.GuardFailure): ...
5+
6+
class OwnerCmds(commands.Component):
7+
def __init__(self, bot: commands.Bot) -> None:
8+
# Passing args is not required...
9+
# We pass bot here as an example...
10+
self.bot = bot
11+
12+
13+
async def component_command_error(self, payload: commands.CommandErrorPayload) -> bool | None:
14+
error = payload.exception
15+
if isinstance(error, NotOwnerError):
16+
ctx = payload.context
17+
18+
await ctx.reply("Only the owner can use this command!")
19+
20+
# This explicit False return stops the error from being dispatched anywhere else...
21+
return False
22+
23+
# Restrict all of the commands in this component to the owner.
24+
@commands.Component.guard()
25+
def is_owner(self, ctx: commands.Context) -> bool:
26+
if ctx.chatter.id != self.bot.owner_id:
27+
raise NotOwnerError
28+
29+
return True
30+
31+
# Manually load the cmds module.
32+
@commands.command()
33+
async def load_cmds(self, ctx: commands.Context) -> None:
34+
await self.bot.load_module("components.cmds")
35+
36+
# Manually unload the cmds module.
37+
@commands.command()
38+
async def unload_cmds(self, ctx: commands.Context) -> None:
39+
await self.bot.unload_module("components.cmds")
40+
41+
# Hot reload the cmds module atomically.
42+
@commands.command()
43+
async def reload_cmds(self, ctx: commands.Context) -> None:
44+
await self.bot.reload_module("components.cmds")
45+
46+
# Check which modules are loaded.
47+
@commands.command()
48+
async def loaded_modules(self, ctx: commands.Context) -> None:
49+
print(self.bot.modules)
50+
51+
# This is our entry point for the module.
52+
async def setup(bot: commands.Bot) -> None:
53+
await bot.add_component(OwnerCmds(bot))
54+
55+
# This is an optional teardown coroutine for miscellaneous clean-up if necessary.
56+
async def teardown(bot: commands.Bot) -> None:
57+
...
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import asyncio
2+
import logging
3+
from typing import TYPE_CHECKING
4+
5+
import asqlite
6+
7+
import twitchio
8+
from twitchio import eventsub
9+
from twitchio.ext import commands
10+
11+
12+
if TYPE_CHECKING:
13+
import sqlite3
14+
15+
16+
LOGGER: logging.Logger = logging.getLogger("Bot")
17+
18+
# Simple example for TwitchIO V3 Alpha...
19+
# Instructions:
20+
21+
# You need to install: https://github.com/Rapptz/asqlite
22+
# pip install -U git+https://github.com/Rapptz/asqlite.git
23+
24+
# 1.) Comment out lines: 54-60 (The subscriptions)
25+
# 2.) Add the Twitch Developer Console and Create an Application
26+
# 3.) Add: http://localhost:4343/oauth/callback as the callback URL
27+
# 4.) Enter your CLIENT_ID, CLIENT_SECRET, BOT_ID and OWNER_ID
28+
# 5.) Run the bot.
29+
# 6.) Logged in the bots user account, visit: http://localhost:4343/oauth?scopes=user:read:chat%20user:write:chat%20user:bot
30+
# 7.) Logged in as your personal user account, visit: http://localhost:4343/oauth?scopes=channel:bot
31+
# 8.) Uncomment lines: 54-60 (The subscriptions)
32+
# 9.) Restart the bot.
33+
# You only have to do the above once for this example.
34+
35+
36+
CLIENT_ID: str = "..."
37+
CLIENT_SECRET: str = "..."
38+
BOT_ID = "..."
39+
OWNER_ID = "..."
40+
41+
42+
class Bot(commands.Bot):
43+
def __init__(self, *, token_database: asqlite.Pool) -> None:
44+
self.token_database = token_database
45+
super().__init__(
46+
client_id=CLIENT_ID,
47+
client_secret=CLIENT_SECRET,
48+
bot_id=BOT_ID,
49+
owner_id=OWNER_ID,
50+
prefix="!",
51+
)
52+
53+
async def setup_hook(self) -> None:
54+
# Subscribe to read chat (event_message) from our channel as the bot...
55+
# This creates and opens a websocket to Twitch EventSub...
56+
subscription = eventsub.ChatMessageSubscription(broadcaster_user_id=OWNER_ID, user_id=BOT_ID)
57+
await self.subscribe_websocket(payload=subscription)
58+
59+
# Subscribe and listen to when a stream goes live..
60+
# For this example listen to our own stream...
61+
subscription = eventsub.StreamOnlineSubscription(broadcaster_user_id=OWNER_ID)
62+
await self.subscribe_websocket(payload=subscription)
63+
64+
# Load the module that contains our component, commands, and listeners.
65+
# Modules can have multiple components.
66+
await self.load_module("components.owner_cmds")
67+
await self.load_module("components.cmds")
68+
69+
async def add_token(self, token: str, refresh: str) -> twitchio.authentication.ValidateTokenPayload:
70+
# Make sure to call super() as it will add the tokens interally and return us some data...
71+
resp: twitchio.authentication.ValidateTokenPayload = await super().add_token(token, refresh)
72+
73+
# Store our tokens in a simle SQLite Database when they are authorized...
74+
query = """
75+
INSERT INTO tokens (user_id, token, refresh)
76+
VALUES (?, ?, ?)
77+
ON CONFLICT(user_id)
78+
DO UPDATE SET
79+
token = excluded.token,
80+
refresh = excluded.refresh;
81+
"""
82+
83+
async with self.token_database.acquire() as connection:
84+
await connection.execute(query, (resp.user_id, token, refresh))
85+
86+
LOGGER.info("Added token to the database for user: %s", resp.user_id)
87+
return resp
88+
89+
async def load_tokens(self, path: str | None = None) -> None:
90+
# We don't need to call this manually, it is called in .login() from .start() internally...
91+
92+
async with self.token_database.acquire() as connection:
93+
rows: list[sqlite3.Row] = await connection.fetchall("""SELECT * from tokens""")
94+
95+
for row in rows:
96+
await self.add_token(row["token"], row["refresh"])
97+
98+
async def setup_database(self) -> None:
99+
# Create our token table, if it doesn't exist..
100+
query = """CREATE TABLE IF NOT EXISTS tokens(user_id TEXT PRIMARY KEY, token TEXT NOT NULL, refresh TEXT NOT NULL)"""
101+
async with self.token_database.acquire() as connection:
102+
await connection.execute(query)
103+
104+
async def event_ready(self) -> None:
105+
LOGGER.info("Successfully logged in as: %s", self.bot_id)
106+
107+
108+
109+
def main() -> None:
110+
twitchio.utils.setup_logging(level=logging.INFO)
111+
112+
async def runner() -> None:
113+
async with asqlite.create_pool("tokens.db") as tdb, Bot(token_database=tdb) as bot:
114+
await bot.setup_database()
115+
await bot.start()
116+
117+
try:
118+
asyncio.run(runner())
119+
except KeyboardInterrupt:
120+
LOGGER.warning("Shutting down due to KeyboardInterrupt...")
121+
122+
123+
if __name__ == "__main__":
124+
main()

0 commit comments

Comments
 (0)