mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
General NoTwitch EventSub Update
This commit is contained in:
parent
51046034e4
commit
1ffec81afb
9 changed files with 506 additions and 205 deletions
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -11,60 +11,103 @@ signal notif_received(data)
|
||||||
|
|
||||||
signal welcome_received()
|
signal welcome_received()
|
||||||
|
|
||||||
|
var keepalive_timer := 0
|
||||||
|
var timeout_time : int
|
||||||
|
|
||||||
|
## 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):
|
||||||
|
|
||||||
func _init(owner):
|
|
||||||
|
|
||||||
connection = owner
|
connection = owner
|
||||||
|
timeout_time = timeout
|
||||||
packet_received.connect(data_received)
|
|
||||||
|
packet_received.connect(data_received)
|
||||||
|
|
||||||
|
|
||||||
|
## 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]):
|
||||||
|
|
||||||
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():
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
var responses : Array[Twitch_Connection.HTTPResponse]
|
|
||||||
|
|
||||||
for all in events:
|
for all in events:
|
||||||
|
|
||||||
responses.append(connection.add_eventsub_subscription(all))
|
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]
|
||||||
|
|
||||||
|
for all in events:
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
match info.metadata.message_type:
|
match info.metadata.message_type:
|
||||||
|
|
||||||
"session_welcome":
|
"session_welcome":
|
||||||
|
|
||||||
session_id = info.payload.session.id
|
session_id = info.payload.session.id
|
||||||
welcome_received.emit()
|
welcome_received.emit()
|
||||||
|
|
||||||
|
|
||||||
"session_ping":
|
|
||||||
|
|
||||||
send_pong(info)
|
|
||||||
|
|
||||||
|
|
||||||
"notification":
|
|
||||||
|
|
||||||
notif_received.emit(info)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func send_pong(pong):
|
|
||||||
|
"session_ping":
|
||||||
pong.metadata.message_type = "session_pong"
|
|
||||||
send_text(str(pong))
|
send_pong(info)
|
||||||
|
|
||||||
|
|
||||||
|
"notification":
|
||||||
|
|
||||||
|
notif_received.emit(info)
|
||||||
|
|
||||||
|
|
||||||
|
"session_keepalive":
|
||||||
|
|
||||||
|
keepalive_timer = 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
print(info)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -35,306 +37,330 @@ var user_info : Dictionary
|
||||||
var responses : Array
|
var responses : Array
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
|
|
||||||
token_received.connect(cache_user_data.unbind(1))
|
token_received.connect(cache_user_data.unbind(1))
|
||||||
|
|
||||||
|
|
||||||
## Function that handles Caching the data of the Account tied to the Connections Token
|
## Function that handles Caching the data of the Account tied to the Connections Token
|
||||||
func cache_user_data():
|
func cache_user_data():
|
||||||
|
|
||||||
if token.is_empty():
|
if token.is_empty():
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
print("User Info Cached")
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Handles the basic Twitch Authentication process to request and then later receive a Token (using [method check_auth_peer]).
|
user_info = data.data
|
||||||
## Returns the authentication URL.
|
print("User Info Cached")
|
||||||
func authenticate_with_twitch(client_id = client_id, scopes : Array[String] = ["chat:read", "chat:edit"]) -> String:
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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_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)
|
||||||
|
|
||||||
# Connects to the Twitch IRC server.
|
# Connects to the Twitch IRC server.
|
||||||
chat_socket.connect_to_chat(token, request_twitch_info)
|
chat_socket.connect_to_chat(token, request_twitch_info)
|
||||||
await chat_socket.socket_open
|
await chat_socket.socket_open
|
||||||
|
|
||||||
if !default_chat.is_empty():
|
if !default_chat.is_empty():
|
||||||
|
|
||||||
chat_socket.join_chat(default_chat)
|
chat_socket.join_chat(default_chat)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel]
|
## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel]
|
||||||
func join_channel(channel : String):
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
## Sets up an EventSub connection to allow subscribing to EventSub events. Ex. Alerts, Channel Point Redemptions etc.
|
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):
|
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()))
|
||||||
|
|
||||||
|
|
||||||
func subscribe_to_channel_points(user_id : String):
|
func subscribe_to_channel_points(user_id : String):
|
||||||
|
|
||||||
if !eventsub_socket.socket_open:
|
if !eventsub_socket.socket_open:
|
||||||
|
|
||||||
push_error("NoTwitch Error: No EventSub Connection, please use setup_eventsub_connection first")
|
push_error("NoTwitch Error: No EventSub Connection, please use setup_eventsub_connection first")
|
||||||
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):
|
||||||
|
|
||||||
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
|
if auth_server and auth_server.is_listening() and auth_server.is_connection_available():
|
||||||
## [param scopes], Twitch Client ID ([param id]) and [param redirect_uri].
|
|
||||||
## [param id] defaults to [member client_id] and both [param redirect] and
|
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]
|
## [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()
|
||||||
|
|
||||||
var full_redirect_uri := redirect
|
var full_redirect_uri := redirect
|
||||||
if !port.is_empty():
|
if !port.is_empty():
|
||||||
full_redirect_uri += ":" + port
|
full_redirect_uri += ":" + port
|
||||||
|
|
||||||
var url := auth_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
|
return url
|
||||||
|
|
||||||
## Utility Function for making a generic HTTP Request to Twitch
|
## Utility Function for making a generic HTTP Request to Twitch
|
||||||
func twitch_request(url : String, method : HTTPClient.Method = HTTPClient.METHOD_GET, body := ""):
|
func twitch_request(url : String, method : HTTPClient.Method = HTTPClient.METHOD_GET, body := ""):
|
||||||
|
|
||||||
var headers : Array = ["Authorization: Bearer " + token, "Client-Id: " + client_id]
|
var headers : Array = ["Authorization: Bearer " + token, "Client-Id: " + client_id]
|
||||||
|
|
||||||
if !body.is_empty():
|
if !body.is_empty():
|
||||||
|
|
||||||
headers.append("Content-Type: application/json")
|
headers.append("Content-Type: application/json")
|
||||||
|
|
||||||
|
|
||||||
# Adds the Content type to the headers if we're actually sending some data along
|
# Adds the Content type to the headers if we're actually sending some data along
|
||||||
if method != HTTPClient.METHOD_GET:
|
if method != HTTPClient.METHOD_GET:
|
||||||
|
|
||||||
headers.append("Content-Type: application/json")
|
headers.append("Content-Type: application/json")
|
||||||
|
|
||||||
|
|
||||||
var http := HTTPRequest.new()
|
var http := HTTPRequest.new()
|
||||||
|
|
||||||
var resp = HTTPResponse.new(http, responses)
|
var resp = HTTPResponse.new(http, responses)
|
||||||
add_child(http)
|
add_child(http)
|
||||||
http.request(url, headers, method, body)
|
http.request(url, headers, method, body)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
## Wrapper function around [method twitch_request] to handle doing a Twitch "Get Users" request.
|
## Wrapper function around [method twitch_request] to handle doing a Twitch "Get Users" request.
|
||||||
func request_user_info(users : Array[String] = []):
|
func request_user_info(users : Array[String] = []):
|
||||||
|
|
||||||
var req_url := "https://api.twitch.tv/helix/users"
|
var req_url := "https://api.twitch.tv/helix/users"
|
||||||
|
|
||||||
if users.is_empty():
|
if users.is_empty():
|
||||||
|
|
||||||
return twitch_request(req_url)
|
return twitch_request(req_url)
|
||||||
|
|
||||||
|
|
||||||
var user_list : String = "?login=" + users[0]
|
var user_list : String = "?login=" + users[0]
|
||||||
for all in users:
|
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.
|
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] = []):
|
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="
|
||||||
|
|
||||||
if channels.is_empty():
|
if channels.is_empty():
|
||||||
|
|
||||||
if user_info.is_empty():
|
if user_info.is_empty():
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
channels.append(user_info.id)
|
channels.append(user_info.id)
|
||||||
|
|
||||||
|
|
||||||
var id_string := channels[0]
|
var id_string := channels[0]
|
||||||
for all in channels:
|
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.
|
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:
|
func make_state(len : int = 16) -> String:
|
||||||
|
|
||||||
var crypto = Crypto.new()
|
var crypto = Crypto.new()
|
||||||
var state = crypto.generate_random_bytes(len).hex_encode()
|
var state = crypto.generate_random_bytes(len).hex_encode()
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
func check_auth_peer(peer : StreamPeerTCP):
|
func check_auth_peer(peer : StreamPeerTCP):
|
||||||
|
|
||||||
var info = peer.get_utf8_string(peer.get_available_bytes())
|
var info = peer.get_utf8_string(peer.get_available_bytes())
|
||||||
printraw(info)
|
printraw(info)
|
||||||
|
|
||||||
var root := redirect_uri
|
var root := redirect_uri
|
||||||
if !redirect_port.is_empty():
|
if !redirect_port.is_empty():
|
||||||
root += ":" + redirect_port
|
root += ":" + redirect_port
|
||||||
root += "/"
|
root += "/"
|
||||||
|
|
||||||
var script = "<script>fetch('" + root + "' + window.location.hash.substr(1))</script>"
|
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())
|
peer.put_data(str("HTTP/1.1 200\n\n" + script).to_utf8_buffer())
|
||||||
|
|
||||||
|
|
||||||
var resp_state = info.split("&state=")
|
var resp_state = info.split("&state=")
|
||||||
|
|
||||||
# Ensures that the received state is correct.
|
# Ensures that the received state is correct.
|
||||||
if !resp_state.size() > 1 or resp_state[0] == state:
|
if !resp_state.size() > 1 or resp_state[0] == state:
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
var token = info.split("access_token=")[1].split("&scope=")[0].strip_edges()
|
var token = info.split("access_token=")[1].split("&scope=")[0].strip_edges()
|
||||||
printraw("Token: ", token, "\n")
|
printraw("Token: ", token, "\n")
|
||||||
self.token = token
|
self.token = token
|
||||||
token_received.emit(token)
|
token_received.emit(token)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
return
|
return
|
||||||
|
|
||||||
chat_received.emit(dict)
|
chat_received.emit(dict)
|
||||||
|
|
||||||
|
|
||||||
## "Promise" Class used for relaying the info received from the supplied [HTTPRequest] node.
|
## "Promise" Class used for relaying the info received from the supplied [HTTPRequest] node.
|
||||||
class HTTPResponse:
|
class HTTPResponse:
|
||||||
|
|
||||||
signal response_received
|
signal response_received
|
||||||
|
|
||||||
## The infuriatingly needed data variable. Because you can't await AND get signal data.
|
## The infuriatingly needed data variable. Because you can't await AND get signal data.
|
||||||
var inf_data
|
var inf_data
|
||||||
|
|
||||||
func _init(http : HTTPRequest, storage : Array):
|
func _init(http : HTTPRequest, storage : Array):
|
||||||
|
|
||||||
storage.append(self)
|
storage.append(self)
|
||||||
http.request_completed.connect(request_complete.bind(http, storage))
|
http.request_completed.connect(request_complete.bind(http, storage))
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
response_received.emit(info)
|
response_received.emit(info)
|
||||||
|
|
||||||
http.queue_free()
|
http.queue_free()
|
||||||
storage.erase(self)
|
storage.erase(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
condition = cond
|
condition = cond
|
||||||
session_id = sess_id
|
session_id = sess_id
|
||||||
|
|
||||||
|
|
||||||
## Returns back the information pertaining to the Subscription in the format needed for twitch.
|
## Returns back the information pertaining to the Subscription in the format needed for twitch.
|
||||||
func return_request_dictionary():
|
func return_request_dictionary():
|
||||||
|
|
||||||
var dict = {"type" : subscription_type, "version" : version, "condition" : condition, "transport" : {"method" : method, "session_id" : session_id}}
|
var dict = {"type" : subscription_type, "version" : version, "condition" : condition, "transport" : {"method" : method, "session_id" : session_id}}
|
||||||
|
|
||||||
return dict
|
return dict
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
class_name Connections
|
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):
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
56
classes/deck/nodes/twitch/twitch_eventsub_event.gd
Normal file
56
classes/deck/nodes/twitch/twitch_eventsub_event.gd
Normal 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
|
||||||
|
|
30
classes/deck/nodes/twitch/twitch_join_chat.gd
Normal file
30
classes/deck/nodes/twitch/twitch_join_chat.gd
Normal 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))
|
||||||
|
|
30
classes/deck/nodes/twitch/twitch_leave_chat.gd
Normal file
30
classes/deck/nodes/twitch/twitch_leave_chat.gd
Normal 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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue