2023-12-15 22:44:25 +01:00
extends Node
class_name Twitch_Connection
## Handler for a Connection to a Single Twitch Account
##
## Used for getting authentication and handling websocket connections.
signal token_received ( token : String )
2024-01-25 07:36:05 +01:00
signal chat_received ( chat_dict )
signal chat_received_rich ( chat_dict )
2023-12-15 22:44:25 +01:00
@ export var client_id : = " qyjg1mtby1ycs5scm1pvctos7yvyc1 "
@ export var redirect_uri : = " http://localhost "
## Port that the redirect_uri will head to on your local system. Defaults to 80 for most cases. Linux tends to prefer 8000 or possibly 1338
@ export var redirect_port : = " 8000 "
2024-01-25 07:36:05 +01:00
var auth_url : = " https://id.twitch.tv/oauth2/authorize?response_type=token& "
2024-01-26 11:26:22 +01:00
var auth_scopes : Array [ String ] = [ " chat:read " , " chat:edit " , " channel:read:redemptions " ]
2023-12-15 22:44:25 +01:00
var auth_server : TCPServer
2024-01-25 07:36:05 +01:00
const chat_socket_class = preload ( " res://addons/no_twitch/chat_socket.gd " )
const eventsub_socket_class = preload ( " res://addons/no_twitch/eventsub_socket.gd " )
## [Websocket_Client] used for receiving all data from Twitch pertaining to chat (Ex. IRC Chat Messages)
var chat_socket : Websocket_Client
## [Websocket_Client] handles all the data from Twitch pertaining to EventSub (Ex. General Alerts)
var eventsub_socket : Websocket_Client
2023-12-15 22:44:25 +01:00
var state : = make_state ( )
var token : String
2024-01-25 07:36:05 +01:00
## The User data of the account that the authorized [member token] is connected to.
var user_info : Dictionary
var responses : Array
2023-12-15 22:44:25 +01:00
func _ready ( ) :
2024-01-25 07:36:05 +01:00
token_received . connect ( cache_user_data . unbind ( 1 ) )
## Function that handles Caching the data of the Account tied to the Connections Token
func cache_user_data ( ) :
if token . is_empty ( ) :
return
var resp = request_user_info ( )
resp . response_received . connect ( func ( data ) :
2024-01-26 11:30:07 +01:00
user_info = data . data
2024-01-25 07:36:05 +01:00
print ( " User Info Cached " )
)
2023-12-15 22:44:25 +01:00
2023-12-16 12:23:17 +01:00
## Handles the basic Twitch Authentication process to request and then later receive a Token (using [method check_auth_peer]).
## Returns the authentication URL.
2024-01-26 11:26:22 +01:00
func authenticate_with_twitch ( client_id = client_id , scopes : Array [ String ] = auth_scopes ) - > String :
2023-12-15 22:44:25 +01:00
auth_server = TCPServer . new ( )
2024-01-26 11:26:22 +01:00
auth_scopes = scopes
var url : = create_auth_url ( )
2023-12-15 22:44:25 +01:00
auth_server . listen ( int ( redirect_port ) )
2023-12-16 12:23:17 +01:00
return url
2023-12-15 22:44:25 +01:00
2024-01-25 07:36:05 +01:00
## Sets up the chat connection. Joining 1 room with [param token] as it's "PASS" specifying what account it's on. And the optional [param default_chat] specifying the default room to join. While [param nick] specifies the "nickname" (Not the username on Twitch)
2023-12-15 22:44:25 +01:00
func setup_chat_connection ( default_chat : String = " " , token : String = token , request_twitch_info = true , nick = " terribletwitch " ) :
2024-01-25 07:36:05 +01:00
chat_socket = chat_socket_class . new ( self )
2023-12-15 22:44:25 +01:00
# Connects to the Twitch IRC server.
chat_socket . connect_to_chat ( token , request_twitch_info )
await chat_socket . socket_open
if ! default_chat . is_empty ( ) :
chat_socket . join_chat ( default_chat )
2024-01-25 07:36:05 +01:00
2023-12-15 22:44:25 +01:00
## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel]
func join_channel ( channel : String ) :
chat_socket . join_chat ( channel )
func send_chat ( msg : String , channel : String = " " ) :
chat_socket . send_chat ( msg , channel )
2024-01-25 07:36:05 +01:00
## Sets up an EventSub connection to allow subscribing to EventSub events. Ex. Alerts, Channel Point Redemptions etc.
func setup_eventsub_connection ( events : Array [ EventSub_Subscription ] = [ ] , timeout_duration : int = 20 ) :
2024-02-09 10:05:55 +01:00
eventsub_socket = eventsub_socket_class . new ( self , timeout_duration )
2024-01-25 07:36:05 +01:00
var ret = await eventsub_socket . connect_to_eventsub ( events )
return ret
func add_eventsub_subscription ( sub_type : EventSub_Subscription ) :
2024-01-26 11:28:35 +01:00
if eventsub_socket == null :
return await setup_eventsub_connection ( [ sub_type ] )
if sub_type . session_id == " " :
sub_type . session_id = eventsub_socket . session_id
2024-01-25 07:36:05 +01:00
return twitch_request ( " https://api.twitch.tv/helix/eventsub/subscriptions " , HTTPClient . METHOD_POST , str ( sub_type . return_request_dictionary ( ) ) )
func subscribe_to_channel_points ( user_id : String ) :
if ! eventsub_socket . socket_open :
push_error ( " NoTwitch Error: No EventSub Connection, please use setup_eventsub_connection first " )
return
2024-01-26 11:28:35 +01:00
return await add_eventsub_subscription ( EventSub_Subscription . new ( " channel.channel_points_custom_reward_redemption.add " , { " broadcaster_user_id " : user_id } , eventsub_socket . session_id ) )
2024-01-25 07:36:05 +01:00
2023-12-15 22:44:25 +01:00
func _process ( delta ) :
if auth_server and auth_server . is_listening ( ) and auth_server . is_connection_available ( ) :
check_auth_peer ( auth_server . take_connection ( ) )
if chat_socket :
chat_socket . poll_socket ( )
2024-01-25 07:36:05 +01:00
if eventsub_socket :
eventsub_socket . poll_socket ( )
2023-12-15 22:44:25 +01:00
## Utility function for creating a Twitch Authentication URL with the given
## [param scopes], Twitch Client ID ([param id]) and [param redirect_uri].
## [param id] defaults to [member client_id] and both [param redirect] and
## [param redirect_port]
2024-01-26 11:26:22 +01:00
func create_auth_url ( port : = redirect_port , id : String = client_id , redirect : String = redirect_uri ) - > String :
2023-12-15 22:44:25 +01:00
var str_scopes : String
2024-01-26 11:26:22 +01:00
for all in auth_scopes :
2023-12-15 22:44:25 +01:00
str_scopes += " " + all
str_scopes = str_scopes . strip_edges ( )
var full_redirect_uri : = redirect
if ! port . is_empty ( ) :
full_redirect_uri += " : " + port
2024-01-25 07:36:05 +01:00
var url : = auth_url + " client_id= " + id + " &redirect_uri= " + full_redirect_uri + " &scope= " + str_scopes + " &state= " + str ( state )
2023-12-15 22:44:25 +01:00
return url
2024-01-25 07:36:05 +01:00
## Utility Function for making a generic HTTP Request to Twitch
func twitch_request ( url : String , method : HTTPClient . Method = HTTPClient . METHOD_GET , body : = " " ) :
var headers : Array = [ " Authorization: Bearer " + token , " Client-Id: " + client_id ]
if ! body . is_empty ( ) :
headers . append ( " Content-Type: application/json " )
# Adds the Content type to the headers if we're actually sending some data along
if method != HTTPClient . METHOD_GET :
headers . append ( " Content-Type: application/json " )
var http : = HTTPRequest . new ( )
var resp = HTTPResponse . new ( http , responses )
add_child ( http )
http . request ( url , headers , method , body )
return resp
## Wrapper function around [method twitch_request] to handle doing a Twitch "Get Users" request.
func request_user_info ( users : Array [ String ] = [ ] ) :
var req_url : = " https://api.twitch.tv/helix/users "
if users . is_empty ( ) :
return twitch_request ( req_url )
var user_list : String = " ?login= " + users [ 0 ]
for all in users :
user_list += " &login= " + all
return twitch_request ( req_url + user_list )
## Wrapper function around [method twitch_request] to grab information about a set of channels based off an Array of Channel IDs Ex. stream title, current category, current tags, etc.
func request_channel_info ( channels : Array [ String ] = [ ] ) :
var req_url : = " https://api.twitch.tv/helix/channels?broadcaster_id= "
if channels . is_empty ( ) :
if user_info . is_empty ( ) :
return
channels . append ( user_info . id )
var id_string : = channels [ 0 ]
for all in channels :
id_string += " &broadcaster_id= " + all
return twitch_request ( req_url + id_string )
2023-12-15 22:44:25 +01:00
## Utility function for creating a "state" used for different requests for some extra security as a semi password.
func make_state ( len : int = 16 ) - > String :
var crypto = Crypto . new ( )
var state = crypto . generate_random_bytes ( len ) . hex_encode ( )
return state
func check_auth_peer ( peer : StreamPeerTCP ) :
var info = peer . get_utf8_string ( peer . get_available_bytes ( ) )
printraw ( info )
var root : = redirect_uri
if ! redirect_port . is_empty ( ) :
root += " : " + redirect_port
root += " / "
var script = " <script>fetch( ' " + root + " ' + window.location.hash.substr(1))</script> "
peer . put_data ( str ( " HTTP/1.1 200 \n \n " + script ) . to_utf8_buffer ( ) )
var resp_state = info . split ( " &state= " )
# Ensures that the received state is correct.
if ! resp_state . size ( ) > 1 or resp_state [ 0 ] == state :
return
var token = info . split ( " access_token= " ) [ 1 ] . split ( " &scope= " ) [ 0 ] . strip_edges ( )
printraw ( " Token: " , token , " \n " )
self . token = token
token_received . emit ( token )
2024-01-25 07:36:05 +01:00
func check_chat_socket ( dict , rich = false ) :
2023-12-15 22:44:25 +01:00
prints ( dict . user , dict . message )
2024-01-25 07:36:05 +01:00
if rich :
chat_received_rich . emit ( dict )
return
2023-12-15 22:44:25 +01:00
2024-01-25 07:36:05 +01:00
chat_received . emit ( dict )
## "Promise" Class used for relaying the info received from the supplied [HTTPRequest] node.
class HTTPResponse :
signal response_received
## The infuriatingly needed data variable. Because you can't await AND get signal data.
var inf_data
func _init ( http : HTTPRequest , storage : Array ) :
storage . append ( self )
http . request_completed . connect ( request_complete . bind ( http , storage ) )
func request_complete ( result : int , response_code : int , headers : PackedStringArray , body : PackedByteArray , http , storage ) :
var info = JSON . parse_string ( body . get_string_from_utf8 ( ) )
2024-01-26 11:30:07 +01:00
info [ " result " ] = result
info [ " code " ] = response_code
info [ " headers " ] = headers
2024-01-25 07:36:05 +01:00
if info . has ( " error " ) :
2024-01-26 11:30:07 +01:00
push_error ( " NoTwitch Twitch API Error: " + str ( info ) )
2024-01-25 07:36:05 +01:00
return
2024-01-26 11:30:07 +01:00
info [ " data " ] = info . data [ 0 ]
2024-01-25 07:36:05 +01:00
print ( " Response Received " )
inf_data = info
response_received . emit ( info )
http . queue_free ( )
storage . erase ( self )
## Data handler class for making it easier to send the data used for EventSub Subscriptions over [member eventsub_socket]
class EventSub_Subscription :
## Specifies the type of subscription this is representing (Ex. "Channel Update" is channel.update)
var subscription_type : String
## Used for setting the "version" the subscription that we're currently using.
var version : String
## Stores the "condition", AKA the condition for being able to subscribe to this Event. (Ex. The User ID of the channel you're listening to)
var condition : Dictionary
## Holds the "session" ID for this EventSub Subscription. Which is held in [member eventsub_socket.session_id]
var session_id : String
var method = " websocket "
2024-01-26 11:28:35 +01:00
func _init ( sub_type : String , cond : Dictionary , sess_id : String = " " , vers : String = " 1 " ) :
2024-01-25 07:36:05 +01:00
subscription_type = sub_type
version = vers
condition = cond
session_id = sess_id
## Returns back the information pertaining to the Subscription in the format needed for twitch.
func return_request_dictionary ( ) :
var dict = { " type " : subscription_type , " version " : version , " condition " : condition , " transport " : { " method " : method , " session_id " : session_id } }
return dict
2023-12-15 22:44:25 +01:00