Twitch

A connector for Twitch.

Requirements

  • A Twitch Account

  • A Twitch App obtained from Twitch developers page

  • The code obtained from the first step of OAuth

Configuration

connectors:
  twitch:
    # required
    code: "hfu923hfks02nd2821jfislf" # Code obtained from the first OAuth step
    client-id: "e0asdj48jfkspod0284"
    client-secret: "kdksd0458j93847j"
    channel: theflyingdev # Broadcaster channel
    redirect: http://localhost # Url to be passed to get oath token - defaults to localhost
    forward-url: 'http://94jfsd9ff.ngrok.io' # Either an URL provided by a forwarding service or an exposed ip address
    # optional
    webhook-lease-seconds: 86400 # how long for webhooks to expire
    always-listening: false # Turn on to connect to the chat server even if stream is offline.

Setup Twitch App

You need to create a Twitch App to use the Twitch Connector. Click the + Register Your Application button, give this app a name and a redirect url - using http://localhost is fine. Once created, you can click the Manage button and get your client-id, you can then get a client-secret by pressing the New Secret button (keep this secret safe as it won’t be shown to you again).

Getting OAuth code

Twitch OAuth has two steps, first you need to make a GET request to a specific URL to obtain a code. After you’ve received the code, you need to make a POST request to the same URL and Twitch will send you an access_token and refresh_token.

After a certain period, the access_token will expire and you have to make a new request with your refresh_token to re-authenticate yourself.

NOTE: The Twitch Connector will handle token expiration and re-authentication for you.

Step 1 - Getting Code

To get your code, you need to make a request to https://id.twitch.tv/oauth2/authorize with the following parameters:

  • client_id

  • redirect_uri

  • response_type

  • scope

Both the client_id and redirect_uri can be obtained when you click the Manage button on your app. The response_type that we want will be code and we will ask for a lot of scopes. You can check the API Scopes and the IRC Scopes to read more about what we are asking and why.

The Twitch Connector interacts with a wide range of services - IRC server, New API, V5 API - so we need to pass a big number of scopes to make sure everything works as expected.

Example: OAuth URL

You can use this example url to make your request - make sure you add your client_id before making the request. After adding your client id, you can open the url on a browser window, accept the request and Twitch will send you back to your redirect_url. Look at the address bar and you will see that it contains a code=jfsd98hjh8d7da983j this is what you need to add to your opsdroid config.

https://id.twitch.tv/oauth2/authorize?client_id=<your client id>&redirect_uri=http://localhost&response_type=code&scope=channel:read:subscriptions+channel_subscriptions+analytics:read:games+chat:read+chat:edit+viewing_activity_read+channel_feed_read+channel_editor+channel:read:subscriptions+user:read:broadcast+user:edit:broadcast+user:edit:follows+channel:moderate+channel_read

Usage

The connector will subscribe to followers alerts, stream status (live/offline) and subscriber alerts, it will also connect to the chat service whenever the stream status notification is triggered and the StreamStarted event is triggered by opsdroid. If you wish you can set the optional config parameter always-listening: True to connect to the chat whenever opsdroid is started.

Events Available

The Twitch Connector contains 10 events that you can use on your custom made skill. Some of these events are triggered automatically whenever an action happens on twitch - for example when a user follows your channel. Others you will have to trigger on a skill - for example, to delete a specific message.

Automatic Events

These events are triggered by opsdroid whenever something happens on twitch.

class opsdroid.events.JoinRoom(user_id=None, user=None, target=None, connector=None, raw_event=None, raw_parses=None, event_id=None, linked_event=None)

Event class to represent a user joining a room.

class opsdroid.events.LeaveRoom(user_id=None, user=None, target=None, connector=None, raw_event=None, raw_parses=None, event_id=None, linked_event=None)

Event class to represent a user leaving a room.

class opsdroid.connector.twitch.events.UserFollowed(follower, followed_at, *args, **kwargs)

Event class to trigger when a user follows the streamer.

class opsdroid.connector.twitch.events.UserSubscribed(user, message, *args, **kwargs)

Event class that triggers whenever a user subscribes to the channel.

class opsdroid.connector.twitch.events.UserGiftedSubscription(gifter_name, gifted_named, *args, **kwargs)

Event class that triggers when a user gifts a subscription to someone.

class opsdroid.connector.twitch.events.StreamStarted(title, viewers, started_at, *args, **kwargs)

Event class that triggers when streamer started broadcasting.

class opsdroid.connector.twitch.events.StreamEnded(user_id=None, user=None, target=None, connector=None, raw_event=None, raw_parses=None, event_id=None, linked_event=None)

Event class that triggers when streamer stoppped broadcasting.

Manual Events

These events will have to be triggered by you with an opsdroid skill.

class opsdroid.connector.twitch.events.UpdateTitle(status, *args, **kwargs)

Event class that updates channel title.

class opsdroid.connector.twitch.events.CreateClip(id, *args, **kwargs)

Event class that creates a clip once triggered.

class opsdroid.events.DeleteMessage(user_id=None, user=None, target=None, connector=None, raw_event=None, raw_parses=None, event_id=None, linked_event=None)

Event to represent deleting a message or other event.

class opsdroid.events.BanUser(user_id=None, user=None, target=None, connector=None, raw_event=None, raw_parses=None, event_id=None, linked_event=None)

Event to represent the banning of a user from a room.

Examples

You can write your custom skills to interact with the Twitch connector, here are a few examples of what you can do. You can also use the Twitch Skill to interact with the connector.

StreamStarted event

Let’s say that you want to send a message to another connector whenever you go live, you can achieve this by writing that will be triggered when the StreamStarted event is triggered.

from opsdroid.skill import Skill
from opsdroid.matchers import match_event
from opsdroid.connector.twitch.events import StreamStarted


class TwitchSkill(Skill):
 """opsdroid skill for Twitch."""
    def __init__(self, opsdroid, config, *args, **kwargs):
        super().__init__(opsdroid, config, *args, **kwargs)
        self.rocketchat_connector = self.opsdroid.get_connector('rocketchat')

    @match_event(StreamStarted)
    async def stream_started_skill(event):
    """Send message to rocketchat channel."""
        await self.rocketchat_connector.send(Message(f"I'm live on twitch, come see me work on {event.title}"))

UserFollowed event

Some bots will send a thank you message to the chat whenever a user follows your channel. You can do the same with opsdroid by using the UserFollowed event.

from opsdroid.skill import Skill
from opsdroid.matchers import match_event
from opsdroid.connector.twitch.events import UserFollowed


class TwitchSkill(Skill):
 """opsdroid skill for Twitch."""
    def __init__(self, opsdroid, config, *args, **kwargs):
        super().__init__(opsdroid, config, *args, **kwargs)
        self.connector = self.opsdroid.get_connector('twitch')

    @match_event(UserFollowed)
    async def say_thank_you(event):
    """Send message to rocketchat channel."""
        await self.connector.send(Message(f"Thank you so much for the follow {event.follower}, you are awesome!"))

BanUser event

We have seen how to send messages to the chat, how about we remove a spam message and ban bots from trying to sell you followers, subs and viewers?

from opsdroid.skill import Skill
from opsdroid.matchers import match_regex
from opsdroid.connector.twitch.events import BanUser, DeleteMessage


class TwitchSkill(Skill):
 """opsdroid skill for Twitch."""
    def __init__(self, opsdroid, config, *args, **kwargs):
        super().__init__(opsdroid, config, *args, **kwargs)
        self.connector = self.opsdroid.get_connector('twitch')

    @match_regex(r'famous\? Buy followers', case_sensitive=False)
    async def goodbye_spam_bot(self, message):
        await self.connector.send(BanUser(user=message.user))
        deletion = DeleteMessage(id=message.event_id)
        await self.connector.send(deletion)

UpdateTitle event

You need to be careful on how you set this skill, you should have a list of users that are allowed to change your broadcast title otherwise it can be abused while you are streaming.

from opsdroid.skill import Skill
from opsdroid.constraints import constrain_users
from opsdroid.matchers import match_regex
from opsdroid.connector.twitch.events import UpdateTitle


class TwitchSkill(Skill):
 """opsdroid skill for Twitch."""
    def __init__(self, opsdroid, config, *args, **kwargs):
        super().__init__(opsdroid, config, *args, **kwargs)
        self.connector = self.opsdroid.get_connector('twitch')

    @match_regex(r'\!title (.*)')
    @constrain_users(your_awesome_twitch_username)
    async def change_title(self, message):
        _LOGGER.info("Attempt to change title")
        await self.connector.send(UpdateTitle(status=message.regex.group(1)))

You could also add a whitelisted config param to your skill and then read the configuration to check if the user that tried to change the title is in that list.

skills:
  - twitch:
    whitelisted: 
      - your_username_on_twitch
      - your_username_on_another_connector
from opsdroid.skill import Skill
from opsdroid.constraints import constrain_users
from opsdroid.matchers import match_regex
from opsdroid.connector.twitch.events import UpdateTitle


class TwitchSkill(Skill):
 """opsdroid skill for Twitch."""
    def __init__(self, opsdroid, config, *args, **kwargs):
        super().__init__(opsdroid, config, *args, **kwargs)
        self.connector = self.opsdroid.get_connector('twitch')

    @match_regex(r'\!title (.*)')
    async def change_title(self, message):
        if message.user in self.config.get('whitelisted', []):
        await self.connector.send(UpdateTitle(status=message.regex.group(1)))

Reference

class opsdroid.connector.twitch.ConnectorTwitch(*args, **kwargs)

A connector for Twitch.

async ban_user(event)

Ban user from the channel.

This event will be used when we need to ban a specific user from the chat channel. Banning a user will also remove all the messages sent by that user, so we don’t need to worry about removing a lot of mensages.

async connect()

Connect to Twitch services.

Within our connect method we do a quick check to see if the file twitch.json exists in the application folder, if this file doesn’t exist we assume that it’s the first time the user is running opsdroid and we do the first request for the oauth token.

If this file exists then we just need to read from the file, get the token in the file and attempt to connect to the websockets and subscribe to the Twitch events webhook.

async connect_websocket()

Connect to the irc chat through websockets.

Our connect method will attempt to make a connection to Twitch through the websockets server. If the connection is made, any sort of failure received from the websocket will be in the form of a NOTICE, unless Twitch closes the websocket connection.

In this method we attempt to connect to the websocket and use the previously saved oauth token to join a twitch channel.

Once we are logged in and on a Twitch channel, we will request access to special features from Twitch.

The commands request is used to allow us to send special commands to the Twitch IRC server.

The tags request is used to receive more information with each message received from twitch. Tags enable us to get metadata such as message ids.

The membership request is used to get notifications when an user enters the chat server (it doesn’t mean that the user is watching the streamer) and also when a user leaves the chat channel.

async create_clip()

Create clip from broadcast.

We send a post request to twitch to create a clip from the broadcast, Twitch will return a response containing a clip id and edit_url . TWitch mentions that the way to check if the clip was created successfully is by making a get request to the clips API enpoint and query by the id obtained from the previous request.

async disconnect()

Disconnect from twitch.

Before opsdroid exists we will want to disconnect the Twitch connector, we need to do some clean up. We first set the while loop flag to False to stop the loop and then try to unsubscribe from all the webhooks that we subscribed to on connect - we want to do that because when we start opsdroid and the connect method is called we will send another subscribe request to Twitch. After we will send a PART command to leave the channel that we joined on connect.

Finally we try to close the websocket connection.

async disconnect_websockets()

Disconnect from the websocket.

get_authorization_data()

Open file containing authentication data.

async get_messages_loop()

Listen for and parse messages.

Since we are using aiohttp websockets support we need to manually send a pong response every time Twitch asks for it. We also need to handle if the connection was closed and if it was closed but we are still live, then a ConnectionError exception is raised so we can attempt to reconnect to the chat server again.

async get_user_id(channel, token, client_id)

Call twitch api to get broadcaster user id.

A lot of webhooks expect you to pass your user id in order to get the notification when a user subscribes or folllows the broadcaster channel.

Since we are calling the Twitch API to get our self.user_id on connect, we will use this method to handle when a token has expired, so if we get a 401 status back from Twitch we will raise a ClientResponseError and send back the status and the message Unauthorized, that way we can refresh the oauth token on connect if the exception is raised.

Parameters
  • channel (string) – Channel that we wish to get the broadcaster id from.

  • token (string) – OAuth token obtained from previous authentication.

  • client_id (string) – Client ID obtained from creating a Twitch App to iteract with opsdroid.

Returns

Broadcaster/user id received from Twitch

Return type

string

Raises
  • ConnectionError – Raised exception if we got an unauthorized code from twitch. Our

  • oauth token probably expired.

async handle_challenge(request)

Challenge handler for get request made by Twitch.

Upon subscription to a Twitch webhook, Twitch will do a get request to the callback url provided to check if the url exists. Twitch will do a get request with a challenge and expects the callback url to return that challenge in plain-text back to Twitch.

This is what we are doing here, we are getting hub.challenge from the request and return it in plain-text, if we can’t find that challenge we will return a status code 500.

Parameters

request (aiohttp.web.Request) – Request made to the get route created for webhook subscription.

Returns

if request contains hub.challenge we return it, otherwise return status 500.

Return type

aiohttp.web.Response

async listen()

Listen method of the connector.

Every connector has to implement the listen method. When an infinite loop is running, it becomes hard to cancel this task. So we are creating a task and set it on a variable so we can cancel the task.

If we need to reconnect to Twitch, Twitch will allow us to reconnect immediatly on the first reconnect and then expects us to wait exponentially to reconnect to the websocket.

async refresh_token()

Attempt to refresh the oauth token.

Twitch oauth tokens expire after a day, so we need to do a post request to twitch to get a new token when ours expires. The refresh token is already saved on the twitch.json file so we can just open that file, get the appropriate token and then update the file with the new received data.

async remove_message(event)

Remove message from the chat.

This event is used when we need to remove a specific message from the chat service. We need to pass the message id to remove a specific message. So this method is calling the /delete method together with the message id to remove that message.

async request_oauth_token()

Call Twitch and requests new oauth token.

This method assumes that the user already has the code obtained from following the first oauth step which is making a get request to the twitch api endpoint: https://id.twitch.tv/oauth2/authorize and passing the needed client id, redirect uri and needed scopes to work with the bot.

This method is the second - and final step - when trying to get the oauth token. We use the code that the user obtained on step one - check documentation - and make a post request to Twitch to get the access_token and refresh_token so we can refresh the access_token when needed. Note that the refresh_token doesn’t change with each refresh.

save_authentication_data(data)

Save data obtained from requesting authentication token.

async send_handshake()

Send needed data to the websockets to be able to make a connection.

If we try to connect to Twitch with an expired oauth token, we need to request a new token. The problem is that Twitch doesn’t close the websocket and will only notify the user that the login authentication failed after we sent the PASS , NICK and JOIN command to the websocket.

So we need to send the initial commands to Twitch, await for a status with await self.websockets.recv() and there will be our notification that the authentication failed in the form of :tmi.twitch.tv NOTICE * :Login authentication failed

This method was created to prevent us from having to copy the same commands and send them to the websocket. If there is an authentication issue, then we will have to send the same commands again - just with a new token.

async send_message(message)

Send message throught websocket.

To send a message to the Twitch IRC server through websocket we need to use the same style, we will always send the command PRIVMSG and the channel we want to send the message to. The message also comes after :.

Parameters

message (string) – Text message that should be sent to Twitch chat.

async twitch_webhook_handler(request)

Handle event from Twitch webhooks.

This method will handle events when they are pushed to the webhook post route. Each webhook will send a different kind of payload so we can handle each event and trigger the right opsdroid event for the received payload.

For follow events the payload will contain from_id (broadcaster id), from_username (broadcaster username) to_id (follower id), to_name (follower name) and followed_at (timestamp).

For stream changes a lot more things are returned but we only really care about type (if live/offline) title (stream title).

For subscriptions events we will want to know event_type , timestamp , event_data.plan_name , event_data.is_gift , event_data.tier , event_data.username and event_data.gifter_name.

Parameters

request (aiohttp.web.Request) – Request made to the post route created for webhook subscription.

Returns

Send a received message and status 200 - Twitch will keep sending the event if it doesn’t get the 200 status code.

Return type

aiohttp.web.Response

async update_stream_title(event)

Update Twitch title.

To update your channel details you need to use Twitch API V5(kraken). The so called “New Twitch API” doesn’t have an enpoint to update the channel. To update your channel details you need to do a put request and pass your title into the url.

Parameters

event (twitch.events.UpdateTitle) – opsdroid event containing status (your title).

async validate_request(request, secret)

Compute sha256 hash of request and secret.

Twitch suggests that we should always validate the requests made to our webhook callback url, that way we protect ourselves from received an event that wasn’t sent by Twitch. After sending hub.secret on our webhook subscribe, Twitch will use that secret to send the x-hub-signature header, that is the hash that we should compare with our own computed one, if they don’t match then the request is not valid and shouldn’t be parsed.

async webhook(topic, mode)

Subscribe to a specific webhook.

Twitch has different webhooks that you can subscribe to, when you subscribe to a particular webhook, a post request needs to be made containing a JSON payload, that tells Twitch what subscription you are attempting to do.

When you submit the post request to TWITCH_WEBHOOK_ENDPOINT , twitch will send back a get request to your callback url (hub.callback ) with a challenge. Twitch will then await for a response containing only the challenge in plain text.

With this in mind, that is the reason why we open two routes (get and post ) that link to /connector/<connector name>.

The hub.topic represents the webhook that we want to suscribe from twitch. The hub.lease_seconds defines the number of seconds until the subscription expires, maximum is 864000 seconds (10 days), but we will set up a day as our expiration since our app oauth tokens seem to expire after a day.

Parameters
  • topic (string) – Twitch webhook url to subscribe/unsubscribe to.

  • mode (string) – subscribe or unsuscribe to the webhook.