From 358b55e23cf1a5eda5c333ca8cc84b8bee51b6af Mon Sep 17 00:00:00 2001 From: Eroax Date: Fri, 19 Jan 2024 16:54:57 -0700 Subject: [PATCH 01/11] Updated NoTwitch Addon to add EventSub Integration --- addons/no_twitch/chat_socket.gd | 4 +- addons/no_twitch/demo/Chat_Join.gd | 2 +- addons/no_twitch/demo/demo_scene.gd | 40 +++++ addons/no_twitch/demo/demo_scene.tscn | 15 ++ addons/no_twitch/demo/test_button.gd | 2 +- addons/no_twitch/demo/token_saver.gd | 4 + addons/no_twitch/eventsub_socket.gd | 70 +++++++++ addons/no_twitch/twitch_connection.gd | 211 ++++++++++++++++++++++++-- 8 files changed, 332 insertions(+), 16 deletions(-) create mode 100644 addons/no_twitch/demo/token_saver.gd create mode 100644 addons/no_twitch/eventsub_socket.gd diff --git a/addons/no_twitch/chat_socket.gd b/addons/no_twitch/chat_socket.gd index eb7f1dc..bdc52cf 100644 --- a/addons/no_twitch/chat_socket.gd +++ b/addons/no_twitch/chat_socket.gd @@ -1,4 +1,5 @@ -extends "res://addons/no_twitch/websocket_client.gd" +extends Websocket_Client +class_name Chat_Socket ## Wrapper class around [Websocket_Client] that handles Twitch Chat @@ -77,7 +78,6 @@ func parse_chat_msg(msg : String, tags : Dictionary): return - var msg_notice = msg.split(" ")[1] match msg_notice: diff --git a/addons/no_twitch/demo/Chat_Join.gd b/addons/no_twitch/demo/Chat_Join.gd index 6bc2608..aa3998a 100644 --- a/addons/no_twitch/demo/Chat_Join.gd +++ b/addons/no_twitch/demo/Chat_Join.gd @@ -29,5 +29,5 @@ func join_chat(): func send_chat(chat : String): - %Twitch_Connection.send_chat_to_channel(chat, %Channel.text) + %Twitch_Connection.send_chat(chat, %Channel.text) diff --git a/addons/no_twitch/demo/demo_scene.gd b/addons/no_twitch/demo/demo_scene.gd index 1af75dd..e03a31d 100644 --- a/addons/no_twitch/demo/demo_scene.gd +++ b/addons/no_twitch/demo/demo_scene.gd @@ -1,8 +1,38 @@ extends Control +var demo_events : Array[Twitch_Connection.EventSub_Subscription] = [ + + Twitch_Connection.EventSub_Subscription.new("channel.update", {"broadcaster_user_id" : ""}, "", "2") + +] + func _ready(): $Twitch_Connection.token_received.connect(save_token) + %Get_User.pressed.connect(func(): + + var resp = %Twitch_Connection.request_user_info() + resp.response_received.connect(print_http_result) + + ) + + %Get_Channel.pressed.connect(func(): + + var resp = %Twitch_Connection.request_channel_info() + resp.response_received.connect(print_http_result) + + ) + + %Connect_Channel_Points.pressed.connect(func(): + + await %Twitch_Connection.setup_eventsub_connection() + print("Passed Await") + %Twitch_Connection.eventsub_socket.notif_received.connect(eventsub_notif_received) + + var resp = %Twitch_Connection.subscribe_to_channel_points(%Twitch_Connection.user_info.id) + resp.response_received.connect(print_http_result) + + ) load_token() @@ -23,5 +53,15 @@ func load_token(): var res = ResourceLoader.load("user://token.tres") $Twitch_Connection.token = res.token + %Twitch_Connection.cache_user_data() +func print_http_result(data): + + print(data) + + +func eventsub_notif_received(info): + print(info) + printt(info.payload.event.reward.id, info.payload.event.user_input, info.payload.event.reward.title) + diff --git a/addons/no_twitch/demo/demo_scene.tscn b/addons/no_twitch/demo/demo_scene.tscn index a636685..c3435cd 100644 --- a/addons/no_twitch/demo/demo_scene.tscn +++ b/addons/no_twitch/demo/demo_scene.tscn @@ -76,6 +76,21 @@ unique_name_in_owner = true layout_mode = 2 text = "Send" +[node name="Get_User" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Get User Info" + +[node name="Get_Channel" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Get Channel Info" + +[node name="Connect_Channel_Points" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Connect Channel Points" + [node name="Twitch_Connection" type="Node" parent="."] unique_name_in_owner = true script = ExtResource("1_13a4v") diff --git a/addons/no_twitch/demo/test_button.gd b/addons/no_twitch/demo/test_button.gd index 7592510..61a1962 100644 --- a/addons/no_twitch/demo/test_button.gd +++ b/addons/no_twitch/demo/test_button.gd @@ -5,5 +5,5 @@ extends Button func _pressed(): - twitch_connection.authenticate_with_twitch() + OS.shell_open(twitch_connection.authenticate_with_twitch(["channel:read:redemptions", "chat:read", "chat:edit"])) diff --git a/addons/no_twitch/demo/token_saver.gd b/addons/no_twitch/demo/token_saver.gd new file mode 100644 index 0000000..b7e1d43 --- /dev/null +++ b/addons/no_twitch/demo/token_saver.gd @@ -0,0 +1,4 @@ +extends Resource +class_name TokenSaver + +@export var token : String diff --git a/addons/no_twitch/eventsub_socket.gd b/addons/no_twitch/eventsub_socket.gd new file mode 100644 index 0000000..682db60 --- /dev/null +++ b/addons/no_twitch/eventsub_socket.gd @@ -0,0 +1,70 @@ +extends Websocket_Client + +const eventsub_url := "wss://eventsub.wss.twitch.tv/ws" + +## Stores the "session id" for this EventSub connection. +var session_id + +var connection : Twitch_Connection + +signal notif_received(data) + +signal welcome_received() + + +func _init(owner): + + connection = owner + + packet_received.connect(data_received) + + +func connect_to_eventsub(events : Array[Twitch_Connection.EventSub_Subscription] = []): + + connect_to_url(eventsub_url) + await welcome_received + + if events.is_empty(): + + return + + + var responses : Array[Twitch_Connection.HTTPResponse] + + for all in events: + + responses.append(connection.add_eventsub_subscription(all)) + + + return responses + + +func data_received(packet : PackedByteArray): + + var info = JSON.parse_string(packet.get_string_from_utf8()) + + match info.metadata.message_type: + + "session_welcome": + + session_id = info.payload.session.id + welcome_received.emit() + + + "session_ping": + + send_pong(info) + + + "notification": + + notif_received.emit(info) + + + + +func send_pong(pong): + + pong.metadata.message_type = "session_pong" + send_text(str(pong)) + diff --git a/addons/no_twitch/twitch_connection.gd b/addons/no_twitch/twitch_connection.gd index 437dfb0..65d8c30 100644 --- a/addons/no_twitch/twitch_connection.gd +++ b/addons/no_twitch/twitch_connection.gd @@ -12,45 +12,74 @@ signal token_received(token : String) ## 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 twitch_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&" +var auth_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&" var auth_server : TCPServer -## Websocket used for handling the chat connection. -var chat_socket = preload("res://addons/no_twitch/chat_socket.gd").new() + +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(): - chat_socket.chat_received.connect(check_chat_socket) + 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 + 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) -> String: +func authenticate_with_twitch(scopes : Array[String] = ["chat:read", "chat:edit"], client_id = client_id) -> String: auth_server = TCPServer.new() - var url := create_auth_url() + var url := create_auth_url(scopes) auth_server.listen(int(redirect_port)) return url -## Sets up a single 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) +## 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"): - # Temporary, closes the socket to allow connecting multiple times without needing to reset. - chat_socket.close() + chat_socket = chat_socket_class.new() + chat_socket.chat_received_rich.connect(check_chat_socket) + # 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(): - Connections.save_credentials({"token" : token, "channel" : default_chat}, "twitch") chat_socket.join_chat(default_chat) + ## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel] func join_channel(channel : String): @@ -62,6 +91,30 @@ 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) + var ret = await eventsub_socket.connect_to_eventsub(events) + return ret + + +func add_eventsub_subscription(sub_type : EventSub_Subscription): + + 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 + + + return 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(): @@ -74,12 +127,17 @@ func _process(delta): 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(scopes : Array[String] = ["chat:read", "chat:edit"], port := redirect_port, id : String = client_id, redirect : String = redirect_uri) -> String: +func create_auth_url(scopes : Array[String], port := redirect_port, id : String = client_id, redirect : String = redirect_uri) -> String: var str_scopes : String @@ -92,10 +150,78 @@ func create_auth_url(scopes : Array[String] = ["chat:read", "chat:edit"], port : if !port.is_empty(): full_redirect_uri += ":" + port - var url := twitch_url + "client_id=" + id + "&redirect_uri=" + full_redirect_uri + "&scope=" + str_scopes + "&state=" + str(state) + 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 := ""): + + 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) + + ## 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: @@ -140,3 +266,64 @@ func check_chat_socket(dict): + +## "Promise" Class used for relaying the info received from the supplied [HTTPRequest] node. +class HTTPResponse: + + signal response_received + + 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()) + + if info.has("error"): + + push_error("NoTwitch Twitch API Error: " + info.error + " " + info.message) + return + + info = info.data[0] + + print("Response Received") + 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" + + 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 + + From 717b69c35c92f3400f8b08e3375de074391cf9d2 Mon Sep 17 00:00:00 2001 From: Eroax Date: Sun, 21 Jan 2024 21:42:11 -0700 Subject: [PATCH 02/11] Updated NoTwitch to allow #50 --- addons/no_twitch/chat_socket.gd | 7 ++++++- addons/no_twitch/twitch_connection.gd | 13 ++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/addons/no_twitch/chat_socket.gd b/addons/no_twitch/chat_socket.gd index bdc52cf..b5ef776 100644 --- a/addons/no_twitch/chat_socket.gd +++ b/addons/no_twitch/chat_socket.gd @@ -16,7 +16,10 @@ var user_regex := RegEx.new() var user_pattern := r":([\w]+)!" -func _init(): +func _init(owner : Twitch_Connection): + + chat_received_rich.connect(owner.check_chat_socket.bind(true)) + chat_received.connect(owner.check_chat_socket) packet_received.connect(data_received) @@ -38,6 +41,8 @@ func connect_to_chat(token, extra = false, nick = "terribletwitch"): send_text("CAP REQ :twitch.tv/commands twitch.tv/tags") + + ## Handles checking the received packet from [signal Websocket_Client.packet_received] func data_received(packet : PackedByteArray): diff --git a/addons/no_twitch/twitch_connection.gd b/addons/no_twitch/twitch_connection.gd index 65d8c30..dc73a5a 100644 --- a/addons/no_twitch/twitch_connection.gd +++ b/addons/no_twitch/twitch_connection.gd @@ -7,6 +7,9 @@ class_name Twitch_Connection 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 @@ -66,8 +69,7 @@ func authenticate_with_twitch(scopes : Array[String] = ["chat:read", "chat:edit" ## 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() - chat_socket.chat_received_rich.connect(check_chat_socket) + chat_socket = chat_socket_class.new(self) # Connects to the Twitch IRC server. chat_socket.connect_to_chat(token, request_twitch_info) @@ -260,11 +262,16 @@ func check_auth_peer(peer : StreamPeerTCP): token_received.emit(token) -func check_chat_socket(dict): +func check_chat_socket(dict, rich = false): prints(dict.user, dict.message) + 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. From 4d2af08807f9cdfee492d5a4b27d49658f285c7e Mon Sep 17 00:00:00 2001 From: Eroax Date: Sun, 21 Jan 2024 22:50:21 -0700 Subject: [PATCH 03/11] Patches DeckHolderRenderer breaking with recent NoTwitch Update --- graph_node_renderer/deck_holder_renderer.gd | 2 +- graph_node_renderer/deck_holder_renderer.tscn | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graph_node_renderer/deck_holder_renderer.gd b/graph_node_renderer/deck_holder_renderer.gd index c4817bc..2bf1cac 100644 --- a/graph_node_renderer/deck_holder_renderer.gd +++ b/graph_node_renderer/deck_holder_renderer.gd @@ -107,7 +107,7 @@ func _ready() -> void: Connections.obs_websocket = no_obsws Connections.twitch = %Twitch_Connection - Connections.twitch.chat_socket.chat_received_rich.connect(Connections._twitch_chat_received) + Connections.twitch.chat_received_rich.connect(Connections._twitch_chat_received) file_popup_menu.set_item_shortcut(FileMenuId.NEW, new_deck_shortcut) file_popup_menu.set_item_shortcut(FileMenuId.OPEN, open_deck_shortcut) diff --git a/graph_node_renderer/deck_holder_renderer.tscn b/graph_node_renderer/deck_holder_renderer.tscn index ec9988b..f57e0b8 100644 --- a/graph_node_renderer/deck_holder_renderer.tscn +++ b/graph_node_renderer/deck_holder_renderer.tscn @@ -8,9 +8,9 @@ [ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="5_3n36q"] [ext_resource type="PackedScene" uid="uid://eioso6jb42jy" path="res://graph_node_renderer/obs_websocket_setup_dialog.tscn" id="5_uo2gj"] [ext_resource type="PackedScene" uid="uid://bq2lxmbnic4lc" path="res://graph_node_renderer/twitch_setup_dialog.tscn" id="7_7rhap"] -[ext_resource type="PackedScene" uid="uid://cuwou2aa7qfc2" path="res://graph_node_renderer/unsaved_changes_dialog_single_deck.tscn" id="8_qf6ve"] -[ext_resource type="PackedScene" uid="uid://cvvkj138fg8jg" path="res://graph_node_renderer/unsaved_changes_dialog.tscn" id="9_4n0q6"] -[ext_resource type="PackedScene" uid="uid://bu466w2w3q08c" path="res://graph_node_renderer/about_dialog.tscn" id="11_6ln7n"] +[ext_resource type="PackedScene" path="res://graph_node_renderer/unsaved_changes_dialog_single_deck.tscn" id="8_qf6ve"] +[ext_resource type="PackedScene" path="res://graph_node_renderer/unsaved_changes_dialog.tscn" id="9_4n0q6"] +[ext_resource type="PackedScene" path="res://graph_node_renderer/about_dialog.tscn" id="11_6ln7n"] [sub_resource type="InputEventKey" id="InputEventKey_giamc"] device = -1 From 89ec9a8ac3337340206d366b71e4177b28de2c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Mon, 22 Jan 2024 08:06:23 +0000 Subject: [PATCH 04/11] fix nodes resizing on port update (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/55 Co-authored-by: Lera ElvoƩ Co-committed-by: Lera ElvoƩ --- graph_node_renderer/deck_node_renderer_graph_node.gd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graph_node_renderer/deck_node_renderer_graph_node.gd b/graph_node_renderer/deck_node_renderer_graph_node.gd index f985006..76f2322 100644 --- a/graph_node_renderer/deck_node_renderer_graph_node.gd +++ b/graph_node_renderer/deck_node_renderer_graph_node.gd @@ -87,6 +87,9 @@ func _on_node_ports_updated() -> void: for port in node.get_all_ports(): update_port(port) + + await get_tree().process_frame + size = Vector2() func _on_node_renamed(new_name: String) -> void: From 792b414c64e4016a6bb256b3c5d8aef8461e5cdc Mon Sep 17 00:00:00 2001 From: Eroax Date: Sun, 21 Jan 2024 22:51:48 -0700 Subject: [PATCH 05/11] Patched Twitch_Setup_Dialog erroring after NoTwitch Update --- graph_node_renderer/twitch_setup_dialog.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph_node_renderer/twitch_setup_dialog.gd b/graph_node_renderer/twitch_setup_dialog.gd index 7091404..b092cb8 100644 --- a/graph_node_renderer/twitch_setup_dialog.gd +++ b/graph_node_renderer/twitch_setup_dialog.gd @@ -42,7 +42,7 @@ func copy_auth_link(): func connect_to_chat(): - if Connections.twitch.chat_socket.get_ready_state() != WebSocketPeer.STATE_OPEN: + if !Connections.twitch.get("chat_socket") or Connections.twitch.chat_socket.get_ready_state() != WebSocketPeer.STATE_OPEN: Connections.twitch.setup_chat_connection(%Default_Chat.text) From 3426f17173d3683783135e7a9152da501535b53e Mon Sep 17 00:00:00 2001 From: Eroax Date: Mon, 22 Jan 2024 20:03:45 -0700 Subject: [PATCH 06/11] Updated NoTwitch's Twitch_Connection.HTTPResponse --- addons/no_twitch/twitch_connection.gd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/no_twitch/twitch_connection.gd b/addons/no_twitch/twitch_connection.gd index dc73a5a..6a5de96 100644 --- a/addons/no_twitch/twitch_connection.gd +++ b/addons/no_twitch/twitch_connection.gd @@ -279,6 +279,9 @@ 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) @@ -297,6 +300,7 @@ class HTTPResponse: info = info.data[0] print("Response Received") + inf_data = info response_received.emit(info) http.queue_free() From 21c9e43978b6a290afa46f751cdeffc5adfc16ef Mon Sep 17 00:00:00 2001 From: Eroax Date: Mon, 22 Jan 2024 20:04:24 -0700 Subject: [PATCH 07/11] Added User Info Caching to Twitch_Setup_Dialog --- graph_node_renderer/twitch_setup_dialog.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/graph_node_renderer/twitch_setup_dialog.gd b/graph_node_renderer/twitch_setup_dialog.gd index b092cb8..12b4adf 100644 --- a/graph_node_renderer/twitch_setup_dialog.gd +++ b/graph_node_renderer/twitch_setup_dialog.gd @@ -21,6 +21,7 @@ func authenticate_twitch(): Connections.twitch.token = loaded_creds.data.token if loaded_creds.data.has("channel"): %Default_Chat.text = loaded_creds.data.channel + Connections.twitch.cache_user_data() connect_to_chat() return From 5feb293bdfd5eb9cf6a31a8bd31c83e66db07216 Mon Sep 17 00:00:00 2001 From: Eroax Date: Mon, 22 Jan 2024 20:04:52 -0700 Subject: [PATCH 08/11] Patched Twitch_Setup_Dialog trying to join Chat Early A return may or may not have been forgotten to stop early joining chat. --- graph_node_renderer/twitch_setup_dialog.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/graph_node_renderer/twitch_setup_dialog.gd b/graph_node_renderer/twitch_setup_dialog.gd index 12b4adf..05d8dfe 100644 --- a/graph_node_renderer/twitch_setup_dialog.gd +++ b/graph_node_renderer/twitch_setup_dialog.gd @@ -45,6 +45,7 @@ func connect_to_chat(): if !Connections.twitch.get("chat_socket") or Connections.twitch.chat_socket.get_ready_state() != WebSocketPeer.STATE_OPEN: Connections.twitch.setup_chat_connection(%Default_Chat.text) + return Connections.twitch.join_channel(%Default_Chat.text) From c7c2e0ca6bdb34b23e92294646b65691b9fcf474 Mon Sep 17 00:00:00 2001 From: Eroax Date: Mon, 22 Jan 2024 20:05:46 -0700 Subject: [PATCH 09/11] Added async equivalent to resolve_input_port_value For some stuff an async version of this function was needed. This is *probably* going to be removed when we move most of the stuff to being async. But for this branch at least this allows some nodes to work. --- classes/deck/deck_node.gd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/classes/deck/deck_node.gd b/classes/deck/deck_node.gd index dd46b26..3876f31 100644 --- a/classes/deck/deck_node.gd +++ b/classes/deck/deck_node.gd @@ -319,6 +319,15 @@ func resolve_input_port_value(input_port: int) -> Variant: else: return null +func resolve_input_port_value_async(input_port: int) -> Variant: + if await request_value_async(input_port) != null: + return await request_value_async(input_port) + elif get_input_ports()[input_port].value_callback.get_object() && get_input_ports()[input_port].value_callback.call() != null: + return get_input_ports()[input_port].value_callback.call() + elif get_input_ports()[input_port].value != null: + return get_input_ports()[input_port].value + else: + return null ## Returns a [Dictionary] representation of this node. func to_dict(with_meta: bool = true) -> Dictionary: From aaec27e763ce7935c0ae442f468b2fae001565f3 Mon Sep 17 00:00:00 2001 From: Eroax Date: Mon, 22 Jan 2024 20:07:37 -0700 Subject: [PATCH 10/11] Added "Twitch Connected Account Info" Node Adds in a node that allows returning back the connected Twitch accounts cached data. It has 3 specific outputs (Login, Display Name, Profile Picture URL) and a general "Dictionary" output for the full dictionary of information. --- .../deck/nodes/twitch/twitch_account_info.gd | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 classes/deck/nodes/twitch/twitch_account_info.gd diff --git a/classes/deck/nodes/twitch/twitch_account_info.gd b/classes/deck/nodes/twitch/twitch_account_info.gd new file mode 100644 index 0000000..9478d03 --- /dev/null +++ b/classes/deck/nodes/twitch/twitch_account_info.gd @@ -0,0 +1,71 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends DeckNode + + +func _init(): + name = "Twitch Connected Account Info" + node_type = name.to_snake_case() + description = "Accesses the Cached data for the Connected Account" + + props_to_serialize = [] + + add_output_port(DeckType.Types.STRING, "Login") + add_output_port(DeckType.Types.STRING, "Display Name") + add_output_port(DeckType.Types.STRING, "Profile Picture URL") + add_output_port(DeckType.Types.DICTIONARY, "User Dictionary") + +#{ + #"id": "141981764", + #"login": "twitchdev", + #"display_name": "TwitchDev", + #"type": "", + #"broadcaster_type": "partner", + #"description": "Supporting third-party developers building Twitch integrations from chatbots to game integrations.", + #"profile_image_url": "https://static-cdn.jtvnw.net/jtv_user_pictures/8a6381c7-d0c0-4576-b179-38bd5ce1d6af-profile_image-300x300.png", + #"offline_image_url": "https://static-cdn.jtvnw.net/jtv_user_pictures/3f13ab61-ec78-4fe6-8481-8682cb3b0ac2-channel_offline_image-1920x1080.png", + #"view_count": 5980557, + #"email": "not-real@email.com", + #"created_at": "2016-12-14T20:32:28Z" +#} + + +func _value_request(from_port): + + var user_dict = Connections.twitch.user_info + + if user_dict.is_empty(): + + DeckHolder.logger.log_node("Twitch Connection: No Data Cached for the connected Twitch Account, please try Connecting again", Logger.LogType.ERROR) + return null + + + match from_port: + + 0: # Login Name + + return user_dict.login + + + 1: # Display Name + + return user_dict.display_name + + + 2: # Profile Picture URL + + return user_dict.profile_image_url + + + _: # Dictionary + + return user_dict + + + + + return Connections.twitch.user_info + + + From 2a3307f70129fe0e685c33238254e55072e658d7 Mon Sep 17 00:00:00 2001 From: Eroax Date: Mon, 22 Jan 2024 20:09:04 -0700 Subject: [PATCH 11/11] Added Twitch Request User Info Adds in a node that exposes access to the Twitch API "Get Users" request. --- .../nodes/twitch/twitch_request_user_info.gd | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 classes/deck/nodes/twitch/twitch_request_user_info.gd diff --git a/classes/deck/nodes/twitch/twitch_request_user_info.gd b/classes/deck/nodes/twitch/twitch_request_user_info.gd new file mode 100644 index 0000000..5b3b9a9 --- /dev/null +++ b/classes/deck/nodes/twitch/twitch_request_user_info.gd @@ -0,0 +1,65 @@ +# (c) 2023-present Eroax +# (c) 2023-present Yagich +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +extends DeckNode + + +func _init(): + name = "Twitch Request User Info" + node_type = name.to_snake_case() + description = "Requests to the Twitch API for the User info for the supplied Login or ID" + + props_to_serialize = [] + +# Adds the User input. + add_input_port(DeckType.Types.STRING, "User Login", "field") + +# Adds the Checkbox for using IDs vs Logins and sets it to true. + add_input_port(DeckType.Types.BOOL, "User Input as ID", "checkbox") + get_input_ports()[1].set_value(true) + + get_input_ports()[1].value_updated.connect(switch_user_input_type) + + add_output_port(DeckType.Types.ANY, "User Info") + + +func switch_user_input_type(value): + + var user_port = get_input_ports()[0] + + if value: + + user_port.label = "User ID" + + else: + + user_port.label = "User Login" + + + ports_updated.emit() + + + +func _value_request(from_port): + + var user_input = resolve_input_port_value(0) + var as_id = resolve_input_port_value(1) + + var req_url := "https://api.twitch.tv/helix/users?" + if as_id: + + req_url += "id=" + user_input + + else: + + req_url += "login=" + user_input + + + var resp = Connections.twitch.twitch_request(req_url) + + await resp.response_received + var data = resp.inf_data + + return data + +