Teekeks / pyTwitchAPI

A Python 3.7 compatible implementation of the Twitch API, EventSub, PubSub and Chat
https://pytwitchapi.dev
MIT License
254 stars 38 forks source link

Chat should not require full app authentication #233

Closed Julian-O closed 1 year ago

Julian-O commented 1 year ago

This is a feature request, to address a "pain point" I am having porting my app from another set of Twitch libraries to twitchAPI.

My system is fairly straightforward. Running on my Streaming PC is an app registered under my Twitch username that subscribes to notifications for PubSub and communicates to Twitch via Helix. In addition, it monitors chat.

The trickiness is that it sometimes posts to chat, and those posts should come from a second Twitch account - an account whose name is clearly indicates it is a bot, so viewers know the responses are automated.

Typically, chatbots don't have to be associated with an app. They need to know the username and oauth details of the user they post as, but no app id or app secret. The old version of my app just logged into Chat (via IRC's PASS and NICK commands) as the bot, while it authenticated as the app for the rest.

However, the twitchAPI doesn't support this. To use Chat, you must first authenticate as an app. It uses whatever Twitch user that was logged into the webbrowser that opened up when UserAuthenticator.authenticate() was called. Naturally, by default, this is my normal streaming Twitch account, not my bot account.

My current work around: Create a spurious app in the Bot account. Create a new Chrome profile that is logged in as the bot. Pass the hard-coded location of the Chrome executable with a hard-coded parameter to open the specific profile name that is logged in as the bot. None of this is necessary.

I recommend that the Chat class be changed to either accept a fully-authenticated Twitch object or just accept a username, an (oauth) secret, and an optional session timeout.

[I could put a PR together, but I will need a very relaxed deadline.]

Teekeks commented 1 year ago

You do not have to use UserAuthenticator.authenticate() on every restart of the bot, the docs only do this to keep the examples easy. If you already have a valid user oauth token, you can just use twitch.set_user_authentication(), if you pass in the refresh token the lib will even try to refresh it for you should it be too old.

An example how this might look like is this, which only uses UserAuthenticator when both the oauth token and refresh token became invalid and at first startup (at which point you would also need to generate a fresh one using your method)

from twitchAPI import Twitch
from twitchAPI.oauth import UserAuthenticator
from twitchAPI.types import AuthScope
from twitchAPI.chat import Chat
import asyncio
import os
import json

APP_ID = 'my_app_id'
APP_SECRET = 'my_app_secret'
USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT]

async def update_stored_tokens(token: str, refresh_token: str):
    with open('token.json', 'w') as _f:
        json.dump({'token': token, 'refresh': refresh_token}, _f)

async def run():
    twitch = await Twitch(APP_ID, APP_SECRET)
    # set our callback method for token changes
    twitch.user_auth_refresh_callback = update_stored_tokens
    needs_auth = true
    # check if token storage exists
    if os.path.exists('token.json'):
        # file exists: try to load and set the stored token pair
        # should the token be invalid, twitch.set_user_authentication will automatically try to refresh it using the refresh token
        try:
            with open('token.json', 'r') as _f:
                creds = json.load(_f)
            await twitch.set_user_authentication(creds['token'], USER_SCOPE, creds['refresh'])
        except:
            logging.info('stored token invalid, refreshing...')
        else:
            needs_auth = false
    if needs_auth:
        # file does not exist or reauth failed -> use normal auth flow and store generated token to file
        auth = UserAuthenticator(twitch, USER_SCOPE)
        token, refresh_token = await auth.authenticate()
        with open('token.json', 'w') as _f:
            json.dump({'token': token, 'refresh': refresh_token}, _f)
        await twitch.set_user_authentication(token, USER_SCOPE, refresh_token)

    # create chat instance
    chat = await Chat(twitch)
    chat.start()
    try:
        input('press ENTER to stop\\n')
    finally:
        chat.stop()
        await twitch.close()

# lets run our setup
asyncio.run(run())

If you need another user for the chat bot than for everything else, you could also just create a second Twitch instance which is authenticated for that user and use that one.

Julian-O commented 1 year ago

I have been muttering to myself for 30 minutes "I trust @Teekeks on this more than I trust myself; I know I am confused - but this explanation doesn't match my experience. Where am I going wrong?" I have been staring at my old code and the library code and the Twitch Developer documentation.

I believe I must have manually generated an Oauth token for the chatbot, and continued to use it for months and months without it ever expiring (in a way that Twitch has enforced). Meanwhile, I am generating a new Oauth token (and blithely opening a webbrowser) for every single session that accesses the PubSub/Helix code.

Because that has been working fine, I have been carrying around the wrong mental model about how it is supposed to work.

I hope I have my head around it now. If OAuth tokens last for months, I will be storing them as you suggest here.

Thank you for your support! I hope I can find someway to contribute to pay you back.