DEV Community

Cover image for Let's make a Twitch bot with Python!
πŸ’Ύ bun9000
πŸ’Ύ bun9000

Posted on • Edited on

Let's make a Twitch bot with Python!

This tutorial gets you up and running with a simple chat bot for Twitch channel.

Who's this tutorial for?

Beginners to coding and experienced coders new to Python.

Contents

We'll start out by setting up the accounts, getting the secrets, and installing the softywares. Then we'll setup and code the bot. By the end of this tutorial you should be ready to start adding your own custom commands.

BUT FIRST... we need to make sure we have our credentials in order. πŸ‘

Papers, please!

πŸ“’ Glory to Arstotzka!

  1. Make an account on Twitch (for your bot) or use the one you stream with. Make it something cool like RealStreamer69 😎
  2. Request an oauth code. You'll need to login and give the app permissions to generate it for you.
  3. Register your app with Twitch dev and request a client-id (so you can interface with Twitch's API)

Keep the oauth and client id somewhere handy but not public. You'll need them later to configure the bot.

πŸ’‘ PROTIPβ„’ -- Keep it secret. Keep it safe.

Install all the things! 🧹

  1. Install Python 3.6 or 3.7 -- Windows // Linux // OS X
  2. Install PIPENV. In the console, run β‡’ pip install pipenv

Create a cozy home for the bot to live in

Virtual environments require a couple extra steps to set up but make developing Python apps a breeze. For this tutorial, we'll use PIPENV which marries pip and venv into a single package.

  • In the console, navigate to your working directory
  • Run β‡’ pipenv --python 3.6 or pipenv --python 3.7
    • This is going to create pipfile and piplock. They hold venv info like Python version and libraries you install.
  • Then run β‡’ pipenv install twitchio

Configuring and authorizing the bot

Create 2 files in your working directory. One called bot.py and another called .env (no file name, just the extension - it's weird, I know).

/.env

Your secrets will live inside the .env file. Add the oauth token and client-id from above after the = in the file. Fill in the other vars as well.



# .env
TMI_TOKEN=oauth:
CLIENT_ID=
BOT_NICK=
BOT_PREFIX=!
CHANNEL=


Enter fullscreen mode Exit fullscreen mode

/bot.py

Inside bot.py, import the libraries we'll need and create the bot obj that we'll start in the next step.



# bot.py
import os # for importing env vars for the bot to use
from twitchio.ext import commands

bot = commands.Bot(
    # set up the bot
    irc_token=os.environ['TMI_TOKEN'],
    client_id=os.environ['CLIENT_ID'],
    nick=os.environ['BOT_NICK'],
    prefix=os.environ['BOT_PREFIX'],
    initial_channels=[os.environ['CHANNEL']]
)


Enter fullscreen mode Exit fullscreen mode

πŸ’‘ PROTIPβ„’ -- When we run bot.py using PIPENV, it first loads the variables from the .env file into the virtual environment and then runs bot.py. So, inside this venv, we have acess (on an instance-basis) to these variables. We're going to use python's os module to import them, just like we would import environment variables on our native environment.

At the bottom of the file, we need to make sure the bot runs when we call bot.py directly using if __name__ == "__main__":



# bot.py
if __name__ == "__main__":
    bot.run()


Enter fullscreen mode Exit fullscreen mode

WAKE BOT UP (wake bot up inside!)

Let's test the bot and make sure it can connect to Twitch.

  • In the console, run β‡’ pipenv run python bot.py

If it worked, you shouldn't get any errors - that means the environment variables loaded correctly and your bot successfully connected to Twitch!

If you got errors, check out the next section before moving on.

Error: Can't wake up. [save me]

A wild Request to join the channel has timed out. Make sure the channel exists. appears. You evade with..

  1. Make sure you have the right tokens (oauth and client-id) in the .env file and that they're in the same directory/folder as bot.py

  2. Your directory structure at this point should look like this...

    working-directory/
    β”œβ”€ .env
    β”œβ”€ bot.py
    β”œβ”€ Pipfile
    └─ Pipfile.lock
    
  3. If that still doesn't fix it, comment below and we'll sort it out for ya!

Adding some functionality to the bot

Greet the chat room!

Back to bot.py.... Below the bot object, let's create a function called event_ready with the decorator @bot.event. This function will run once when we start the bot. It then reports to terminal and chat that it successfully connected to Twitch.



# bot.py, below bot object
@bot.event
async def event_ready():
    'Called once when the bot goes online.'
    print(f"{os.environ['BOT_NICK']} is online!")
    ws = bot._ws  # this is only needed to send messages within event_ready
    await ws.send_privmsg(os.environ['CHANNEL'], f"/me has landed!")


Enter fullscreen mode Exit fullscreen mode

Go ahead and test the bot again. It should greet chat when it comes online now.

landed

Respond to messages in chat

Next up, we're going to add a function that's run every time a message is sent in your channel. You can add all sorts of logic here later, but we'll start out with making sure the bot ignores itself.



# bot.py, below event_ready
@bot.event
async def event_message(ctx):
    'Runs every time a message is sent in chat.'

    # make sure the bot ignores itself and the streamer
    if ctx.author.name.lower() == os.environ['BOT_NICK'].lower():
        return


Enter fullscreen mode Exit fullscreen mode

After that, we'll drop in a line of code that will annoyingly echo back every message sent in chat. ᴷᡃᡖᡖᡃ



# bot.py, in event_message, below the bot-ignoring stuff
await ctx.channel.send(ctx.content)


Enter fullscreen mode Exit fullscreen mode

Restart the bot and check it out!

rip

πŸ’‘ PROTIPβ„’ -- Comment out that line now cuz it's actually really annoying.

Making a chat command

Any command you make needs to follow this format when defining them..

  • Decorated with @bot.command(name='whatever')
  • Be asynchronous functions with names that match the name variable in the decorator
  • Pass the message context in through the function

How the function works and what it does is all up to you. For this example, we'll create a command called !test that says test passed! in chat when we call it.



# bot.py, below event_message function
@bot.command(name='test')
async def test(ctx):
    await ctx.send('test passed!')


Enter fullscreen mode Exit fullscreen mode

Before this can work, we need to make sure that the bot knows to listen for commands coming through.

Add this just below the ignore bot code in event_message:



    #bot.py, in event_message, below the bot ignore stuffs
    await bot.handle_commands(ctx)


Enter fullscreen mode Exit fullscreen mode

Alright! Time to test it out. Reboot the bot and send !test in chat!

test passed

Responding to specific messages

Tell my bot I said... "Hello."

You can respond to specific messages in your chat too, they don't have to be !commands. Let's write some code that says hi when people say hello.



    # bot.py, at the bottom of event_message
    if 'hello' in ctx.content.lower():
        await ctx.channel.send(f"Hi, @{ctx.author.name}!")


Enter fullscreen mode Exit fullscreen mode

Go ahead and test it out! You've got the framework to start buildng your bot and adding commands.

tell my bot i said

Here's what you should have when you're done

/bot.py



import os
from twitchio.ext import commands

# set up the bot
bot = commands.Bot(
    irc_token=os.environ['TMI_TOKEN'],
    client_id=os.environ['CLIENT_ID'],
    nick=os.environ['BOT_NICK'],
    prefix=os.environ['BOT_PREFIX'],
    initial_channels=[os.environ['CHANNEL']]
)

@bot.event
async def event_ready():
    'Called once when the bot goes online.'
    print(f"{os.environ['BOT_NICK']} is online!")
    ws = bot._ws  # this is only needed to send messages within event_ready
    await ws.send_privmsg(os.environ['CHANNEL'], f"/me has landed!")


@bot.event
async def event_message(ctx):
    'Runs every time a message is sent in chat.'

    # make sure the bot ignores itself and the streamer
    if ctx.author.name.lower() == os.environ['BOT_NICK'].lower():
        return

    await bot.handle_commands(ctx)

    # await ctx.channel.send(ctx.content)

    if 'hello' in ctx.content.lower():
        await ctx.channel.send(f"Hi, @{ctx.author.name}!")


@bot.command(name='test')
async def test(ctx):
    await ctx.send('test passed!')


if __name__ == "__main__":
    bot.run()


Enter fullscreen mode Exit fullscreen mode

And ofc your .env with your secrets and a pipfile and Piplock.

I've uploaded the files to a github repo too, if that's your thing.

Congrats!! πŸ₯³πŸŽ‰

You've made it this far.. You know what that means? Time to celebrate by clicking this GitKraken referral link and signing up so I can get free socks (or maybe a Tesla? 😎).

Also, feel free to check out the Live Coding Stream recap where we developed this tutorial. Shoutouts to everyone in chat that collaborated!

What do you want to do next?

Questions? Comments? Ideas? Let me know in the comments below!

I'll be following up to this post soon with some tips on how to get the most out of the TwitchIO library -- more stuff that's not really well-documented and was a PITA to figure out, like how to get the author, using badges for permissions, etc.

Top comments (56)

Collapse
 
mrbackbreaker_ profile image
Angry Bob

I have a problem at the point where i have to wake the bot up with "pipenv run python bot.py". It keeps saying "Loading .env environment variables…" but nothing more. Its just stuck at this point and doesnt even give me an error.
I would really appreciate an answer since im stuck here for several hours and its driving me nuts. Thank you in advance

Collapse
 
erosika profile image
erosika • Edited

Not sure if its actually an issue to be stuck on "Loading .env environment variables…" because I can continue with the rest of the code, and my bot does go online.

This happens when my Bot_Nick and Channel are the same. Before making my Bot_Nick the same as my channel name, I was having the "channel does not exist" error. If I make my Bot_Nick my channel name, it doesn't seem to matter what the Channel input is, because the bot still goes online as my channel name.

Would love to see this fixed because it's very easy to get running otherwise!

Collapse
 
lukasdim27 profile image
Lukas Dimitroff

Bot_Nick is not a nickname, it is the name of the channel.

Collapse
 
holphana profile image
Holly Pennington • Edited

I'm so sorry that it took someone more than a year to find this but this is a super easy fix that was not very clear in the guide.

(the following example has a couple extra spaces to avoid formatting)

the:
if __ name __ == "__ main __":
bot.run()

must go at the bottom of your bot.py file

if there are functions after it, it will not load.

Collapse
 
pesteaux profile image
pesteaux

Hi I am having the same problem and can't get around it. I am stuck on this error. Did anyone get a fix? The name/channel seems to be key but I can't get a working combination. I don't understand the user who said the nick is the channel.

Collapse
 
seanlodhammar profile image
Sean Lodhammar

The reason "Loading .env environment variables..." will stay, is because there is nothing to print to the console afterwards. In the "event_ready" function, if you print something, then there will be output.

Collapse
 
zakdugie profile image
Dugie

Same problem, eventually when trying to abort it prints this error:
C:\Users\User.virtualenvs\Chatbot_work--fd3mIVH\lib\site-packages\twitchio\websocket.py:618: RuntimeWarning: coroutine 'WebSocketCommonProtocol.close' was never awaited
self._websocket.close()

Collapse
 
xninjarose profile image
Ninja | Rose [A+,Net+]

If it says nothing more and your still at beginning of tutorial, then you did it right, it's in the channel, do the next step and it should put a message in chat like it says in tutorial

Collapse
 
phyrexxii profile image
Sam

i get this error

Traceback (most recent call last):
File "B:\ChatLoyalty\ChatLoyalty.py", line 7, in
irc_token=os.environ['TMI_TOKEN'],
File "C:\Users\user\AppData\Local\Programs\Python\Python38\lib\os.py", line 675, in getitem
raise KeyError(key) from None
KeyError: 'TMI_TOKEN'

what am i doing wrong?

Collapse
 
spiringosu profile image
Spiring

I figured this out. What you want to do is put in what is in these quotations for each of the environments into bot.py instead of .env.
"os.environ['TMI_TOKEN']=
os.environ['CLIENT_ID']=
os.environ['BOT_NICK']=
os.environ['BOT_PREFIX']=
os.environ['CHANNEL']=
"
For some reason, the OP didn't specify how to call the .env file before it tries to run the rest of bot.py, but doing that eliminates the need for .env

Collapse
 
sunnybirdboi profile image
sunnybirdboi

I'm getting the same issue and cannot figure out where to put your solution. I've tried putting the .env info directly into bot.py as variables to eliminate the need for the .env, and I still get the KeyError despite any changes I make. Where am I putting this info to fix this problem? Thank you!

Collapse
 
space__cdt profile image
Space Cadet

Hey y'all! Like most of you I ended up here and had trouble getting the channel to connect.

What you want to do is lead the channel name with a "#" in your env file.

For instance:

CHANNEL="#space_cdt"

After this I was able to connect!

Collapse
 
spiringosu profile image
Spiring

Could you be a little more specific? Doing this still leads to having it not set the correct channel name,

Collapse
 
cgusb profile image
Gus Becker

This is more likely an error with you TMI token or Client ID. Make sure both are generated using your bot's Twitch account and not your personal Twitch account.

Collapse
 
xdnuggets profile image
Nuggets • Edited

It keeps saying "Make sure channel exists. I have tried the url type, twitch.tv/channelname, and just the channel name. I have made a new oauth, and tried everything I can. EDIT: Just looked at the other comments, and figured out that bot_nick needs to be the channel name, so what do I put at CHANNEL=?

Collapse
 
cgusb profile image
Gus Becker

BOT_NICK is the channel name of the bot account, CHANNEL is the channel name of the channel whose chat you'd like the bot to join. Just the usernames, don't use the "twitch.tv/" prefix.

Collapse
 
space__cdt profile image
Space Cadet

you need to define CHANNEL in your .env file like this:

CHANNEL="#channel_name"

Collapse
 
kicksent profile image
Nick Trierweiler

To everyone with the error:
self._websocket.close()
If you scroll up there should be an earlier error saying:
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1097)

To resolve this I used this: gist.github.com/marschhuynh/31c937...

Simply copy this into another python file called install_ssl_certs.py and run it with
pipenv run python install_ssl_certs.py

After doing this I was able to connect and got the message: Ready | frenchtoastbot_

Collapse
 
chreaus profile image
Chreaus • Edited

this wound up working great! ty for the start :) what section of the api docs did you find these?

EDIT:nvm... i just figured it out... you need to use the Account name as the NICK not the botname... might help if you make that a bit more clear in the instructions.

I'm having the same issue as Jordan and Cai...

I think its safe to assume it is connecting to twitch API but something about the channel name variable is not working.

I created a new twitch account just for the bot and registered the application on that account.

I have tried with a different channel (a friends) to no avail.

I have checked and double checked my oAuth and Client-ID and they're both correct.
My (4) files are all in the same directory. (changing the channel name changes name in the error so pipenv is definitely working from the correct .env and other files...)

could you maybe post a "throw away" screen cap of what its supposed to look like all filled out? maybe its a simple syntax derp that i'm missing... i'm currently not using " marks or anything else after the = in the .env file.

-error return below-

PS C:\Users\thato\Documents\Python Files> pipenv run python bot.py
Loading .env environment variables…
Task exception was never retrieved
future: exception=KeyError('chreaus')>
Traceback (most recent call last):
File "C:\Users\thato.virtualenvs\Python_Files-vA_Pk7W6\lib\site-packages\twitchio\websocket.py", line 558, in join_action
cache = self._channel_cache[channel]['channel']._users
KeyError: 'chreaus'
Task exception was never retrieved
future: exception=KeyError('chreaus')>
Traceback (most recent call last):
File "C:\Users\thato.virtualenvs\Python_Files-vA_Pk7W6\lib\site-packages\twitchio\websocket.py", line 558, in join_action
cache = self._channel_cache[channel]['channel']._users
KeyError: 'chreaus'
Task exception was never retrieved
future: exception=TimeoutError('Request to join the "chreaus" channel has timed out. Make sure the channel exists.')>
Traceback (most recent call last):
File "C:\Users\thato.virtualenvs\Python_Files-vA_Pk7W6\lib\site-packages\twitchio\websocket.py", line 280, in _join_channel
await asyncio.wait_for(fut, timeout=10)
File "c:\users\thato\appdata\local\programs\python\python37\lib\asyncio\tasks.py", line 449, in wait_for
raise futures.TimeoutError()
concurrent.futures._base.TimeoutError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\thato.virtualenvs\Python_Files-vA_Pk7W6\lib\site-packages\twitchio\websocket.py", line 228, in auth_seq
await self.join_channels(channels)
File "C:\Users\thato.virtualenvs\Python_Files-vA_Pk7W6\lib\site-packages\twitchio\websocket.py", line 271, in join_channels
await asyncio.gather(
[self._join_channel(x) for x in channels])
File "C:\Users\thato.virtualenvs\Python_Files-vA_Pk7W6\lib\site-packages\twitchio\websocket.py", line 285, in _join_channel
f'Request to join the "{channel}" channel has timed out. Make sure the channel exists.')
concurrent.futures._base.TimeoutError: Request to join the "chreaus" channel has timed out. Make sure the channel exists.

Collapse
 
cgusb profile image
Gus Becker

Hi all! After some headaches with this yesterday, I wanted to make some clarifications that would have helped me when I started trying to make my bot!
1) The TMI token and Client ID should both be generated using the Twitch account you set up for your bot. If you are having the "channel does not exist" error, try re-registering your app with Twitch dev and make sure you save the changes and get the updated Client ID! If you are using a personal computer as opposed to a web server, make sure for the entry box "OAuth Redirect URLs" you are entering "localhost" (no quotes).
2) The .env file is a way to keep your TMI token and Client ID secret even if you use version control software like Git to store your code somewhere like GitHub. You need to have a Python package called dotenv to load these variables into your bot.py file. To download this package, you can do it using pip with the command pip install python-dotenv. You then need to import the function load_dotenv() from the package at the top of your file. This can be done by including from dotenv import load_dotenv with all your other package imports at the top of the file. Then you need to call the load_dotenv() function at the top of your bot.py file (below the imports but above the os.environ calls. This allows os to properly import those variable from the .env file. If you don't want to download dotenv, you can list these variables directly in bot.commands(), but realize that if you do this, you shouldn't share this code publicly because someone else getting their hands on the TMI token and Client ID would allow them to control your bot.
3) The variables in the .env file should be as follows:
TMI_TOKEN=oauth:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BOT_NICK=BotChannelName
BOT_PREFIX=!
CHANNEL=ChannelNameOfChatToEnter
Important note: BOT_PREFIX refers to the prefix of the commands that the bot will respond to. Most Twitch bots use "! " (e.g. !discord), so it's probably a good idea to keep it as a single exclamation mark. A lot of comments say to use you bot's channel name, but this will make it so that your bot doesn't respond to commands!
4) When testing your bot, you may want to edit the code in your bot.py file. To do this, you must stop the code from executing and re-execute the code to update the bot with the new info in the saved bot.py file. To do this, go to the command line that your prompts are run from and hit Crtl + C. This will stop the execution and allow you to re-execute.

I hope this helps anybody that might be confused in ways similar to how I was!

Collapse
 
jordoosu profile image
Jordan • Edited

I keep trying to run the bot but get the error: KeyError: 'jordo1'.

asyncio.exceptions.TimeoutError: Request to join the "jordo1" channel has timed out. Make sure the channel exists.

My channel certainly exists. I tried it with both Python 3.7 and 3.8. My code is basically the same, but I made my own class inheriting Bot instead.

I regenerated my oauth and reconfirmed my client ID.

Please help. Driving me nuts for hours.

Edit: Just realized the bot is working but this error still appears despite joining the channel. Strange. The nick isn't working though, it's just using my twitch username.

Collapse
 
dunkelweizen profile image
Cai Nowicki

I'm getting the same error and resolution. And I can't get any of the bot.command functions to work, even with copy/pasting the code given here. If I fold it inside the event_message function it works, but not as its own thing.

Collapse
 
runneypo profile image
runneypo

I'm having trouble registering an app on the dev.twitch.tv/console/apps/create, using localhost gives me Cartman.GetAuthorizationToken: 401: {"code":401,"status":"invalid oauth token"}, any idea what I'm supposed to do to get it to work?

Collapse
 
jacouby profile image
EkoDoesntDev

Keep getting this error, have looked at the comments and everything people say seems to be correct. Please help cause this is confusing me very much.

Task exception was never retrieved
future: exception=TimeoutError('Request to join the "josh_liv3" channel has timed out. Make sure the channel exists.')>
Traceback (most recent call last):
File "/opt/virtualenvs/python3/lib/python3.8/site-packages/twitchio/websocket.py", line 280, in _join_channel
await asyncio.wait_for(fut, timeout=10)
File "/usr/lib/python3.8/asyncio/tasks.py", line 501, in wait_for
raise exceptions.TimeoutError()
asyncio.exceptions.TimeoutError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/opt/virtualenvs/python3/lib/python3.8/site-packages/twitchio/websocket.py", line 228, in auth_seq
await self.join_channels(channels)
File "/opt/virtualenvs/python3/lib/python3.8/site-packages/twitchio/websocket.py", line 271, in join_channels
await asyncio.gather(
[self._join_channel(x) for x in channels])
File "/opt/virtualenvs/python3/lib/python3.8/site-packages/twitchio/websocket.py", line 284, in _join_channel
raise asyncio.TimeoutError(
asyncio.exceptions.TimeoutError: Request to join the "josh_liv3" channel has timed out. Make sure the channel exists.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.