Skip to content

Commit e51830c

Browse files
committed
Change quickstart
1 parent 8464071 commit e51830c

File tree

1 file changed

+131
-65
lines changed

1 file changed

+131
-65
lines changed

docs/getting-started/quickstart.rst

Lines changed: 131 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
Quickstart
55
###########
66

7+
Consider reading through the `GitHub Examples <https://github.com/PythonistaGuild/TwitchIO/tree/main/examples>`_
8+
79
This mini tutorial will serve as an entry point into TwitchIO 3. After it you should have a small working bot and a basic understanding of TwitchIO.
810

911
If you haven't already installed TwitchIO 3, please check :doc:`installing`.
@@ -26,16 +28,15 @@ Since TwitchIO 3 is fully asynchronous we will be using `asqlite` as our library
2628
2729
pip install -U git+https://github.com/Rapptz/asqlite.git
2830
31+
2932
Before running the code below, there just a couple more steps we need to take.
3033

3134
#. Create a new Twitch account. This will be the dedicated bot account.
32-
#. Enter your CLIENT_ID, CLIENT_SECRET, BOT_ID and OWNER_ID into the placeholders in the below example.
33-
#. Comment out everything in ``setup_hook``.
34-
#. Run the bot.
35-
#. Open a new browser / incognito mode, log in as the bot account and visit http://localhost:4343/oauth?scopes=user:read:chat%20user:write:chat%20user:bot
36-
#. In your main browser whilst logged in as your account, visit http://localhost:4343/oauth?scopes=channel:bot
37-
#. Stop the bot and uncomment everything in ``setup_hook``.
38-
#. Start the bot.
35+
#. Enter your CLIENT_ID, CLIENT_SECRET, BOT_ID and OWNER_ID into the placeholders in the below example. See :ref:`faqs` on how to retrieve the ``BOT_ID`` and ``OWNER_ID``.
36+
#. Run and start the bot from the code below.
37+
#. Open a new browser / incognito mode, log in as the ``BOT ACCOUNT`` and visit http://localhost:4343/oauth?scopes=user:read:chat%20user:write:chat%20user:bot&force_verify=true
38+
#. In your main browser whilst logged in as ``YOUR ACCOUNT``, visit http://localhost:4343/oauth?scopes=channel:bot&force_verify=true
39+
#. You can now use chat commands in your channel!
3940

4041
.. note::
4142
If you are unsure how to get the user IDs for BOT_ID and OWNER_ID, please check :ref:`bot-id-owner-id`
@@ -44,48 +45,75 @@ Before running the code below, there just a couple more steps we need to take.
4445

4546
.. code:: python3
4647
48+
"""An example of connecting to a conduit and subscribing to EventSub when a User Authorizes the application.
49+
50+
This bot can be restarted as many times without needing to subscribe or worry about tokens:
51+
- Tokens are stored in '.tio.tokens.json' by default
52+
- Subscriptions last 72 hours after the bot is disconnected and refresh when the bot starts.
53+
54+
Consider reading through the documentation for AutoBot for more in depth explanations.
55+
"""
56+
4757
import asyncio
4858
import logging
49-
import sqlite3
59+
import random
60+
from typing import TYPE_CHECKING
5061
5162
import asqlite
63+
5264
import twitchio
53-
from twitchio.ext import commands
5465
from twitchio import eventsub
66+
from twitchio.ext import commands
67+
68+
69+
if TYPE_CHECKING:
70+
import sqlite3
5571
5672
5773
LOGGER: logging.Logger = logging.getLogger("Bot")
5874
59-
CLIENT_ID: str = "..." # The CLIENT ID from the Twitch Dev Console
60-
CLIENT_SECRET: str = "..." # The CLIENT SECRET from the Twitch Dev Console
75+
# Consider using a .env or another form of Configuration file!
76+
CLIENT_ID: str = "..." # The CLIENT ID from the Twitch Dev Console
77+
CLIENT_SECRET: str = "..." # The CLIENT SECRET from the Twitch Dev Console
6178
BOT_ID = "..." # The Account ID of the bot user...
6279
OWNER_ID = "..." # Your personal User ID..
6380
6481
65-
class Bot(commands.Bot):
66-
def __init__(self, *, token_database: asqlite.Pool) -> None:
82+
class Bot(commands.AutoBot):
83+
def __init__(self, *, token_database: asqlite.Pool, subs: list[eventsub.SubscriptionPayload]) -> None:
6784
self.token_database = token_database
85+
6886
super().__init__(
6987
client_id=CLIENT_ID,
7088
client_secret=CLIENT_SECRET,
7189
bot_id=BOT_ID,
7290
owner_id=OWNER_ID,
7391
prefix="!",
92+
subscriptions=subs,
7493
)
7594
7695
async def setup_hook(self) -> None:
7796
# Add our component which contains our commands...
7897
await self.add_component(MyComponent(self))
7998
80-
# Subscribe to read chat (event_message) from our channel as the bot...
81-
# This creates and opens a websocket to Twitch EventSub...
82-
subscription = eventsub.ChatMessageSubscription(broadcaster_user_id=OWNER_ID, user_id=BOT_ID)
83-
await self.subscribe_websocket(payload=subscription)
99+
async def event_oauth_authorized(self, payload: twitchio.authentication.UserTokenPayload) -> None:
100+
await self.add_token(payload.access_token, payload.refresh_token)
101+
102+
if not payload.user_id:
103+
return
84104
85-
# Subscribe and listen to when a stream goes live..
86-
# For this example listen to our own stream...
87-
subscription = eventsub.StreamOnlineSubscription(broadcaster_user_id=OWNER_ID)
88-
await self.subscribe_websocket(payload=subscription)
105+
if payload.user_id == self.bot_id:
106+
# We usually don't want subscribe to events on the bots channel...
107+
return
108+
109+
# A list of subscriptions we would like to make to the newly authorized channel...
110+
subs: list[eventsub.SubscriptionPayload] = [
111+
eventsub.ChatMessageSubscription(broadcaster_user_id=payload.user_id, user_id=self.bot_id),
112+
]
113+
114+
resp: twitchio.MultiSubscribePayload = await self.multi_subscribe(subs)
115+
if resp.errors:
116+
LOGGER.warning("Failed to subscribe to: %r, for user: %s", resp.errors, payload.user_id)
89117
90118
async def add_token(self, token: str, refresh: str) -> twitchio.authentication.ValidateTokenPayload:
91119
# Make sure to call super() as it will add the tokens interally and return us some data...
@@ -96,7 +124,7 @@ Before running the code below, there just a couple more steps we need to take.
96124
INSERT INTO tokens (user_id, token, refresh)
97125
VALUES (?, ?, ?)
98126
ON CONFLICT(user_id)
99-
DO UPDATE SET
127+
DO UPDATE SET
100128
token = excluded.token,
101129
refresh = excluded.refresh;
102130
"""
@@ -107,43 +135,72 @@ Before running the code below, there just a couple more steps we need to take.
107135
LOGGER.info("Added token to the database for user: %s", resp.user_id)
108136
return resp
109137
110-
async def load_tokens(self, path: str | None = None) -> None:
111-
# We don't need to call this manually, it is called in .login() from .start() internally...
112-
113-
async with self.token_database.acquire() as connection:
114-
rows: list[sqlite3.Row] = await connection.fetchall("""SELECT * from tokens""")
115-
116-
for row in rows:
117-
await self.add_token(row["token"], row["refresh"])
118-
119-
async def setup_database(self) -> None:
120-
# Create our token table, if it doesn't exist..
121-
query = """CREATE TABLE IF NOT EXISTS tokens(user_id TEXT PRIMARY KEY, token TEXT NOT NULL, refresh TEXT NOT NULL)"""
122-
async with self.token_database.acquire() as connection:
123-
await connection.execute(query)
124-
125138
async def event_ready(self) -> None:
126139
LOGGER.info("Successfully logged in as: %s", self.bot_id)
127140
128141
129142
class MyComponent(commands.Component):
130-
def __init__(self, bot: Bot):
143+
# An example of a Component with some simple commands and listeners
144+
# You can use Components within modules for a more organized codebase and hot-reloading.
145+
146+
def __init__(self, bot: Bot) -> None:
131147
# Passing args is not required...
132148
# We pass bot here as an example...
133149
self.bot = bot
134-
150+
151+
# An example of listening to an event
135152
# We use a listener in our Component to display the messages received.
136153
@commands.Component.listener()
137154
async def event_message(self, payload: twitchio.ChatMessage) -> None:
138155
print(f"[{payload.broadcaster.name}] - {payload.chatter.name}: {payload.text}")
139156
140-
@commands.command(aliases=["hello", "howdy", "hey"])
157+
@commands.command()
141158
async def hi(self, ctx: commands.Context) -> None:
142-
"""Simple command that says hello!
159+
"""Command that replys to the invoker with Hi <name>!
143160
144-
!hi, !hello, !howdy, !hey
161+
!hi
145162
"""
146-
await ctx.reply(f"Hello {ctx.chatter.mention}!")
163+
await ctx.reply(f"Hi {ctx.chatter}!")
164+
165+
@commands.command()
166+
async def say(self, ctx: commands.Context, *, message: str) -> None:
167+
"""Command which repeats what the invoker sends.
168+
169+
!say <message>
170+
"""
171+
await ctx.send(message)
172+
173+
@commands.command()
174+
async def add(self, ctx: commands.Context, left: int, right: int) -> None:
175+
"""Command which adds to integers together.
176+
177+
!add <number> <number>
178+
"""
179+
await ctx.reply(f"{left} + {right} = {left + right}")
180+
181+
@commands.command()
182+
async def choice(self, ctx: commands.Context, *choices: str) -> None:
183+
"""Command which takes in an arbitrary amount of choices and randomly chooses one.
184+
185+
!choice <choice_1> <choice_2> <choice_3> ...
186+
"""
187+
await ctx.reply(f"You provided {len(choices)} choices, I choose: {random.choice(choices)}")
188+
189+
@commands.command(aliases=["thanks", "thank"])
190+
async def give(self, ctx: commands.Context, user: twitchio.User, amount: int, *, message: str | None = None) -> None:
191+
"""A more advanced example of a command which has makes use of the powerful argument parsing, argument converters and
192+
aliases.
193+
194+
The first argument will be attempted to be converted to a User.
195+
The second argument will be converted to an integer if possible.
196+
The third argument is optional and will consume the reast of the message.
197+
198+
!give <@user|user_name> <number> [message]
199+
!thank <@user|user_name> <number> [message]
200+
!thanks <@user|user_name> <number> [message]
201+
"""
202+
msg = f"with message: {message}" if message else ""
203+
await ctx.send(f"{ctx.chatter.mention} gave {amount} thanks to {user.mention} {msg}")
147204
148205
@commands.group(invoke_fallback=True)
149206
async def socials(self, ctx: commands.Context) -> None:
@@ -161,40 +218,49 @@ Before running the code below, there just a couple more steps we need to take.
161218
"""
162219
await ctx.send("discord.gg/...")
163220
164-
@commands.command(aliases=["repeat"])
165-
@commands.is_moderator()
166-
async def say(self, ctx: commands.Context, *, content: str) -> None:
167-
"""Moderator only command which repeats back what you say.
168221
169-
!say hello world, !repeat I am cool LUL
170-
"""
171-
await ctx.send(content)
222+
async def setup_database(db: asqlite.Pool) -> tuple[list[tuple[str, str]], list[eventsub.SubscriptionPayload]]:
223+
# Create our token table, if it doesn't exist..
224+
# You should add the created files to .gitignore or potentially store them somewhere safer
225+
# This is just for example purposes...
172226
173-
@commands.Component.listener()
174-
async def event_stream_online(self, payload: twitchio.StreamOnline) -> None:
175-
# Event dispatched when a user goes live from the subscription we made above...
176-
177-
# Keep in mind we are assuming this is for ourselves
178-
# others may not want your bot randomly sending messages...
179-
await payload.broadcaster.send_message(
180-
sender=self.bot.bot_id,
181-
message=f"Hi... {payload.broadcaster}! You are live!",
182-
)
227+
query = """CREATE TABLE IF NOT EXISTS tokens(user_id TEXT PRIMARY KEY, token TEXT NOT NULL, refresh TEXT NOT NULL)"""
228+
async with db.acquire() as connection:
229+
await connection.execute(query)
230+
231+
# Fetch any existing tokens...
232+
rows: list[sqlite3.Row] = await connection.fetchall("""SELECT * from tokens""")
183233
234+
tokens: list[tuple[str, str]] = []
235+
subs: list[eventsub.SubscriptionPayload] = []
184236
237+
for row in rows:
238+
tokens.append((row["token"], row["refresh"]))
239+
subs.extend([eventsub.ChatMessageSubscription(broadcaster_user_id=row["user_id"], user_id=BOT_ID)])
240+
241+
return tokens, subs
242+
243+
244+
# Our main entry point for our Bot
245+
# Best to setup_logging here, before anything starts
185246
def main() -> None:
186247
twitchio.utils.setup_logging(level=logging.INFO)
187248
188249
async def runner() -> None:
189-
async with asqlite.create_pool("tokens.db") as tdb, Bot(token_database=tdb) as bot:
190-
await bot.setup_database()
191-
await bot.start()
250+
async with asqlite.create_pool("tokens.db") as tdb:
251+
tokens, subs = await setup_database(tdb)
252+
253+
async with Bot(token_database=tdb, subs=subs) as bot:
254+
for pair in tokens:
255+
await bot.add_token(*pair)
256+
257+
await bot.start(load_tokens=False)
192258
193259
try:
194260
asyncio.run(runner())
195261
except KeyboardInterrupt:
196-
LOGGER.warning("Shutting down due to KeyboardInterrupt...")
262+
LOGGER.warning("Shutting down due to KeyboardInterrupt")
197263
198264
199265
if __name__ == "__main__":
200-
main()
266+
main()

0 commit comments

Comments
 (0)