issork / gift

Godot IRC For Twitch addon
MIT License
150 stars 23 forks source link

Handling a bot that can view events on a different channel (2 different twitch accounts) #24

Open jitspoe opened 1 year ago

jitspoe commented 1 year ago

So this is more of a request/discussion than a bug, but I was attempting to create a bot that would sit in my channel and have responses as the bot name but also respond to events like point redeems and follows to my channel. It seems like the only way to do this is to have 2 different authentications: one from my twitch, and one from the bot's twitch. The follow notifications worked by having the bot as a moderator, but it seems like other events don't have that functionality.

So I effectively, I set up 2 different scopes and 2 different authorizations. It was kind of a pain, because, since it opens the browser which is logged into my main twitch account, it would automatically authenticate. I ended up having the 2nd one just copy to the clipboard instead of open a browser window from the shell. That way I could paste it into a different browser that was logged into my bot account.

I'm not sure if this is the intended approach -- seems like a real hot mess, but I did get it technically working. Not sure the best way to handle the UX for this due to needing 2 different logins. I guess an "authenticate channel" button and "authenticate bot account" button that would copy the URLs to be pasted in appropriate browser windows?

I just wanted my bot to respond to point redeems! 😭

Side note, the refresh_tokens() function seems like it might not be entirely correct? Maybe I just don't understand it.

func refresh_token() -> void:
    await(get_tree().create_timer(3600).timeout)
    if (await(is_token_valid(token_user["access_token"])) == ""):
        user_token_invalid.emit()
        return
    else:
        refresh_token()
    var to_remove : Array[String] = []
    for entry in eventsub_messages.keys():
        if (Time.get_ticks_msec() - eventsub_messages[entry] > 600000):
            to_remove.append(entry)
    for n in to_remove:
        eventsub_messages.erase(n)

It seems it waits 1 hour, then, if the token is invalid, it emits a signal and returns. If it's still valid, it calls refresh_token() recursively, which waits 1 hour, etc. So if, after the first hour, the token is invalid, the signal emits (does anything listen to this?) and returns without clearing the eventsub messages (though now that I'm looking at it closer, I guess it only cares about removing old stuff, so maybe that's fine?)

For my thing, I rewrote it like this, but I'm not 100% sure if that's correct, either:

func refresh_token() -> void:
    var tokens_valid := true
    while (tokens_valid):
        await(get_tree().create_timer(3600).timeout)
        if (await(validate_token_and_set_id(token_user["access_token"], ScopeType.SCOPE_TYPE_USER)) == ""):
            tokens_valid = false
        if (await(validate_token_and_set_id(token_channel["access_token"], ScopeType.SCOPE_TYPE_CHANNEL)) == ""):
            tokens_valid = false

        if (!tokens_valid):
            user_token_invalid.emit()
            var to_remove : Array[String] = []
            for entry in eventsub_messages.keys():
                if (Time.get_ticks_msec() - eventsub_messages[entry] > 600000):
                    to_remove.append(entry)
            for n in to_remove:
                eventsub_messages.erase(n)
            print("TOKENS INVALID!!!!!")

I did eventually run into the token becoming invalid after a few hours, and I'm not sure what to do after that -- just completely reconnect everything? Doesn't look like the original code handles that case either.

I can share more of my code if you'd like, but it's kind of a hot mess...

issork commented 1 year ago

Currently, it is intended that you handle invalid tokens yourself - for example by requesting a new one, I unfortunately did not find the time yet to add handling all cases and error messages. It is not important for the EventSub messages to be cleared, so I didn't bother with that because clearing after 1h if you decide to reconnect is fine too ^^

Receiving channel point redemption events should work in theory from the bot account by adding the channel:read:redemptions or channel:manage:redemptions scope, but I've also heard from issues with that approach which I couldn't find anything about in the documentation. My guess for one of the issues was that only the app that created the reward may receive notifications of its redemption.

jitspoe commented 1 year ago

Gotcha, so I'm supposed to connect to the invalid token event and get a new token? Also, if the token goes invalid, is there any down time between when the token went invalid and when the check happens that could result in lost events? Sorry, I'm new to all this twitch event stuff, and it's super confusing. 😅

The problem with the scopes is it gives it the scope to read the channel points on the bot's channel, not my channel. At least, I couldn't figure out how to do it -- either I had to be 100% on my account or the bot's account with a single token, so I made 2.

issork commented 1 year ago

The event subscription has a 'condition' parameter, make sure to pass in a dictionary with "broadcaster_user_id": "<your_broadcaster_user_id>" as an entry. (see https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelchannel_points_custom_reward_redemptionadd)

I think there is a downtime, but I'm not 100% sure.

issork commented 1 year ago

Took another look, Twitch does some guessing and sometimes resends events, but it's not guaranteed - so better take into account that an event might've been lost. Maybe by requesting reward redemptions from the regular API upon reconnect? (would also require you to create the rewards with the app itself)

issork commented 9 months ago

Just an update on this issue - I am currently rewriting the entire plugin and in the coming release multiple bot accounts will be possible by using the "force_verify" parameter, which will forcefully show the login window again on which you can change the account to be used.