Updated NoTwitch Addon to add EventSub Integration

This commit is contained in:
Eroax 2024-01-19 16:54:57 -07:00
parent bb7b7d0adb
commit 358b55e23c
8 changed files with 332 additions and 16 deletions

View file

@ -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 ## Wrapper class around [Websocket_Client] that handles Twitch Chat
@ -77,7 +78,6 @@ func parse_chat_msg(msg : String, tags : Dictionary):
return return
var msg_notice = msg.split(" ")[1] var msg_notice = msg.split(" ")[1]
match msg_notice: match msg_notice:

View file

@ -29,5 +29,5 @@ func join_chat():
func send_chat(chat : String): func send_chat(chat : String):
%Twitch_Connection.send_chat_to_channel(chat, %Channel.text) %Twitch_Connection.send_chat(chat, %Channel.text)

View file

@ -1,8 +1,38 @@
extends Control extends Control
var demo_events : Array[Twitch_Connection.EventSub_Subscription] = [
Twitch_Connection.EventSub_Subscription.new("channel.update", {"broadcaster_user_id" : ""}, "", "2")
]
func _ready(): func _ready():
$Twitch_Connection.token_received.connect(save_token) $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() load_token()
@ -23,5 +53,15 @@ func load_token():
var res = ResourceLoader.load("user://token.tres") var res = ResourceLoader.load("user://token.tres")
$Twitch_Connection.token = res.token $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)

View file

@ -76,6 +76,21 @@ unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "Send" 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="."] [node name="Twitch_Connection" type="Node" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("1_13a4v") script = ExtResource("1_13a4v")

View file

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

View file

@ -0,0 +1,4 @@
extends Resource
class_name TokenSaver
@export var token : String

View file

@ -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))

View file

@ -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 ## 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 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 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 state := make_state()
var token : String 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(): 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]). ## Handles the basic Twitch Authentication process to request and then later receive a Token (using [method check_auth_peer]).
## Returns the authentication URL. ## 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() auth_server = TCPServer.new()
var url := create_auth_url() var url := create_auth_url(scopes)
auth_server.listen(int(redirect_port)) auth_server.listen(int(redirect_port))
return url 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"): 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 = chat_socket_class.new()
chat_socket.close() chat_socket.chat_received_rich.connect(check_chat_socket)
# 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():
Connections.save_credentials({"token" : token, "channel" : default_chat}, "twitch")
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):
@ -62,6 +91,30 @@ 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.
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): func _process(delta):
if auth_server and auth_server.is_listening() and auth_server.is_connection_available(): 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() chat_socket.poll_socket()
if eventsub_socket:
eventsub_socket.poll_socket()
## Utility function for creating a Twitch Authentication URL with the given ## Utility function for creating a Twitch Authentication URL with the given
## [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] = ["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 var str_scopes : String
@ -92,10 +150,78 @@ func create_auth_url(scopes : Array[String] = ["chat:read", "chat:edit"], port :
if !port.is_empty(): if !port.is_empty():
full_redirect_uri += ":" + port 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 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. ## 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:
@ -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