extends Node class_name No_Twitch ## Handler for a Connection to a Single Twitch Account ## ## Used for getting authentication and handling websocket connections. signal token_received(token : String) signal chat_received(chat_dict) signal chat_received_rich(chat_dict) @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" var auth_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&" var auth_scopes : Array[String] = ["chat:read", "chat:edit", "channel:manage:redemptions", "bits:read", "channel:read:goals", "channel:manage:polls", "channel:read:hype_train", "channel:manage:predictions", "moderator:read:followers", "user:read:chat", "channel:read:subscriptions", "channel:moderate", "channel:read:charity", "moderator:read:shield_mode", "moderator:manage:shoutouts"] var auth_server : TCPServer 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 var state := make_state() var token : String ## The User data of the account that the authorized [member token] is connected to. var user_info : Dictionary var responses : Array func _ready(): 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): user_info = data.data print("User Info Cached") ) ## Handles the basic Twitch Authentication process to request and then later receive a Token ## (using [method check_auth_peer]). Returns the authentication URL. func authenticate_with_twitch(client_id = client_id, scopes : Array[String] = auth_scopes) -> String: auth_server = TCPServer.new() auth_scopes = scopes var url := create_auth_url() auth_server.listen(int(redirect_port)) return url ## 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) func setup_chat_connection(default_chat : String = "", token : String = token, request_twitch_info = true, nick = "terribletwitch"): chat_socket = chat_socket_class.new(self) # 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) ## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel] func join_channel(channel : String): chat_socket.join_chat(channel) ## Leaves or "PART"s the given [param channel] over IRC. (Sends PART #[param channel]) func leave_channel(channel : String): chat_socket.leave_chat(channel) func send_chat(msg : String, channel : String = ""): chat_socket.send_chat(msg, channel) ## 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): eventsub_socket = eventsub_socket_class.new(self, timeout_duration) var ret = await eventsub_socket.connect_to_eventsub(events) return ret ## Adds a new subscription to a given EventSub Event using a [No_Twitch.EventSub_Subscription] ## to handle the data. func add_eventsub_subscription(sub_type : EventSub_Subscription): if eventsub_socket == null: return await setup_eventsub_connection([sub_type]) if sub_type.session_id == "": sub_type.session_id = eventsub_socket.session_id 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("No_Twitch Error: No EventSub Connection, please use setup_eventsub_connection first") return return await add_eventsub_subscription(EventSub_Subscription.new("channel.channel_points_custom_reward_redemption.add", {"broadcaster_user_id" : user_id}, eventsub_socket.session_id)) 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() if eventsub_socket: eventsub_socket.poll_socket() ## 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] func create_auth_url(port := redirect_port, id : String = client_id, redirect : String = redirect_uri) -> String: var str_scopes : String for all in auth_scopes: str_scopes += " " + all str_scopes = str_scopes.strip_edges() var full_redirect_uri := redirect if !port.is_empty(): full_redirect_uri += ":" + port var url := auth_url + "client_id=" + id + "&redirect_uri=" + full_redirect_uri + "&scope=" + str_scopes + "&state=" + str(state) return url ## Utility Function for making a generic HTTP Request to Twitch func twitch_request(url : String, method : HTTPClient.Method = HTTPClient.METHOD_GET, body := "") -> HTTPResponse: var headers : Array = ["Authorization: Bearer " + token, "Client-Id: " + client_id] # 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) ## 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 = "" 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) func check_chat_socket(dict, rich = false): if rich: chat_received_rich.emit(dict) return 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 : Dictionary var str = body.get_string_from_utf8() if !body.is_empty() and !JSON.parse_string(str) == null: info = JSON.parse_string(body.get_string_from_utf8()) info["result"] = result info["code"] = response_code info["headers"] = headers if info.has("error"): push_error("No_Twitch Twitch API Error: " + str(info)) emit_response(info, http, storage) return if info.data is Array and info.data.size() == 1: info.data = info.data[0] # Revisit later cause Twitch weirdness #info["data"] = info.get("data", [[]])[0] print("Response Received") emit_response(info, http, storage) func emit_response(info, http, storage): 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 ## Stores the subscription ID returned from the EventSub Subscription API Request var sub_id var method = "websocket" func _init(sub_type : String, cond : Dictionary, sess_id : String = "", vers : String = "1"): 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