mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
TWITCH INTEGRATION (#13)
Implemented a basic Twitch Connection, + Twitch Receive Chat and Twitch Send Chat Co-authored-by: Eroax <eroaxe.business@gmail.com> Reviewed-on: https://codeberg.org/Eroax/StreamGraph/pulls/13
This commit is contained in:
parent
dbdda4614a
commit
c071e4d644
18 changed files with 709 additions and 3 deletions
166
addons/no_twitch/chat_socket.gd
Normal file
166
addons/no_twitch/chat_socket.gd
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
extends "res://addons/no_twitch/websocket_client.gd"
|
||||||
|
|
||||||
|
## Wrapper class around [Websocket_Client] that handles Twitch Chat
|
||||||
|
|
||||||
|
signal chat_received
|
||||||
|
signal chat_received_raw
|
||||||
|
signal chat_received_rich
|
||||||
|
|
||||||
|
signal chat_connected
|
||||||
|
|
||||||
|
var channels : Array[String]
|
||||||
|
var extra_info : bool
|
||||||
|
|
||||||
|
var user_regex := RegEx.new()
|
||||||
|
var user_pattern := r":([\w]+)!"
|
||||||
|
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
|
||||||
|
packet_received.connect(data_received)
|
||||||
|
|
||||||
|
user_regex.compile(user_pattern)
|
||||||
|
|
||||||
|
|
||||||
|
## Connects to the Twitch IRC server.
|
||||||
|
func connect_to_chat(token, extra = false, nick = "terribletwitch"):
|
||||||
|
|
||||||
|
extra_info = extra
|
||||||
|
|
||||||
|
connect_to_url("wss://irc-ws.chat.twitch.tv:443")
|
||||||
|
await socket_open
|
||||||
|
send_text("PASS oauth:" + token)
|
||||||
|
send_text("NICK " + nick)
|
||||||
|
|
||||||
|
if extra:
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
# Gets the text from the packet, strips the end and splits the different lines
|
||||||
|
var messages = packet.get_string_from_utf8().strip_edges(false).split("\r\n")
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
# Checks if this is a message that has tags enabled and if so, parses out the tags.
|
||||||
|
var tags : Dictionary
|
||||||
|
if msg.begins_with("@"):
|
||||||
|
|
||||||
|
# Grabs the actual end of the string with the message in it.
|
||||||
|
var real_msg = msg.split(" ", false, 1)
|
||||||
|
msg = real_msg[1]
|
||||||
|
|
||||||
|
# Loops through all the tags splitting them up by ; and then by = to get the keys and values.
|
||||||
|
for tag in real_msg[0].split(";"):
|
||||||
|
|
||||||
|
var key_value = tag.split("=")
|
||||||
|
tags[key_value[0]] = key_value[1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parse_chat_msg(msg, tags)
|
||||||
|
|
||||||
|
|
||||||
|
#@badge-info=subscriber/34;badges=broadcaster/1,subscriber/6,game-developer/1;client-nonce=b5009ae3ee034a7706d86fe221882925;color=#2E8B57;display-name=EroAxee;emotes=;first-msg=0;flags=;id=be05dae8-4067-4edf-83f2-e6be02974904;mod=0;returning-chatter=0;room-id=160349129;subscriber=1;tmi-sent-ts=1702009303249;turbo=0;user-id=160349129;user-type= :eroaxee!eroaxee@eroaxee.tmi.twitch.tv PRIVMSG #eroaxee :More
|
||||||
|
|
||||||
|
## Parses the given [param msg] [String]
|
||||||
|
func parse_chat_msg(msg : String, tags : Dictionary):
|
||||||
|
|
||||||
|
var msg_dict : Dictionary
|
||||||
|
|
||||||
|
if msg == "PING :tmi.twitch.tv":
|
||||||
|
|
||||||
|
send_text(msg.replace("PING", "PONG"))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
var msg_notice = msg.split(" ")[1]
|
||||||
|
match msg_notice:
|
||||||
|
|
||||||
|
"PRIVMSG":
|
||||||
|
|
||||||
|
var space_split = msg.split(" ", true, 3)
|
||||||
|
|
||||||
|
msg_dict["username"] = user_regex.search(msg).get_string(1)
|
||||||
|
msg_dict["message"] = space_split[3].trim_prefix(":")
|
||||||
|
msg_dict["channel"] = space_split[2].trim_prefix("#")
|
||||||
|
msg_dict.merge(parse_tags(tags))
|
||||||
|
prints(msg_dict.username, msg_dict.message, msg_dict.channel)
|
||||||
|
|
||||||
|
#(__username_regex.search(split[0]).get_string(1), split[3].right(1), split[2], tags)
|
||||||
|
|
||||||
|
if !tags.is_empty():
|
||||||
|
|
||||||
|
chat_received_rich.emit(msg_dict)
|
||||||
|
return
|
||||||
|
|
||||||
|
chat_received.emit(msg_dict)
|
||||||
|
|
||||||
|
|
||||||
|
# Connection Message
|
||||||
|
"001":
|
||||||
|
|
||||||
|
prints("Connection Established", msg)
|
||||||
|
chat_connected.emit()
|
||||||
|
|
||||||
|
# Chat Joining Message
|
||||||
|
"JOIN":
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Chat Leaving Message
|
||||||
|
"PART":
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#@badge-info=subscriber/34;badges=broadcaster/1,subscriber/6,game-developer/1;client-nonce=02d73777ab1fab1aee33ada1830d52b5;color=#2E8B57;display-name=EroAxee;emotes=;first-msg=0;flags=;id=4ff91a8c-b965-43f8-85a1-ddd541a2b438;mod=0;returning-chatter=0;room-id=160349129;subscriber=1;tmi-sent-ts=1701850826667;turbo=0;user-id=160349129;user-type= :eroaxee!eroaxee@eroaxee.tmi.twitch.tv PRIVMSG #eroaxee :Stuff
|
||||||
|
|
||||||
|
## Utility function that takes a Dictionary of tags from a Twitch message and parses them to be slightly more usable.
|
||||||
|
func parse_tags(tags : Dictionary):
|
||||||
|
|
||||||
|
var new_tags : Dictionary
|
||||||
|
for all in tags.keys():
|
||||||
|
|
||||||
|
if all == "badges":
|
||||||
|
|
||||||
|
tags[all] = tags[all].split(",")
|
||||||
|
|
||||||
|
|
||||||
|
new_tags[all.replace("-", "_")] = tags[all]
|
||||||
|
|
||||||
|
|
||||||
|
return new_tags
|
||||||
|
#{ "@badge-info": "subscriber/34", "badges": "broadcaster/1,subscriber/6,game-developer/1", "client-nonce": "b2e3524806f51c94cadd61d338bc14ed", "color": "#2E8B57", "display-name": "EroAxee", "emotes": "", "first-msg": "0", "flags": "", "id": "494dc47e-0d9c-4407-83ec-309764e1adf3", "mod": "0", "returning-chatter": "0", "room-id": "160349129", "subscriber": "1", "tmi-sent-ts": "1701853794297", "turbo": "0", "user-id": "160349129", "user-type": "" }
|
||||||
|
|
||||||
|
## Wrapper function around [method WebSocketPeer.send_text]
|
||||||
|
func send_chat(msg : String, channel : String = ""):
|
||||||
|
|
||||||
|
if channel.is_empty():
|
||||||
|
|
||||||
|
channel = channels[0]
|
||||||
|
|
||||||
|
|
||||||
|
channel = channel.strip_edges()
|
||||||
|
|
||||||
|
send_text("PRIVMSG #" + channel + " :" + msg + "\r\n")
|
||||||
|
|
||||||
|
|
||||||
|
## Utility function that handles joining the supplied [param channel]'s chat.
|
||||||
|
func join_chat(channel : String):
|
||||||
|
|
||||||
|
send_text("JOIN #" + channel + "\r\n")
|
||||||
|
channels.append(channel)
|
||||||
|
|
||||||
|
|
||||||
|
## Utility function that handles leaving the supplied [param channel]'s chat.
|
||||||
|
func leave_chat(channel : String):
|
||||||
|
|
||||||
|
send_chat("PART #" + channel)
|
||||||
|
channels.erase(channel)
|
||||||
|
|
33
addons/no_twitch/demo/Chat_Join.gd
Normal file
33
addons/no_twitch/demo/Chat_Join.gd
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
extends HBoxContainer
|
||||||
|
|
||||||
|
var channel : String
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
|
||||||
|
%Channel_Input.text_changed.connect(update_channel)
|
||||||
|
%Join_Chat.pressed.connect(join_chat)
|
||||||
|
%Start_Chat_Connection.pressed.connect(start_chat_connection)
|
||||||
|
|
||||||
|
%Chat_Msg.text_submitted.connect(send_chat)
|
||||||
|
%Send_Button.pressed.connect(func(): send_chat(%Chat_Msg.text))
|
||||||
|
|
||||||
|
|
||||||
|
func update_channel(new_channel):
|
||||||
|
|
||||||
|
channel = new_channel
|
||||||
|
|
||||||
|
|
||||||
|
func start_chat_connection():
|
||||||
|
|
||||||
|
%Twitch_Connection.setup_chat_connection(channel)
|
||||||
|
|
||||||
|
|
||||||
|
func join_chat():
|
||||||
|
|
||||||
|
%Twitch_Connection.join_channel(channel)
|
||||||
|
|
||||||
|
|
||||||
|
func send_chat(chat : String):
|
||||||
|
|
||||||
|
%Twitch_Connection.send_chat_to_channel(chat, %Channel.text)
|
||||||
|
|
27
addons/no_twitch/demo/demo_scene.gd
Normal file
27
addons/no_twitch/demo/demo_scene.gd
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
|
||||||
|
$Twitch_Connection.token_received.connect(save_token)
|
||||||
|
|
||||||
|
load_token()
|
||||||
|
|
||||||
|
|
||||||
|
func save_token(token):
|
||||||
|
|
||||||
|
var res = TokenSaver.new()
|
||||||
|
res.token = token
|
||||||
|
ResourceSaver.save(res, "user://token.tres")
|
||||||
|
|
||||||
|
|
||||||
|
func load_token():
|
||||||
|
|
||||||
|
if !FileAccess.file_exists("user://token.tres"):
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
var res = ResourceLoader.load("user://token.tres")
|
||||||
|
$Twitch_Connection.token = res.token
|
||||||
|
|
||||||
|
|
81
addons/no_twitch/demo/demo_scene.tscn
Normal file
81
addons/no_twitch/demo/demo_scene.tscn
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
[gd_scene load_steps=5 format=3 uid="uid://dhss3lpo1mhke"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/no_twitch/twitch_connection.gd" id="1_13a4v"]
|
||||||
|
[ext_resource type="Script" path="res://addons/no_twitch/demo/demo_scene.gd" id="1_ebv0f"]
|
||||||
|
[ext_resource type="Script" path="res://addons/no_twitch/demo/test_button.gd" id="1_hhhwv"]
|
||||||
|
[ext_resource type="Script" path="res://addons/no_twitch/demo/Chat_Join.gd" id="2_b8f4l"]
|
||||||
|
|
||||||
|
[node name="Control" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_ebv0f")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -206.0
|
||||||
|
offset_top = -68.0
|
||||||
|
offset_right = 211.0
|
||||||
|
offset_bottom = 68.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="Authenticate" type="Button" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Authenticate"
|
||||||
|
script = ExtResource("1_hhhwv")
|
||||||
|
|
||||||
|
[node name="Start_Chat_Connection" type="Button" parent="VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Start Connection"
|
||||||
|
|
||||||
|
[node name="Chat_Join" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
script = ExtResource("2_b8f4l")
|
||||||
|
|
||||||
|
[node name="Channel_Input" type="LineEdit" parent="VBoxContainer/Chat_Join"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Channel"
|
||||||
|
|
||||||
|
[node name="Join_Chat" type="Button" parent="VBoxContainer/Chat_Join"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Join"
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Channel" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 0.45
|
||||||
|
placeholder_text = "Channel"
|
||||||
|
|
||||||
|
[node name="Chat_Msg" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Chat Message"
|
||||||
|
|
||||||
|
[node name="Send_Button" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Send"
|
||||||
|
|
||||||
|
[node name="Twitch_Connection" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("1_13a4v")
|
9
addons/no_twitch/demo/test_button.gd
Normal file
9
addons/no_twitch/demo/test_button.gd
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
extends Button
|
||||||
|
|
||||||
|
|
||||||
|
@onready var twitch_connection : Twitch_Connection = $"../../Twitch_Connection"
|
||||||
|
|
||||||
|
func _pressed():
|
||||||
|
|
||||||
|
twitch_connection.authenticate_with_twitch()
|
||||||
|
|
4
addons/no_twitch/demo/token_saver.gd
Normal file
4
addons/no_twitch/demo/token_saver.gd
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
extends Resource
|
||||||
|
class_name TokenSaver
|
||||||
|
|
||||||
|
@export var token : String
|
7
addons/no_twitch/plugin.cfg
Normal file
7
addons/no_twitch/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="No Twitch"
|
||||||
|
description=""
|
||||||
|
author="Eroax"
|
||||||
|
version="0.0.1"
|
||||||
|
script="plugin.gd"
|
12
addons/no_twitch/plugin.gd
Normal file
12
addons/no_twitch/plugin.gd
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
# Initialization of the plugin goes here.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
# Clean-up of the plugin goes here.
|
||||||
|
pass
|
131
addons/no_twitch/twitch_connection.gd
Normal file
131
addons/no_twitch/twitch_connection.gd
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
extends Node
|
||||||
|
class_name Twitch_Connection
|
||||||
|
|
||||||
|
## Handler for a Connection to a Single Twitch Account
|
||||||
|
##
|
||||||
|
## Used for getting authentication and handling websocket connections.
|
||||||
|
|
||||||
|
signal token_received(token : String)
|
||||||
|
|
||||||
|
@export var client_id := "qyjg1mtby1ycs5scm1pvctos7yvyc1"
|
||||||
|
@export var redirect_uri := "http://localhost"
|
||||||
|
## Port that the redirect_uri will head to on your local system. Defaults to 80 for most cases. Linux tends to prefer 8000 or possibly 1338
|
||||||
|
@export var redirect_port := "80"
|
||||||
|
|
||||||
|
var twitch_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()
|
||||||
|
|
||||||
|
var state := make_state()
|
||||||
|
var token : String
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
|
||||||
|
chat_socket.chat_received.connect(check_chat_socket)
|
||||||
|
|
||||||
|
|
||||||
|
## Handles the basic Twitch Authentication process to request and then later receive a Token (using [method check_auth_peer])
|
||||||
|
func authenticate_with_twitch(client_id = client_id):
|
||||||
|
auth_server = TCPServer.new()
|
||||||
|
|
||||||
|
OS.shell_open(create_auth_url())
|
||||||
|
auth_server.listen(int(redirect_port))
|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
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()
|
||||||
|
# Connects to the Twitch IRC server.
|
||||||
|
chat_socket.connect_to_chat(token, request_twitch_info)
|
||||||
|
await chat_socket.socket_open
|
||||||
|
|
||||||
|
if !default_chat.is_empty():
|
||||||
|
|
||||||
|
chat_socket.join_chat(default_chat)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Joins the given [param channel] over IRC. Essentially just sending JOIN #[param channel]
|
||||||
|
func join_channel(channel : String):
|
||||||
|
|
||||||
|
chat_socket.join_chat(channel)
|
||||||
|
|
||||||
|
|
||||||
|
func send_chat(msg : String, channel : String = ""):
|
||||||
|
|
||||||
|
chat_socket.send_chat(msg, channel)
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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):
|
||||||
|
|
||||||
|
var str_scopes : String
|
||||||
|
|
||||||
|
for all in scopes:
|
||||||
|
|
||||||
|
str_scopes += " " + all
|
||||||
|
str_scopes = str_scopes.strip_edges()
|
||||||
|
|
||||||
|
var url = twitch_url + "client_id=" + id + "&redirect_uri=" + redirect + "&scope=" + str_scopes + "&state=" + str(state)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
## Utility function for creating a "state" used for different requests for some extra security as a semi password.
|
||||||
|
func make_state(len : int = 16) -> String:
|
||||||
|
|
||||||
|
var crypto = Crypto.new()
|
||||||
|
var state = crypto.generate_random_bytes(len).hex_encode()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
func check_auth_peer(peer : StreamPeerTCP):
|
||||||
|
|
||||||
|
var info = peer.get_utf8_string(peer.get_available_bytes())
|
||||||
|
printraw(info)
|
||||||
|
|
||||||
|
var script = "<script>fetch('http://localhost/' + window.location.hash.substr(1))</script>"
|
||||||
|
|
||||||
|
peer.put_data(str("HTTP/1.1 200\n\n" + script).to_utf8_buffer())
|
||||||
|
|
||||||
|
|
||||||
|
var resp_state = info.split("&state=")
|
||||||
|
|
||||||
|
# Ensures that the received state is correct.
|
||||||
|
if !resp_state.size() > 1 or resp_state[0] == state:
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
var token = info.split("access_token=")[1].split("&scope=")[0].strip_edges()
|
||||||
|
printraw("Token: ", token, "\n")
|
||||||
|
self.token = token
|
||||||
|
token_received.emit(token)
|
||||||
|
|
||||||
|
|
||||||
|
func check_chat_socket(dict):
|
||||||
|
|
||||||
|
prints(dict.user, dict.message)
|
||||||
|
|
||||||
|
|
||||||
|
|
58
addons/no_twitch/websocket_client.gd
Normal file
58
addons/no_twitch/websocket_client.gd
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
extends WebSocketPeer
|
||||||
|
class_name Websocket_Client
|
||||||
|
|
||||||
|
## Helper Class for handling the freaking polling of a WebSocketPeer
|
||||||
|
|
||||||
|
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||||
|
## [member WebSocketPeer.STATE_OPEN]
|
||||||
|
signal socket_open
|
||||||
|
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||||
|
## [member WebSocketPeer.STATE_CLOSED]
|
||||||
|
signal socket_closed(close_code, close_reason)
|
||||||
|
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||||
|
## [member WebSocketPeer.STATE_CONNECTING]
|
||||||
|
signal socket_connecting
|
||||||
|
## Emitted when [method WebSocketPeer.get_ready_state()] returns
|
||||||
|
## [member WebSocketPeer.STATE_CLOSING]
|
||||||
|
signal socket_closing
|
||||||
|
## Emitted when [method WebSocketPeer.get_available_packets()] returns greater
|
||||||
|
## than 0. Or, when a packet has been received.
|
||||||
|
signal packet_received(packet_data)
|
||||||
|
|
||||||
|
## Works as a wrapper around [method WebSocketPeer.poll] to handle the logic of
|
||||||
|
## checking get_ready_state() more simply.
|
||||||
|
func poll_socket():
|
||||||
|
|
||||||
|
poll()
|
||||||
|
|
||||||
|
var state = get_ready_state()
|
||||||
|
|
||||||
|
match state:
|
||||||
|
|
||||||
|
STATE_OPEN:
|
||||||
|
|
||||||
|
socket_open.emit(get_connected_host(), get_connected_port())
|
||||||
|
|
||||||
|
if get_available_packet_count() > 0:
|
||||||
|
|
||||||
|
packet_received.emit(get_packet())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
STATE_CONNECTING:
|
||||||
|
|
||||||
|
socket_connecting.emit()
|
||||||
|
|
||||||
|
|
||||||
|
STATE_CLOSING:
|
||||||
|
|
||||||
|
socket_closing.emit()
|
||||||
|
|
||||||
|
|
||||||
|
STATE_CLOSED:
|
||||||
|
|
||||||
|
socket_closed.emit(get_close_code(), get_close_reason())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
class_name Connections
|
class_name Connections
|
||||||
|
|
||||||
static var obs_websocket
|
static var obs_websocket
|
||||||
|
static var twitch
|
||||||
|
|
||||||
|
static func _twitch_chat_received(msg_dict : Dictionary):
|
||||||
|
|
||||||
|
DeckHolder.send_event(&"twitch_chat", msg_dict)
|
||||||
|
|
||||||
|
|
28
classes/deck/nodes/twitch_chat_received.gd
Normal file
28
classes/deck/nodes/twitch_chat_received.gd
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
extends DeckNode
|
||||||
|
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
name = "Twitch Chat Received"
|
||||||
|
node_type = "twitch_chat_received"
|
||||||
|
description = "Receives Twitch Chat Events from a Twitch Connection"
|
||||||
|
category = "twitch"
|
||||||
|
|
||||||
|
add_output_port(DeckType.Types.STRING, "Username")
|
||||||
|
add_output_port(DeckType.Types.STRING, "Message")
|
||||||
|
add_output_port(DeckType.Types.STRING, "Channel")
|
||||||
|
add_output_port(DeckType.Types.DICTIONARY, "Tags")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _event_received(event_name : StringName, event_data : Dictionary = {}):
|
||||||
|
|
||||||
|
if event_name != &"twitch_chat":
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
send(0, event_data.username)
|
||||||
|
send(1, event_data.message)
|
||||||
|
send(2, event_data.channel)
|
||||||
|
send(3, event_data)
|
||||||
|
|
57
classes/deck/nodes/twitch_send_chat.gd
Normal file
57
classes/deck/nodes/twitch_send_chat.gd
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
extends DeckNode
|
||||||
|
|
||||||
|
|
||||||
|
func _init():
|
||||||
|
name = "Twitch Send Chat"
|
||||||
|
node_type = "twitch_send_chat"
|
||||||
|
description = "Sends Twitch chat Messages"
|
||||||
|
category = "twitch"
|
||||||
|
|
||||||
|
add_input_port(
|
||||||
|
DeckType.Types.STRING,
|
||||||
|
"Message",
|
||||||
|
"field"
|
||||||
|
)
|
||||||
|
add_input_port(
|
||||||
|
DeckType.Types.STRING,
|
||||||
|
"Channel",
|
||||||
|
"field"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_input_port(
|
||||||
|
DeckType.Types.BOOL,
|
||||||
|
"Send",
|
||||||
|
"button"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _receive(to_input_port, data: Variant, extra_data: Array = []):
|
||||||
|
|
||||||
|
if to_input_port != 2:
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
var msg = get_value_for_input_port(0)
|
||||||
|
var channel = get_value_for_input_port(1)
|
||||||
|
|
||||||
|
if channel == null:
|
||||||
|
|
||||||
|
channel = ""
|
||||||
|
|
||||||
|
if msg == null:
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
Connections.twitch.send_chat(msg, channel)
|
||||||
|
|
||||||
|
|
||||||
|
func get_value_for_input_port(port: int) -> Variant:
|
||||||
|
if request_value(port) != null:
|
||||||
|
return request_value(port)
|
||||||
|
elif get_input_ports()[port].value_callback.get_object() && get_input_ports()[port].value_callback.call() != null:
|
||||||
|
return get_input_ports()[port].value_callback.call()
|
||||||
|
else:
|
||||||
|
return null
|
|
@ -32,6 +32,7 @@ enum FileMenuId {
|
||||||
|
|
||||||
enum ConnectionsMenuId {
|
enum ConnectionsMenuId {
|
||||||
OBS,
|
OBS,
|
||||||
|
TWITCH,
|
||||||
}
|
}
|
||||||
|
|
||||||
## Weak Reference to the Deck that is currently going to be saved.
|
## Weak Reference to the Deck that is currently going to be saved.
|
||||||
|
@ -40,9 +41,11 @@ var _deck_to_save: WeakRef
|
||||||
@onready var no_obsws: NoOBSWS = %NoOBSWS as NoOBSWS
|
@onready var no_obsws: NoOBSWS = %NoOBSWS as NoOBSWS
|
||||||
|
|
||||||
@onready var obs_setup_dialog: OBSWebsocketSetupDialog = $OBSWebsocketSetupDialog as OBSWebsocketSetupDialog
|
@onready var obs_setup_dialog: OBSWebsocketSetupDialog = $OBSWebsocketSetupDialog as OBSWebsocketSetupDialog
|
||||||
|
@onready var twitch_setup_dialog : TwitchSetupDialog = $Twitch_Setup_Dialog as TwitchSetupDialog
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
||||||
tab_container.add_button_pressed.connect(add_empty_deck)
|
tab_container.add_button_pressed.connect(add_empty_deck)
|
||||||
|
|
||||||
tab_container.tab_close_requested.connect(
|
tab_container.tab_close_requested.connect(
|
||||||
|
@ -53,6 +56,10 @@ func _ready() -> void:
|
||||||
|
|
||||||
file_dialog.canceled.connect(disconnect_file_dialog_signals)
|
file_dialog.canceled.connect(disconnect_file_dialog_signals)
|
||||||
Connections.obs_websocket = no_obsws
|
Connections.obs_websocket = no_obsws
|
||||||
|
Connections.twitch = %Twitch_Connection
|
||||||
|
|
||||||
|
Connections.twitch.chat_socket.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.NEW, new_deck_shortcut)
|
||||||
file_popup_menu.set_item_shortcut(FileMenuId.OPEN, open_deck_shortcut)
|
file_popup_menu.set_item_shortcut(FileMenuId.OPEN, open_deck_shortcut)
|
||||||
|
@ -172,6 +179,8 @@ func _on_connections_id_pressed(id: int) -> void:
|
||||||
match id:
|
match id:
|
||||||
ConnectionsMenuId.OBS:
|
ConnectionsMenuId.OBS:
|
||||||
obs_setup_dialog.popup_centered()
|
obs_setup_dialog.popup_centered()
|
||||||
|
ConnectionsMenuId.TWITCH:
|
||||||
|
twitch_setup_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
func _on_obs_websocket_setup_dialog_connect_button_pressed(state: OBSWebsocketSetupDialog.ConnectionState) -> void:
|
func _on_obs_websocket_setup_dialog_connect_button_pressed(state: OBSWebsocketSetupDialog.ConnectionState) -> void:
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
[gd_scene load_steps=16 format=3 uid="uid://duaah5x0jhkn6"]
|
[gd_scene load_steps=18 format=3 uid="uid://duaah5x0jhkn6"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://graph_node_renderer/deck_holder_renderer.gd" id="1_67g2g"]
|
[ext_resource type="Script" path="res://graph_node_renderer/deck_holder_renderer.gd" id="1_67g2g"]
|
||||||
[ext_resource type="PackedScene" uid="uid://b84f2ngtcm5b8" path="res://graph_node_renderer/tab_container_custom.tscn" id="1_s3ug2"]
|
[ext_resource type="PackedScene" uid="uid://b84f2ngtcm5b8" path="res://graph_node_renderer/tab_container_custom.tscn" id="1_s3ug2"]
|
||||||
[ext_resource type="Theme" uid="uid://dqqdqscid2iem" path="res://graph_node_renderer/default_theme.tres" id="1_tgul2"]
|
[ext_resource type="Theme" uid="uid://dqqdqscid2iem" path="res://graph_node_renderer/default_theme.tres" id="1_tgul2"]
|
||||||
[ext_resource type="Script" path="res://addons/no-obs-ws/NoOBSWS.gd" id="4_nu72u"]
|
[ext_resource type="Script" path="res://addons/no-obs-ws/NoOBSWS.gd" id="4_nu72u"]
|
||||||
|
[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://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"]
|
||||||
|
|
||||||
[sub_resource type="InputEventKey" id="InputEventKey_giamc"]
|
[sub_resource type="InputEventKey" id="InputEventKey_giamc"]
|
||||||
device = -1
|
device = -1
|
||||||
|
@ -114,9 +116,11 @@ unique_name_in_owner = true
|
||||||
|
|
||||||
[node name="Connections" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
[node name="Connections" type="PopupMenu" parent="MarginContainer/VSplitContainer/VBoxContainer/MenuBar"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
item_count = 1
|
item_count = 2
|
||||||
item_0/text = "OBS..."
|
item_0/text = "OBS..."
|
||||||
item_0/id = 0
|
item_0/id = 0
|
||||||
|
item_1/text = "Twitch.."
|
||||||
|
item_1/id = 1
|
||||||
|
|
||||||
[node name="TabContainerCustom" parent="MarginContainer/VSplitContainer/VBoxContainer" instance=ExtResource("1_s3ug2")]
|
[node name="TabContainerCustom" parent="MarginContainer/VSplitContainer/VBoxContainer" instance=ExtResource("1_s3ug2")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
@ -137,7 +141,15 @@ use_native_dialog = true
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("4_nu72u")
|
script = ExtResource("4_nu72u")
|
||||||
|
|
||||||
|
[node name="Twitch_Connection" type="Node" parent="Connections"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("5_3n36q")
|
||||||
|
|
||||||
[node name="OBSWebsocketSetupDialog" parent="." instance=ExtResource("5_uo2gj")]
|
[node name="OBSWebsocketSetupDialog" parent="." instance=ExtResource("5_uo2gj")]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Twitch_Setup_Dialog" parent="." instance=ExtResource("7_7rhap")]
|
||||||
|
visible = false
|
||||||
|
|
||||||
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/File" to="." method="_on_file_id_pressed"]
|
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/File" to="." method="_on_file_id_pressed"]
|
||||||
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Connections" to="." method="_on_connections_id_pressed"]
|
[connection signal="id_pressed" from="MarginContainer/VSplitContainer/VBoxContainer/MenuBar/Connections" to="." method="_on_connections_id_pressed"]
|
||||||
|
|
18
graph_node_renderer/twitch_setup_dialog.gd
Normal file
18
graph_node_renderer/twitch_setup_dialog.gd
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
extends ConfirmationDialog
|
||||||
|
class_name TwitchSetupDialog
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
|
||||||
|
%Authenticate.pressed.connect(authenticate_twitch)
|
||||||
|
confirmed.connect(connect_to_chat)
|
||||||
|
|
||||||
|
|
||||||
|
func authenticate_twitch():
|
||||||
|
|
||||||
|
Connections.twitch.authenticate_with_twitch(%Client_ID.text)
|
||||||
|
|
||||||
|
|
||||||
|
func connect_to_chat():
|
||||||
|
|
||||||
|
Connections.twitch.setup_chat_connection(%Default_Chat.text)
|
||||||
|
|
48
graph_node_renderer/twitch_setup_dialog.tscn
Normal file
48
graph_node_renderer/twitch_setup_dialog.tscn
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bq2lxmbnic4lc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://graph_node_renderer/twitch_setup_dialog.gd" id="1_xx7my"]
|
||||||
|
|
||||||
|
[node name="twitch_setup_dialog" type="ConfirmationDialog"]
|
||||||
|
title = "Twitch Connection Setup"
|
||||||
|
position = Vector2i(0, 36)
|
||||||
|
size = Vector2i(375, 158)
|
||||||
|
visible = true
|
||||||
|
min_size = Vector2i(375, 150)
|
||||||
|
script = ExtResource("1_xx7my")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
anchors_preset = 5
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = -179.5
|
||||||
|
offset_top = 8.0
|
||||||
|
offset_right = 179.5
|
||||||
|
offset_bottom = 109.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
size_flags_horizontal = 4
|
||||||
|
|
||||||
|
[node name="Authentication" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Client_ID" type="LineEdit" parent="VBoxContainer/Authentication"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
tooltip_text = "The Twitch Client ID used by this Connection for it's Authentication. Defaults to the built in Client ID that the program ships with."
|
||||||
|
placeholder_text = "Custom Client ID (Optional)"
|
||||||
|
|
||||||
|
[node name="Authenticate" type="Button" parent="VBoxContainer/Authentication"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_stretch_ratio = 0.25
|
||||||
|
text = "Authenticate"
|
||||||
|
|
||||||
|
[node name="CheckBox" type="CheckBox" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Extra Chat Info"
|
||||||
|
|
||||||
|
[node name="Default_Chat" type="LineEdit" parent="VBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
placeholder_text = "Default Chat Channel"
|
|
@ -25,7 +25,7 @@ NodeDB="*res://classes/deck/node_db.gd"
|
||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
enabled=PackedStringArray("res://addons/no-obs-ws/plugin.cfg")
|
enabled=PackedStringArray("res://addons/no-obs-ws/plugin.cfg", "res://addons/no_twitch/plugin.cfg")
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue