General NoTwitch EventSub Update

This commit is contained in:
Eroax 2024-01-26 03:26:22 -07:00 committed by Lera Elvoé
parent 51046034e4
commit 1ffec81afb
No known key found for this signature in database
9 changed files with 506 additions and 205 deletions

View file

@ -5,5 +5,5 @@ extends Button
func _pressed(): func _pressed():
OS.shell_open(twitch_connection.authenticate_with_twitch(["channel:read:redemptions", "chat:read", "chat:edit"])) OS.shell_open(twitch_connection.authenticate_with_twitch())

View file

@ -11,34 +11,76 @@ signal notif_received(data)
signal welcome_received() signal welcome_received()
var keepalive_timer := 0
var timeout_time : int
func _init(owner): ## Dictionary used for storing all the subscribed events in the format "subscription_type : Twitch_Connection.EventSub_Subscription"
var subscribed_events : Dictionary
func _init(owner, timeout : int):
connection = owner connection = owner
timeout_time = timeout
packet_received.connect(data_received) packet_received.connect(data_received)
func connect_to_eventsub(events : Array[Twitch_Connection.EventSub_Subscription] = []): ## Overrides the default poll function for [Websocket_Client] to add functionality for a keepalive timer and reconnecting when the connection is lost.
func poll_socket():
super()
keepalive_timer += connection.get_process_delta_time()
if keepalive_timer >= timeout_time:
socket_closed.emit()
close()
connect_to_eventsub(subscribed_events.values())
## Handles setting up the connection to EventSub with an Array of the Events that should be subscribed to.
func connect_to_eventsub(events : Array[Twitch_Connection.EventSub_Subscription]):
connect_to_url(eventsub_url) connect_to_url(eventsub_url)
await welcome_received await welcome_received
if events.is_empty(): for all in events:
return subscribed_events[all.subscription_type] = all
return await subscribe_to_events(events)
## Utility function for subscribing to multiple Twitch EventSub events at once.
func subscribe_to_events(events : Array[Twitch_Connection.EventSub_Subscription]):
var responses : Array[Twitch_Connection.HTTPResponse] var responses : Array[Twitch_Connection.HTTPResponse]
for all in events: for all in events:
responses.append(connection.add_eventsub_subscription(all)) responses.append(await connection.add_eventsub_subscription(all))
if responses.size() == 1:
return responses[0]
return responses return responses
func new_eventsub_subscription(info, event_subscription : Twitch_Connection.EventSub_Subscription):
if !event_subscription in subscribed_events:
subscribed_events[event_subscription.subscription_type] = event_subscription
event_subscription.sub_id = info.data.id
func data_received(packet : PackedByteArray): func data_received(packet : PackedByteArray):
var info = JSON.parse_string(packet.get_string_from_utf8()) var info = JSON.parse_string(packet.get_string_from_utf8())
@ -61,10 +103,11 @@ func data_received(packet : PackedByteArray):
notif_received.emit(info) notif_received.emit(info)
"session_keepalive":
keepalive_timer = 0
func send_pong(pong):
pong.metadata.message_type = "session_pong" print(info)
send_text(str(pong))

View file

@ -12,10 +12,12 @@ signal chat_received_rich(chat_dict)
@export var client_id := "qyjg1mtby1ycs5scm1pvctos7yvyc1" @export var client_id := "qyjg1mtby1ycs5scm1pvctos7yvyc1"
@export var redirect_uri := "http://localhost" @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 ## 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" @export var redirect_port := "8000"
var auth_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&" var auth_url := "https://id.twitch.tv/oauth2/authorize?response_type=token&"
var auth_scopes : Array[String] = ["chat:read", "chat:edit", "channel:read:redemptions"]
var auth_server : TCPServer var auth_server : TCPServer
@ -50,23 +52,26 @@ func cache_user_data():
var resp = request_user_info() var resp = request_user_info()
resp.response_received.connect(func(data): resp.response_received.connect(func(data):
user_info = data user_info = data.data
print("User Info Cached") print("User Info Cached")
) )
## Handles the basic Twitch Authentication process to request and then later receive a Token (using [method check_auth_peer]). ## Handles the basic Twitch Authentication process to request and then later receive a Token
## Returns the authentication URL. ## (using [method check_auth_peer]). Returns the authentication URL.
func authenticate_with_twitch(client_id = client_id, scopes : Array[String] = ["chat:read", "chat:edit"]) -> String: func authenticate_with_twitch(client_id = client_id, scopes : Array[String] = auth_scopes) -> String:
auth_server = TCPServer.new() auth_server = TCPServer.new()
var url := create_auth_url(scopes) auth_scopes = scopes
var url := create_auth_url()
auth_server.listen(int(redirect_port)) auth_server.listen(int(redirect_port))
return url 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) ## 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"): func setup_chat_connection(default_chat : String = "", token : String = token, request_twitch_info = true, nick = "terribletwitch"):
chat_socket = chat_socket_class.new(self) chat_socket = chat_socket_class.new(self)
@ -88,19 +93,28 @@ func join_channel(channel : String):
chat_socket.join_chat(channel) 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 = ""): func send_chat(msg : String, channel : String = ""):
chat_socket.send_chat(msg, channel) chat_socket.send_chat(msg, channel)
## Sets up an EventSub connection to allow subscribing to EventSub events. Ex. Alerts, Channel Point Redemptions etc. ## 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): func setup_eventsub_connection(events : Array[EventSub_Subscription] = [], timeout_duration : int = 20):
eventsub_socket = eventsub_socket_class.new(self) eventsub_socket = eventsub_socket_class.new(self, timeout_duration)
var ret = await eventsub_socket.connect_to_eventsub(events) var ret = await eventsub_socket.connect_to_eventsub(events)
return ret return ret
## Adds a new subscription to a given EventSub Event using a [Twitch_Connection.EventSub_Subscription]
## to handle the data.
func add_eventsub_subscription(sub_type : EventSub_Subscription): 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())) return twitch_request("https://api.twitch.tv/helix/eventsub/subscriptions", HTTPClient.METHOD_POST, str(sub_type.return_request_dictionary()))
@ -114,7 +128,7 @@ func subscribe_to_channel_points(user_id : String):
return return
return add_eventsub_subscription(EventSub_Subscription.new("channel.channel_points_custom_reward_redemption.add", {"broadcaster_user_id" : user_id}, eventsub_socket.session_id)) 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): func _process(delta):
@ -139,11 +153,11 @@ func _process(delta):
## [param scopes], Twitch Client ID ([param id]) and [param redirect_uri]. ## [param scopes], Twitch Client ID ([param id]) and [param redirect_uri].
## [param id] defaults to [member client_id] and both [param redirect] and ## [param id] defaults to [member client_id] and both [param redirect] and
## [param redirect_port] ## [param redirect_port]
func create_auth_url(scopes : Array[String], port := redirect_port, id : String = client_id, redirect : String = redirect_uri) -> String: func create_auth_url(port := redirect_port, id : String = client_id, redirect : String = redirect_uri) -> String:
var str_scopes : String var str_scopes : String
for all in scopes: for all in auth_scopes:
str_scopes += " " + all str_scopes += " " + all
str_scopes = str_scopes.strip_edges() str_scopes = str_scopes.strip_edges()
@ -200,7 +214,8 @@ func request_user_info(users : Array[String] = []):
return twitch_request(req_url + user_list) 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. ## 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] = []): func request_channel_info(channels : Array[String] = []):
var req_url := "https://api.twitch.tv/helix/channels?broadcaster_id=" var req_url := "https://api.twitch.tv/helix/channels?broadcaster_id="
@ -224,7 +239,8 @@ func request_channel_info(channels : Array[String] = []):
return twitch_request(req_url + id_string) 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. ## 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: func make_state(len : int = 16) -> String:
var crypto = Crypto.new() var crypto = Crypto.new()
@ -264,8 +280,6 @@ func check_auth_peer(peer : StreamPeerTCP):
func check_chat_socket(dict, rich = false): func check_chat_socket(dict, rich = false):
prints(dict.user, dict.message)
if rich: if rich:
chat_received_rich.emit(dict) chat_received_rich.emit(dict)
@ -290,14 +304,21 @@ class HTTPResponse:
func request_complete(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, 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()) var info : Dictionary
if !body.is_empty():
info = JSON.parse_string(body.get_string_from_utf8())
info["result"] = result
info["code"] = response_code
info["headers"] = headers
if info.has("error"): if info.has("error"):
push_error("NoTwitch Twitch API Error: " + info.error + " " + info.message) push_error("NoTwitch Twitch API Error: " + str(info))
return return
info = info.data[0]
info["data"] = info.get("data", [[]])[0]
print("Response Received") print("Response Received")
inf_data = info inf_data = info
@ -308,21 +329,26 @@ class HTTPResponse:
## Data handler class for making it easier to send the data used for EventSub Subscriptions over [member eventsub_socket] ## Data handler class for making it easier to send the data used for EventSub Subscriptions over
## [member eventsub_socket]
class EventSub_Subscription: class EventSub_Subscription:
## Specifies the type of subscription this is representing (Ex. "Channel Update" is channel.update) ## Specifies the type of subscription this is representing (Ex. "Channel Update" is channel.update)
var subscription_type : String var subscription_type : String
## Used for setting the "version" the subscription that we're currently using. ## Used for setting the "version" the subscription that we're currently using.
var version : String 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) ## 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 var condition : Dictionary
## Holds the "session" ID for this EventSub Subscription. Which is held in [member eventsub_socket.session_id] ## Holds the "session" ID for this EventSub Subscription. Which is held in
## [member eventsub_socket.session_id]
var session_id : String var session_id : String
## Stores the subscription ID returned from the EventSub Subscription API Request
var sub_id
var method = "websocket" var method = "websocket"
func _init(sub_type : String, cond : Dictionary, sess_id : String, vers : String = "1"): func _init(sub_type : String, cond : Dictionary, sess_id : String = "", vers : String = "1"):
subscription_type = sub_type subscription_type = sub_type
version = vers version = vers

View file

@ -6,6 +6,11 @@ class_name Connections
static var obs_websocket static var obs_websocket
static var twitch static var twitch
static func _twitch_eventsub_event_received(event_data : Dictionary):
DeckHolder.send_event(&"twitch_eventsub", event_data)
static func _twitch_chat_received(msg_dict : Dictionary): static func _twitch_chat_received(msg_dict : Dictionary):
DeckHolder.send_event(&"twitch_chat", msg_dict) DeckHolder.send_event(&"twitch_chat", msg_dict)

View file

@ -0,0 +1,76 @@
# (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
var subscription_data : Twitch_Connection.EventSub_Subscription
func _init():
name = "Twitch Add EventSub Subscription"
node_type = name.to_snake_case()
description = "Adds a subscription to a specific Twitch EventSub Event with the given dictionary 'condition' for the data needed."
props_to_serialize = []
#Event Name
add_input_port(DeckType.Types.STRING, "Event Name", "field")
#Subscription Data
add_input_port(DeckType.Types.DICTIONARY, "Subscription Data")
#Trigger
add_input_port(DeckType.Types.ANY, "Add Subscription", "button")
func _receive(to_input_port, data: Variant, extra_data: Array = []):
if to_input_port != 2:
return
var input_data = await resolve_input_port_value_async(1)
if input_data == null or !"condition" in input_data.keys():
DeckHolder.logger.log_node(name + ": Incorrect Subscription Data Connected, please supply a Dictionary with condition and if needed, version. Last supplied Data was: " + str(input_data), Logger.LogType.ERROR)
return
var sub_type = await resolve_input_port_value_async(0)
# Creates an instance of Twitch_Connection.EventSub_Subscription to store the data with all the given inputs.
subscription_data = Twitch_Connection.EventSub_Subscription.new(sub_type, input_data.condition)
# Checks if the data has a version field, if so sets it on the EventSub_Subscription
if input_data.has("version"):
subscription_data.version = input_data.version
# Calls the connection to add the Subscription
var req = await Connections.twitch.add_eventsub_subscription(subscription_data)
req.response_received.connect(eventsub_subscription_response)
## Handles checking the [Twitch_Connection.HTTPResponse] returned by [method Twitch_Connection.add_eventsub_subscription] to ensure that it succeeded.
func eventsub_subscription_response(data):
match data.code:
202:
var succ_string = name + ": EventSub Subscription Added for " + subscription_data.subscription_type + " successfully"
DeckHolder.logger.log_node(succ_string, Logger.LogType.INFO)
Connections.twitch.eventsub_socket.notif_received.connect(Connections._twitch_eventsub_event_received)
_:
var error_string = name + ": Error" + data.code + " Received from Twitch when Subscribing to " + subscription_data.sub_type + " with " + str(subscription_data.return_request_dictionary)
DeckHolder.logger.log_node(error_string, Logger.LogType.ERROR)

View file

@ -0,0 +1,56 @@
# (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
var cached_event_data : Dictionary
func _init():
name = "Twitch EventSub Event"
node_type = name.to_snake_case()
description = "Listens for a specific Event from Twitch EventSub"
props_to_serialize = []
# Adds a port that allows specifying what type of event to listen for.
add_input_port(DeckType.Types.STRING, "Event Name", "field")
# Adds a port that outputs when the Event has been received
add_output_port(DeckType.Types.ANY, "Event Received")
# Adds a port that outputs the data received when the Event has been received.
add_output_port(DeckType.Types.DICTIONARY, "Event Data")
func _event_received(event_name: StringName, event_data: Dictionary = {}):
if event_name != &"twitch_eventsub":
return
var port_0 = await resolve_input_port_value_async(0)
print("Event Name ", event_data)
if port_0 == null or port_0 != event_data.payload.subscription.type:
return
# Sends to indicate that the specified event has happened.
send(0, null)
# Sends the data along as well as the fact that the event happened. While also caching the event data for later access
cached_event_data = event_data
# Sends the data along as well as the fact that the event happened. While also caching the event data for later access
cached_event_data = event_data
send(1, event_data)
func _value_request(port):
if port != 1:
return
return cached_event_data

View file

@ -0,0 +1,30 @@
# (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 Join Chat"
node_type = name.to_snake_case()
description = "Simple node to send a premade 'join' message to Twitch IRC allowing you to Join the given Channel and receive messages from it."
props_to_serialize = []
# Adds Input port for channel name
add_input_port(DeckType.Types.STRING, "Channel", "field")
# Adds Trigger for leaving the specified channel
add_input_port(DeckType.Types.ANY, "Leave Channel", "button")
func _receive(to_input_port, data: Variant, extra_data: Array = []):
if to_input_port != 1:
return
Connections.twitch.join_channel(await resolve_input_port_value_async(0))

View file

@ -0,0 +1,30 @@
# (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 Leave Chat"
node_type = name.to_snake_case()
description = "Simple node to send a premade 'leave' message to Twitch IRC. Allowing you to leave the specified channel and stop receiving from it."
props_to_serialize = []
# Adds Input port for channel name
add_input_port(DeckType.Types.STRING, "Channel", "field")
# Adds Trigger for leaving the specified channel
add_input_port(DeckType.Types.ANY, "Leave Channel", "button")
func _receive(to_input_port, data: Variant, extra_data: Array = []):
if to_input_port != 1:
return
Connections.twitch.leave_channel(await resolve_input_port_value_async(0))

View file

@ -0,0 +1,35 @@
# (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 Remove EventSub Subscription"
node_type = name.to_snake_case()
description = "Removes the given EventSub Subscription by it's Type."
props_to_serialize = []
add_input_port(DeckType.Types.STRING, "Subscription Type", "field")
add_input_port(DeckType.Types.ANY, "Remove Subscription", "button")
func _receive(to_input_port, data: Variant, extra_data: Array = []):
if !to_input_port == 1:
return
var sub_type = await resolve_input_port_value_async(0)
Connections.twitch.remove_eventsub_subscription_type(sub_type).response_completed.connect(eventsub_response_received)
func eventsub_response_received(info):
pass # TODO: Add Error Handling Later