diff --git a/TwitchGod.tscn b/TwitchGod.tscn
new file mode 100644
index 0000000..11c63b4
--- /dev/null
+++ b/TwitchGod.tscn
@@ -0,0 +1,22 @@
+[gd_scene load_steps=7 format=3 uid="uid://bsved37ft3klq"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/twitch_god.gd" id="1_41vp4"]
+[ext_resource type="PackedScene" uid="uid://dsev0vyt8vkuf" path="res://addons/TwitchGod/http/HttpClient.tscn" id="2_04lmt"]
+[ext_resource type="PackedScene" uid="uid://dxaclxfi6m2gk" path="res://addons/TwitchGod/http/WebsocketClient.tscn" id="2_tgrgj"]
+[ext_resource type="PackedScene" uid="uid://bn2omqaosdoqu" path="res://addons/TwitchGod/http/TwitchSetting.tscn" id="2_v0pcb"]
+[ext_resource type="PackedScene" uid="uid://d7mhkh8sua4x" path="res://addons/TwitchGod/http/HttpServer.tscn" id="4_fhjvp"]
+[ext_resource type="Script" path="res://addons/TwitchGod/auth.gd" id="6_dmql6"]
+
+[node name="TwitchGod" type="Node"]
+script = ExtResource("1_41vp4")
+
+[node name="TwitchSetting" parent="." instance=ExtResource("2_v0pcb")]
+
+[node name="HttpServer" parent="." instance=ExtResource("4_fhjvp")]
+
+[node name="WebsocketClient" parent="." instance=ExtResource("2_tgrgj")]
+
+[node name="HttpClient" parent="." instance=ExtResource("2_04lmt")]
+
+[node name="Auth" type="Node" parent="."]
+script = ExtResource("6_dmql6")
diff --git a/auth.gd b/auth.gd
new file mode 100644
index 0000000..dd12266
--- /dev/null
+++ b/auth.gd
@@ -0,0 +1,19 @@
+extends Node
+
+const BROADCASTER_ID = ""
+const USER_ID = ""
+const CLIENT_SECRET = ""
+const CLIENT_ID = ""
+const PORT = 7171
+const SCOPE = "moderator%3Amanage%3Ashoutouts+channel%3Aread%3Apolls+channel%3Amanage%3Apolls+user%3Aread%3Achat+user%3Awrite%3Achat+channel%3Aread%3Aredemptions+channel%3Amanage%3Aredemptions+channel%3Aread%3Apolls"
+const SUBS = [
+ TwitchEvents.Event.CHANNEL_CHAT_MESSAGE,
+ TwitchEvents.Event.CHANNEL_POLL_BEGIN,
+ TwitchEvents.Event.CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD,
+ TwitchEvents.Event.CHANNEL_POLL_BEGIN,
+ TwitchEvents.Event.CHANNEL_POLL_PROGRESS,
+ TwitchEvents.Event.CHANNEL_POLL_END
+]
+const SCOPES = [
+ ""
+]
diff --git a/http/HttpClient.tscn b/http/HttpClient.tscn
new file mode 100644
index 0000000..f2971d3
--- /dev/null
+++ b/http/HttpClient.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://dsev0vyt8vkuf"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/http/http_client.gd" id="1_prokr"]
+
+[node name="HttpClient" type="Node"]
+script = ExtResource("1_prokr")
diff --git a/http/HttpServer.tscn b/http/HttpServer.tscn
new file mode 100644
index 0000000..d8886c0
--- /dev/null
+++ b/http/HttpServer.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://d7mhkh8sua4x"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/http/http_server.gd" id="1_im1bn"]
+
+[node name="HttpServer" type="Node"]
+script = ExtResource("1_im1bn")
diff --git a/http/TwitchSetting.tscn b/http/TwitchSetting.tscn
new file mode 100644
index 0000000..df6a55c
--- /dev/null
+++ b/http/TwitchSetting.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bn2omqaosdoqu"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/http/twitch_setting.gd" id="1_qpw3i"]
+
+[node name="TwitchSetting" type="Node"]
+script = ExtResource("1_qpw3i")
diff --git a/http/WebsocketClient.tscn b/http/WebsocketClient.tscn
new file mode 100644
index 0000000..4a5a244
--- /dev/null
+++ b/http/WebsocketClient.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://dxaclxfi6m2gk"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/http/websocket_client.gd" id="1_2ut2u"]
+
+[node name="WebsocketClient" type="Node"]
+script = ExtResource("1_2ut2u")
diff --git a/http/http_client.gd b/http/http_client.gd
new file mode 100644
index 0000000..91210e0
--- /dev/null
+++ b/http/http_client.gd
@@ -0,0 +1,118 @@
+extends Node
+class_name HttpClient
+
+signal request_completed(type: API_TYPE, data: Dictionary)
+
+const API_URL = "https://api.twitch.tv/helix"
+
+enum API_TYPE {
+ AUTH,
+ CREATE_EVENTSUB,
+ VERIFY,
+ SEND_CHAT_MESSAGE,
+ SHOUTOUT,
+ USERS,
+ EMOTE
+}
+
+var auth
+
+func send_message(message: String):
+ var parent = get_parent()
+
+ request(API_TYPE.SEND_CHAT_MESSAGE, {}, {
+ "broadcaster_id": parent.auth.BROADCASTER_ID,
+ "sender_id": parent.auth.USER_ID,
+ "message": message
+ })
+
+func request(type: API_TYPE, arguments: Dictionary = {}, data: Dictionary = {}):
+ var path = _get_path_from_type(type, arguments)
+ var headers = _get_headers_from_type(type)
+ var method = HTTPClient.METHOD_POST
+
+ if type == API_TYPE.VERIFY:
+ headers.push_back("Authorization: OAuth %s" % arguments.token)
+ auth = arguments.token
+ method = HTTPClient.METHOD_GET
+ if type == API_TYPE.USERS:
+ method = HTTPClient.METHOD_GET
+ if type == API_TYPE.EMOTE:
+ method = HTTPClient.METHOD_GET
+
+ var request = HTTPRequest.new()
+ add_child(request)
+ #prints(path, headers, method, JSON.stringify(data))
+ if method == HTTPClient.METHOD_GET:
+ request.request(path, headers, method)
+ else:
+ request.request(path, headers, method, JSON.stringify(data))
+ request.request_completed.connect(_on_request_completed.bind(type))
+
+ var response = await request.request_completed
+
+ if response[2].has("Content-Type: image/png"):
+ return response[3]
+
+ var string = ""
+ for character in response[3]:
+ string += char(character)
+
+ var json = JSON.parse_string(string)
+
+ return json
+
+
+func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, type: API_TYPE):
+ var string = ""
+ for character in body:
+ string += char(character)
+
+ if headers.has("Content-Type: application/json"):
+ var json = JSON.parse_string(string)
+
+ request_completed.emit(type, json)
+
+func _get_path_from_type(type: API_TYPE, arguments: Dictionary):
+ var comp_args = ""
+
+ for argument in arguments:
+ if comp_args == "":
+ comp_args += "?%s=%s" % [argument, arguments[argument]]
+ else:
+ comp_args += "&%s=%s" % [argument, arguments[argument]]
+
+ match type:
+ API_TYPE.AUTH:
+ return "https://id.twitch.tv/oauth2/token" + comp_args
+ API_TYPE.CREATE_EVENTSUB:
+ return "%s/eventsub/subscriptions" % [API_URL]
+ API_TYPE.VERIFY:
+ return "https://id.twitch.tv/oauth2/validate"
+ API_TYPE.SEND_CHAT_MESSAGE:
+ return "%s/chat/messages" % [API_URL] + comp_args
+ API_TYPE.USERS:
+ return "%s/users" % [API_URL] + comp_args
+ API_TYPE.SHOUTOUT:
+ return "%s/chat/shoutouts" % [API_URL] + comp_args
+ API_TYPE.EMOTE:
+ return "https://static-cdn.jtvnw.net/emoticons/v2/%s/static/dark/1.0" % [arguments.id]
+
+
+func _get_headers_from_type(type: API_TYPE):
+ match type:
+ API_TYPE.AUTH:
+ return []
+ API_TYPE.VERIFY:
+ return []
+ _:
+ return [_get_bearer(), _get_client_id(), "Content-Type: application/json"]
+
+
+func _get_bearer():
+ return "Authorization: Bearer %s" % [auth]
+
+
+func _get_client_id():
+ var parent = get_parent()
+ return "Client-Id: %s" % [parent.auth.CLIENT_ID]
diff --git a/http/http_server.gd b/http/http_server.gd
new file mode 100644
index 0000000..b593549
--- /dev/null
+++ b/http/http_server.gd
@@ -0,0 +1,78 @@
+extends Node
+
+var server: TCPServer
+
+signal started
+signal received(auth)
+
+var clients: Array[StreamPeerTCP] = []
+
+var listening = false
+
+func start():
+ var port = get_parent().auth.PORT
+ print(port)
+ server = TCPServer.new()
+ server.listen(port)
+
+
+
+func _process(delta):
+ if !server: return
+ if(!server.is_listening()): return;
+
+ if not listening:
+ print("SERVER STARTED")
+ started.emit()
+ listening = true
+
+ if(server.is_connection_available()):
+ _handle_connect();
+
+ for client in clients:
+ _process_request(client);
+
+
+func _handle_connect():
+ var client := server.take_connection()
+ clients.push_back(client)
+ print("CONNECT")
+
+
+func _process_request(client: StreamPeerTCP):
+ match client.get_status():
+ StreamPeerTCP.STATUS_CONNECTED:
+ client.poll()
+ if client.get_available_bytes() > 0:
+ var string = client.get_utf8_string(client.get_available_bytes())
+ var split = string.split("\n")
+ var first = split[0]
+ var regex = RegEx.new()
+ regex.compile("code=(.*)&")
+ var result = regex.search(first)
+ if result:
+ var auth = result.get_string(1)
+ _send_response(client, "200 OK", "
LoginSuccess!".to_utf8_buffer());
+ received.emit(auth)
+
+func auth(arguments: Dictionary):
+ var comp_args = ""
+
+ for argument in arguments:
+ if comp_args == "":
+ comp_args += "?%s=%s" % [argument, arguments[argument]]
+ else:
+ comp_args += "&%s=%s" % [argument, arguments[argument]]
+
+ OS.shell_open("https://id.twitch.tv/oauth2/authorize%s" % [comp_args])
+
+
+func _send_response(client, response_code : String, body : PackedByteArray) -> void:
+ client.put_data(("HTTP/1.1 %s\r\n" % response_code).to_utf8_buffer())
+ client.put_data("Server: Godot Engine\r\n".to_utf8_buffer())
+ client.put_data(("Content-Length: %d\r\n"% body.size()).to_utf8_buffer())
+ client.put_data("Connection: close\r\n".to_utf8_buffer())
+ client.put_data("Content-Type: text/html; charset=UTF-8\r\n".to_utf8_buffer())
+ client.put_data("\r\n".to_utf8_buffer())
+ client.put_data(body)
+ client.disconnect_from_host();
diff --git a/http/twitch_setting.gd b/http/twitch_setting.gd
new file mode 100644
index 0000000..b8ea33c
--- /dev/null
+++ b/http/twitch_setting.gd
@@ -0,0 +1,24 @@
+@tool
+extends Node
+
+
+
+
+
+func _init():
+ add_custom_project_setting("twitch_god/client_id", "Test", TYPE_STRING)
+ print("Test")
+
+func add_custom_project_setting(name: String, default_value, type: int, hint: int = PROPERTY_HINT_NONE, hint_string: String = ""):
+ #if ProjectSettings.has_setting(name): return
+
+ var setting_info: Dictionary = {
+ "name": name,
+ "type": type,
+ "hint": hint,
+ "hint_string": hint_string
+ }
+
+ ProjectSettings.set_setting(name, default_value)
+ ProjectSettings.add_property_info(setting_info)
+ ProjectSettings.set_initial_value(name, default_value)
diff --git a/http/websocket_client.gd b/http/websocket_client.gd
new file mode 100644
index 0000000..48a7306
--- /dev/null
+++ b/http/websocket_client.gd
@@ -0,0 +1,71 @@
+extends Node
+class_name WebsocketClient
+
+signal opened
+signal received(type: TwitchEvents.Event, data: Dictionary)
+
+const URL = "wss://eventsub.wss.twitch.tv/ws"
+
+var socket := WebSocketPeer.new()
+var connection_state := WebSocketPeer.STATE_CLOSED
+var id := ""
+
+
+# -- Built in Methods
+
+
+func _process(delta: float) -> void:
+ socket.poll()
+
+ var new_connection_state := socket.get_ready_state()
+ if new_connection_state != connection_state: _change_state(new_connection_state)
+
+ match connection_state:
+ WebSocketPeer.STATE_OPEN:
+ _read_data()
+
+
+# -- Public Methods
+
+
+## Open up the websocket to start listening
+func open():
+ socket.connect_to_url(URL)
+
+
+# -- Private Methods
+
+
+func _change_state(new_state: WebSocketPeer.State):
+ match new_state:
+ WebSocketPeer.STATE_OPEN:
+ opened.emit()
+
+ connection_state = new_state
+
+
+func _read_data():
+ while (socket.get_available_packet_count()):
+ var packet := socket.get_packet()
+ var data = _read_packet(packet)
+ var payload = data.payload
+
+ match data.metadata.message_type:
+ "session_welcome":
+ id = payload.session.id
+ "session_keepalive":
+ pass
+ "session_reconnect":
+ pass
+ _:
+ var event_type = TwitchEvents.get_event_type_from_name(payload.subscription.type)
+ received.emit(event_type, payload.event)
+
+
+func _read_packet(packet: PackedByteArray):
+ var string = ""
+ for chunk in packet:
+ string += char(chunk)
+ var data = JSON.parse_string(string)
+
+ return data
diff --git a/icons/bell.svg b/icons/bell.svg
new file mode 100644
index 0000000..95eb12f
--- /dev/null
+++ b/icons/bell.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/bell.svg.import b/icons/bell.svg.import
new file mode 100644
index 0000000..6682d08
--- /dev/null
+++ b/icons/bell.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbbslha8yjaga"
+path="res://.godot/imported/bell.svg-b8e0c96cd4e3b89ad8e1d4d55e418910.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/bell.svg"
+dest_files=["res://.godot/imported/bell.svg-b8e0c96cd4e3b89ad8e1d4d55e418910.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/bot-message-square.svg b/icons/bot-message-square.svg
new file mode 100644
index 0000000..67fe514
--- /dev/null
+++ b/icons/bot-message-square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/bot-message-square.svg.import b/icons/bot-message-square.svg.import
new file mode 100644
index 0000000..2a5900d
--- /dev/null
+++ b/icons/bot-message-square.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgxn1mb2i8ttj"
+path="res://.godot/imported/bot-message-square.svg-786cdae9f8058aaa187e28cad3dbee61.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/bot-message-square.svg"
+dest_files=["res://.godot/imported/bot-message-square.svg-786cdae9f8058aaa187e28cad3dbee61.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/cog.svg b/icons/cog.svg
new file mode 100644
index 0000000..7457690
--- /dev/null
+++ b/icons/cog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/cog.svg.import b/icons/cog.svg.import
new file mode 100644
index 0000000..1c1e0d2
--- /dev/null
+++ b/icons/cog.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dsr1447417xrq"
+path="res://.godot/imported/cog.svg-6bc9ddbf8b718bdb9823647c50ba7c4c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/cog.svg"
+dest_files=["res://.godot/imported/cog.svg-6bc9ddbf8b718bdb9823647c50ba7c4c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/key-round.svg b/icons/key-round.svg
new file mode 100644
index 0000000..f37bb5d
--- /dev/null
+++ b/icons/key-round.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/key-round.svg.import b/icons/key-round.svg.import
new file mode 100644
index 0000000..1963f8c
--- /dev/null
+++ b/icons/key-round.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1dp2foe4qm3v"
+path="res://.godot/imported/key-round.svg-09fd537499c75dbded8a700396a22502.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/key-round.svg"
+dest_files=["res://.godot/imported/key-round.svg-09fd537499c75dbded8a700396a22502.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/message-square.svg b/icons/message-square.svg
new file mode 100644
index 0000000..d4535a8
--- /dev/null
+++ b/icons/message-square.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/message-square.svg.import b/icons/message-square.svg.import
new file mode 100644
index 0000000..f60f2a7
--- /dev/null
+++ b/icons/message-square.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dyg54d6jepjgh"
+path="res://.godot/imported/message-square.svg-2af3a5bb9409f01fb3cbfc5b7a3dcbd7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/message-square.svg"
+dest_files=["res://.godot/imported/message-square.svg-2af3a5bb9409f01fb3cbfc5b7a3dcbd7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/pyramid.svg b/icons/pyramid.svg
new file mode 100644
index 0000000..c18a94d
--- /dev/null
+++ b/icons/pyramid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/pyramid.svg.import b/icons/pyramid.svg.import
new file mode 100644
index 0000000..93471c0
--- /dev/null
+++ b/icons/pyramid.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d1738kasi15th"
+path="res://.godot/imported/pyramid.svg-b581da31e5fb1851b7edda64e9bc57e8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/pyramid.svg"
+dest_files=["res://.godot/imported/pyramid.svg-b581da31e5fb1851b7edda64e9bc57e8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/regex.svg b/icons/regex.svg
new file mode 100644
index 0000000..cdf2a29
--- /dev/null
+++ b/icons/regex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/regex.svg.import b/icons/regex.svg.import
new file mode 100644
index 0000000..3f77c79
--- /dev/null
+++ b/icons/regex.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dinqgmvqeen8t"
+path="res://.godot/imported/regex.svg-35497d236d1bbc6045dd332d794afc45.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/regex.svg"
+dest_files=["res://.godot/imported/regex.svg-35497d236d1bbc6045dd332d794afc45.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/server.svg b/icons/server.svg
new file mode 100644
index 0000000..223bda7
--- /dev/null
+++ b/icons/server.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/server.svg.import b/icons/server.svg.import
new file mode 100644
index 0000000..b46bd3f
--- /dev/null
+++ b/icons/server.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cix7tp6kf6k44"
+path="res://.godot/imported/server.svg-b484a4103196fbad974efece4ea210c6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/server.svg"
+dest_files=["res://.godot/imported/server.svg-b484a4103196fbad974efece4ea210c6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/twitch.svg b/icons/twitch.svg
new file mode 100644
index 0000000..bc5fed3
--- /dev/null
+++ b/icons/twitch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/twitch.svg.import b/icons/twitch.svg.import
new file mode 100644
index 0000000..09d613f
--- /dev/null
+++ b/icons/twitch.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cupvtgpmau5tl"
+path="res://.godot/imported/twitch.svg-caa6e01720fb38b32b0fe8194c462f21.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/twitch.svg"
+dest_files=["res://.godot/imported/twitch.svg-caa6e01720fb38b32b0fe8194c462f21.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icons/unplug.svg b/icons/unplug.svg
new file mode 100644
index 0000000..cf3e661
--- /dev/null
+++ b/icons/unplug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icons/unplug.svg.import b/icons/unplug.svg.import
new file mode 100644
index 0000000..6e7ba51
--- /dev/null
+++ b/icons/unplug.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://donvetfqnw6o4"
+path="res://.godot/imported/unplug.svg-36958912a145d5a5ae67d5324e3af911.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/TwitchGod/icons/unplug.svg"
+dest_files=["res://.godot/imported/unplug.svg-36958912a145d5a5ae67d5324e3af911.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/nodes/TwitchChatCommandListener.tscn b/nodes/TwitchChatCommandListener.tscn
new file mode 100644
index 0000000..ddd76bb
--- /dev/null
+++ b/nodes/TwitchChatCommandListener.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=3 uid="uid://cunhrde7dsed6"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/nodes/twitch_chat_command_listener.gd" id="1_y088d"]
+[ext_resource type="PackedScene" uid="uid://b2585bqiywbwv" path="res://addons/TwitchGod/nodes/TwitchEventListener.tscn" id="2_p4sf8"]
+
+[node name="TwitchChatCommandListener" type="Node"]
+script = ExtResource("1_y088d")
+
+[node name="TwitchEventListener" parent="." instance=ExtResource("2_p4sf8")]
+event = 9
+
+[connection signal="received" from="TwitchEventListener" to="." method="_on_twitch_event_listener_received"]
diff --git a/nodes/TwitchChatMessageListener.tscn b/nodes/TwitchChatMessageListener.tscn
new file mode 100644
index 0000000..cc35751
--- /dev/null
+++ b/nodes/TwitchChatMessageListener.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=3 uid="uid://ceqp0eqllp4ai"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/nodes/twitch_chat_message_listener.gd" id="1_8blbc"]
+[ext_resource type="PackedScene" uid="uid://b2585bqiywbwv" path="res://addons/TwitchGod/nodes/TwitchEventListener.tscn" id="2_23mvm"]
+
+[node name="TwitchChatMessageListener" type="Node"]
+script = ExtResource("1_8blbc")
+
+[node name="TwitchEventListener" parent="." instance=ExtResource("2_23mvm")]
+event = 9
+
+[connection signal="received" from="TwitchEventListener" to="." method="_on_twitch_event_listener_received"]
diff --git a/nodes/TwitchChatRegexListener.tscn b/nodes/TwitchChatRegexListener.tscn
new file mode 100644
index 0000000..d9f98c8
--- /dev/null
+++ b/nodes/TwitchChatRegexListener.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=3 uid="uid://bln2snjbqkg42"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/nodes/twitch_chat_regex_listener.gd" id="1_snfls"]
+[ext_resource type="PackedScene" uid="uid://b2585bqiywbwv" path="res://addons/TwitchGod/nodes/TwitchEventListener.tscn" id="2_lqtl8"]
+
+[node name="TwitchChatRegexListener" type="Node"]
+script = ExtResource("1_snfls")
+
+[node name="TwitchEventListener" parent="." instance=ExtResource("2_lqtl8")]
+event = 9
+
+[connection signal="received" from="TwitchEventListener" to="." method="_on_twitch_event_listener_received"]
diff --git a/nodes/TwitchEventListener.tscn b/nodes/TwitchEventListener.tscn
new file mode 100644
index 0000000..8fd1442
--- /dev/null
+++ b/nodes/TwitchEventListener.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://b2585bqiywbwv"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/nodes/twitch_event_listener.gd" id="1_siryd"]
+
+[node name="TwitchEventListener" type="Node"]
+script = ExtResource("1_siryd")
diff --git a/nodes/TwitchPointRedemptionListener.tscn b/nodes/TwitchPointRedemptionListener.tscn
new file mode 100644
index 0000000..72f6d3f
--- /dev/null
+++ b/nodes/TwitchPointRedemptionListener.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=3 format=3 uid="uid://vghgdn7lns0x"]
+
+[ext_resource type="Script" path="res://addons/TwitchGod/nodes/twitch_point_redemption_listener.gd" id="1_l7obv"]
+[ext_resource type="PackedScene" uid="uid://b2585bqiywbwv" path="res://addons/TwitchGod/nodes/TwitchEventListener.tscn" id="2_86a3u"]
+
+[node name="TwitchPointRedemptionListener" type="Node"]
+script = ExtResource("1_l7obv")
+
+[node name="TwitchEventListener" parent="." instance=ExtResource("2_86a3u")]
+event = 36
+
+[connection signal="received" from="TwitchEventListener" to="." method="_on_twitch_event_listener_received"]
diff --git a/nodes/twitch_chat_command_listener.gd b/nodes/twitch_chat_command_listener.gd
new file mode 100644
index 0000000..c918541
--- /dev/null
+++ b/nodes/twitch_chat_command_listener.gd
@@ -0,0 +1,26 @@
+@icon("../icons/bot-message-square.svg")
+class_name TwitchChatCommandListener
+extends Node
+
+signal received(data: Dictionary)
+
+@export var names: Array[String] = []
+
+
+# -- Private Methods
+
+
+func _on_twitch_event_listener_received(data):
+ var message = data.message.text
+
+ for name in names:
+ if message.begins_with("!%s " % [name]) or message == ("!%s" % [name]):
+ var args = data.message.text.split(" ")
+
+ args.remove_at(0)
+
+ data.args = args
+ data.argcount = args.size()
+
+ received.emit(data)
+ return
diff --git a/nodes/twitch_chat_message_listener.gd b/nodes/twitch_chat_message_listener.gd
new file mode 100644
index 0000000..8c2107d
--- /dev/null
+++ b/nodes/twitch_chat_message_listener.gd
@@ -0,0 +1,17 @@
+@icon("../icons/message-square.svg")
+class_name TwitchChatMessageListener
+extends Node
+
+signal received(data: Dictionary)
+
+@export var messages: Array[String] = []
+
+
+# -- Private Methods
+
+
+func _on_twitch_event_listener_received(data):
+ var message = data.message.text
+
+ if messages.has(message):
+ received.emit(data)
diff --git a/nodes/twitch_chat_regex_listener.gd b/nodes/twitch_chat_regex_listener.gd
new file mode 100644
index 0000000..8617529
--- /dev/null
+++ b/nodes/twitch_chat_regex_listener.gd
@@ -0,0 +1,24 @@
+@icon("../icons/regex.svg")
+class_name TwitchChatRegexListener
+extends Node
+
+signal received(data: Dictionary)
+
+@export var regexes: Array[String] = []
+
+
+# -- Private Methods
+
+
+func _on_twitch_event_listener_received(data):
+ var message = data.message.text
+
+ for regex in regexes:
+ var pattern = RegEx.new()
+ pattern.compile(regex)
+
+ var result = pattern.search(message)
+
+ if result:
+ received.emit(data)
+ return
diff --git a/nodes/twitch_event_listener.gd b/nodes/twitch_event_listener.gd
new file mode 100644
index 0000000..ee313f3
--- /dev/null
+++ b/nodes/twitch_event_listener.gd
@@ -0,0 +1,24 @@
+@icon("../icons/bell.svg")
+class_name TwitchEventListener
+extends Node
+
+signal received(data: Dictionary)
+
+@export var event: TwitchEvents.Event
+
+
+# -- Built in Methods
+
+
+func _ready():
+ assert(event != null)
+
+ TwitchGod.websocket_client.received.connect(_on_received)
+
+
+# -- Private Methods
+
+
+func _on_received(type: TwitchEvents.Event, data: Dictionary):
+ if type == event:
+ received.emit(data);
diff --git a/nodes/twitch_point_redemption_listener.gd b/nodes/twitch_point_redemption_listener.gd
new file mode 100644
index 0000000..0fdddf4
--- /dev/null
+++ b/nodes/twitch_point_redemption_listener.gd
@@ -0,0 +1,18 @@
+class_name TwitchPointRedemptionListener
+extends Node
+
+signal received(data: Dictionary)
+
+@export var titles: Array[String] = []
+
+
+# -- Private Methods
+
+
+func _on_twitch_event_listener_received(data):
+ var reward_title = data.reward.title
+
+ print(reward_title)
+
+ if titles.has(reward_title):
+ received.emit(data)
diff --git a/twitch_events.gd b/twitch_events.gd
new file mode 100644
index 0000000..8c11296
--- /dev/null
+++ b/twitch_events.gd
@@ -0,0 +1,215 @@
+@tool
+class_name TwitchEvents
+extends Object
+
+enum Event {
+ AUTOMOD_MESSAGE_HOLD,
+ AUTOMOD_MESSAGE_UPDATE,
+ AUTOMOD_SETTINGS_UPDATE,
+ AUTOMOD_TERMS_UPDATE,
+ CHANNEL_UPDATE,
+ CHANNEL_FOLLOW,
+ CHANNEL_AD_BREAK_BEGIN,
+ CHANNEL_CHAT_CLEAR,
+ CHANNEL_CHAT_CLEAR_USER_MESSAGES,
+ CHANNEL_CHAT_MESSAGE,
+ CHANNEL_CHAT_MESSAGE_DELETE,
+ CHANNEL_CHAT_NOTIFICATION,
+ CHANNEL_CHAT_SETTINGS_UPDATE,
+ CHANNEL_CHAT_USER_MESSAGE_HOLD,
+ CHANNEL_CHAT_USER_MESSAGE_UPDATE,
+ CHANNEL_SUBSCRIBE,
+ CHANNEL_SUBSCRIPTION_END,
+ CHANNEL_SUBSCRIPTION_GIFT,
+ CHANNEL_SUBSCRIPTION_MESSAGE,
+ CHANNEL_CHEER,
+ CHANNEL_RAID,
+ CHANNEL_BAN,
+ CHANNEL_UNBAN,
+ CHANNEL_UNBAN_REQUEST_CREATE,
+ CHANNEL_UNBAN_REQUEST_RESOLVE,
+ CHANNEL_MODERATE,
+ CHANNEL_MODERATOR_ADD,
+ CHANNEL_MODERATOR_REMOVE,
+ CHANNEL_GUEST_STAR_SESSION_BEGIN,
+ CHANNEL_GUEST_STAR_SESSION_END,
+ CHANNEL_GUEST_STAR_GUEST_UPDATE,
+ CHANNEL_GUEST_STAR_SETTINGS_UPDATE,
+ CHANNEL_CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD,
+ CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_ADD,
+ CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_UPDATE,
+ CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REMOVE,
+ CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD,
+ CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_UPDATE,
+ CHANNEL_POLL_BEGIN,
+ CHANNEL_POLL_PROGRESS,
+ CHANNEL_POLL_END,
+ CHANNEL_PREDICTION_BEGIN,
+ CHANNEL_PREDICTION_PROGRESS,
+ CHANNEL_PREDICTION_LOCK,
+ CHANNEL_PREDICTION_END,
+ CHANNEL_SUSPICIOUS_USER_MESSAGE,
+ CHANNEL_SUSPICIOUS_USER_UPDATE,
+ CHANNEL_VIP_ADD,
+ CHANNEL_VIP_REMOVE,
+ CHANNEL_CHARITY_CAMPAIGN_DONATE,
+ CHANNEL_CHARITY_CAMPAIGN_START,
+ CHANNEL_CHARITY_CAMPAIGN_PROGRESS,
+ CHANNEL_CHARITY_CAMPAIGN_STOP,
+ CONDUIT_SHARD_DISABLED,
+ DROP_ENTITLEMENT_GRANT,
+ EXTENSION_BITS_TRANSACTION_CREATE,
+ CHANNEL_GOAL_BEGIN,
+ CHANNEL_GOAL_PROGRESS,
+ CHANNEL_GOAL_END,
+ CHANNEL_HYPE_TRAIN_BEGIN,
+ CHANNEL_HYPE_TRAIN_PROGRESS,
+ CHANNEL_HYPE_TRAIN_END,
+ CHANNEL_SHIELD_MODE_BEGIN,
+ CHANNEL_SHIELD_MODE_END,
+ CHANNEL_SHOUTOUT_CREATE,
+ CHANNEL_SHOUTOUT_RECEIVE,
+ STREAM_ONLINE,
+ STREAM_OFFLINE,
+ USER_AUTHORIZATION_GRANT,
+ USER_AUTHORIZATION_REVOKE,
+ USER_UPDATE,
+ USER_WHISPER_RECEIVED
+}
+
+static var _events = [
+ EventInfo.new(Event.AUTOMOD_MESSAGE_HOLD, "automod.message.hold", "1"),
+ EventInfo.new(Event.AUTOMOD_MESSAGE_UPDATE, "automod.message.update", "1"),
+ EventInfo.new(Event.AUTOMOD_SETTINGS_UPDATE, "automod.settings.update", "1"),
+ EventInfo.new(Event.AUTOMOD_TERMS_UPDATE, "automod.terms.update", "1"),
+ EventInfo.new(Event.CHANNEL_UPDATE, "channel.update", "2"),
+ EventInfo.new(Event.CHANNEL_FOLLOW, "channel.follow", "2"),
+ EventInfo.new(Event.CHANNEL_AD_BREAK_BEGIN, "channel.ad_break.begin", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_CLEAR, "channel.chat.clear", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_CLEAR_USER_MESSAGES, "channel.chat.clear_user_messages", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_MESSAGE, "channel.chat.message", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_MESSAGE_DELETE, "channel.chat.message_delete", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_NOTIFICATION, "channel.chat.notification", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_SETTINGS_UPDATE, "channel.chat_settings.update", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_USER_MESSAGE_HOLD, "channel.chat.user_message_hold", "1"),
+ EventInfo.new(Event.CHANNEL_CHAT_USER_MESSAGE_UPDATE, "channel.chat.user_message_update", "1"),
+ EventInfo.new(Event.CHANNEL_SUBSCRIBE, "channel.subscribe", "1"),
+ EventInfo.new(Event.CHANNEL_SUBSCRIPTION_END, "channel.subscription.end", "1"),
+ EventInfo.new(Event.CHANNEL_SUBSCRIPTION_GIFT, "channel.subscription.gift", "1"),
+ EventInfo.new(Event.CHANNEL_SUBSCRIPTION_MESSAGE, "channel.subscription.message", "1"),
+ EventInfo.new(Event.CHANNEL_CHEER, "channel.cheer", "1"),
+ EventInfo.new(Event.CHANNEL_RAID, "channel.raid", "1"),
+ EventInfo.new(Event.CHANNEL_BAN, "channel.ban", "1"),
+ EventInfo.new(Event.CHANNEL_UNBAN, "channel.unban", "1"),
+ EventInfo.new(Event.CHANNEL_UNBAN_REQUEST_CREATE, "channel.unban_request.create", "1"),
+ EventInfo.new(Event.CHANNEL_UNBAN_REQUEST_RESOLVE, "channel.unban_request.resolve", "1"),
+ EventInfo.new(Event.CHANNEL_MODERATE, "channel.moderate", "1"),
+ EventInfo.new(Event.CHANNEL_MODERATOR_ADD, "channel.moderator.add", "1"),
+ EventInfo.new(Event.CHANNEL_MODERATOR_REMOVE, "channel.moderator.remove", "1"),
+ EventInfo.new(Event.CHANNEL_GUEST_STAR_SESSION_BEGIN, "channel.guest_star_session.begin", "beta"),
+ EventInfo.new(Event.CHANNEL_GUEST_STAR_SESSION_END, "channel.guest_star_session.end", "beta"),
+ EventInfo.new(Event.CHANNEL_GUEST_STAR_GUEST_UPDATE, "channel.guest_star_guest.update", "beta"),
+ EventInfo.new(Event.CHANNEL_GUEST_STAR_SETTINGS_UPDATE, "channel.guest_star_settings.update", "beta"),
+ EventInfo.new(Event.CHANNEL_CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD, "channel.channel_points_automatic_reward_redemption.add", "1"),
+ EventInfo.new(Event.CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_ADD, "channel.channel_points_custom_reward.add", "1"),
+ EventInfo.new(Event.CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_UPDATE, "channel.channel_points_custom_reward.update", "1"),
+ EventInfo.new(Event.CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REMOVE, "channel.channel_points_custom_reward.remove", "1"),
+ EventInfo.new(Event.CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD, "channel.channel_points_custom_reward_redemption.add", "1"),
+ EventInfo.new(Event.CHANNEL_CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_UPDATE, "channel.channel_points_custom_reward_redemption.update", "1"),
+ EventInfo.new(Event.CHANNEL_POLL_BEGIN, "channel.poll.begin", "1"),
+ EventInfo.new(Event.CHANNEL_POLL_PROGRESS, "channel.poll.progress", "1"),
+ EventInfo.new(Event.CHANNEL_POLL_END, "channel.poll.end", "1"),
+ EventInfo.new(Event.CHANNEL_PREDICTION_BEGIN, "channel.prediction.begin", "1"),
+ EventInfo.new(Event.CHANNEL_PREDICTION_PROGRESS, "channel.prediction.progress", "1"),
+ EventInfo.new(Event.CHANNEL_PREDICTION_LOCK, "channel.prediction.lock", "1"),
+ EventInfo.new(Event.CHANNEL_PREDICTION_END, "channel.prediction.end", "1"),
+ EventInfo.new(Event.CHANNEL_SUSPICIOUS_USER_MESSAGE, "channel.suspicious_user.message", "1"),
+ EventInfo.new(Event.CHANNEL_SUSPICIOUS_USER_UPDATE, "channel.suspicious_user.update", "1"),
+ EventInfo.new(Event.CHANNEL_VIP_ADD, "channel.vip.add", "1"),
+ EventInfo.new(Event.CHANNEL_VIP_REMOVE, "channel.vip.remove", "1"),
+ EventInfo.new(Event.CHANNEL_CHARITY_CAMPAIGN_DONATE, "channel.charity_campaign.donate", "1"),
+ EventInfo.new(Event.CHANNEL_CHARITY_CAMPAIGN_START, "channel.charity_campaign.start", "1"),
+ EventInfo.new(Event.CHANNEL_CHARITY_CAMPAIGN_PROGRESS, "channel.charity_campaign.progress", "1"),
+ EventInfo.new(Event.CHANNEL_CHARITY_CAMPAIGN_STOP, "channel.charity_campaign.stop", "1"),
+ EventInfo.new(Event.CONDUIT_SHARD_DISABLED, "conduit.shard.disabled", "1"),
+ EventInfo.new(Event.DROP_ENTITLEMENT_GRANT, "drop.entitlement.grant", "1"),
+ EventInfo.new(Event.EXTENSION_BITS_TRANSACTION_CREATE, "extension.bits_transaction.create", "1"),
+ EventInfo.new(Event.CHANNEL_GOAL_BEGIN, "channel.goal.begin", "1"),
+ EventInfo.new(Event.CHANNEL_GOAL_PROGRESS, "channel.goal.progress", "1"),
+ EventInfo.new(Event.CHANNEL_GOAL_END, "channel.goal.end", "1"),
+ EventInfo.new(Event.CHANNEL_HYPE_TRAIN_BEGIN, "channel.hype_train.begin", "1"),
+ EventInfo.new(Event.CHANNEL_HYPE_TRAIN_PROGRESS, "channel.hype_train.progress", "1"),
+ EventInfo.new(Event.CHANNEL_HYPE_TRAIN_END, "channel.hype_train.end", "1"),
+ EventInfo.new(Event.CHANNEL_SHIELD_MODE_BEGIN, "channel.shield_mode.begin", "1"),
+ EventInfo.new(Event.CHANNEL_SHIELD_MODE_END, "channel.shield_mode.end", "1"),
+ EventInfo.new(Event.CHANNEL_SHOUTOUT_CREATE, "channel.shoutout.create", "1"),
+ EventInfo.new(Event.CHANNEL_SHOUTOUT_RECEIVE, "channel.shoutout.receive", "1"),
+ EventInfo.new(Event.STREAM_ONLINE, "stream.online", "1"),
+ EventInfo.new(Event.STREAM_OFFLINE, "stream.offline", "1"),
+ EventInfo.new(Event.USER_AUTHORIZATION_GRANT, "user.authorization.grant", "1"),
+ EventInfo.new(Event.USER_AUTHORIZATION_REVOKE, "user.authorization.revoke", "1"),
+ EventInfo.new(Event.USER_UPDATE, "user.update", "1"),
+ EventInfo.new(Event.USER_WHISPER_RECEIVED, "user.whisper.message", "1")
+]
+
+
+# -- Public Methods
+
+
+static func get_event_from_type(type: Event):
+ var result = _events.filter(func(event): return event.type == type)
+
+ if result:
+ return result[0]
+ else:
+ return null
+
+
+static func get_event_from_name(name: String):
+ var result = _events.filter(func(event): return event.name == name)
+
+ if result:
+ return result[0]
+ else:
+ return null
+
+
+static func get_event_type_from_name(name: String):
+ var result = _events.filter(func(event): return event.name == name)
+
+ if result:
+ return result[0].type
+ else:
+ return null
+
+
+# -- Classes
+
+
+class EventInfo:
+ var type: Event
+ var name: String
+ var version: String
+ var conditions: Array[String]
+
+
+ func _init(type: Event, name: String, version: String, conditions: Array[String] = []):
+ self.type = type
+ self.name = name
+ self.version = version
+ self.conditions = conditions
+
+
+ func generate_sub(arguments: Dictionary):
+ return {
+ "type": self.name,
+ "version": self.version,
+ "condition": { # Change to generalize for all types
+ "broadcaster_user_id": arguments.broadcaster,
+ "user_id": arguments.user
+ },
+ "transport": {
+ "method": "websocket",
+ "session_id": arguments.websocket
+ }
+ }
diff --git a/twitch_god.gd b/twitch_god.gd
new file mode 100644
index 0000000..3f8e3bd
--- /dev/null
+++ b/twitch_god.gd
@@ -0,0 +1,82 @@
+extends Node
+
+@onready var http_client = $"HttpClient"
+@onready var websocket_client = $"WebsocketClient"
+@onready var http_server = $"HttpServer"
+@onready var auth = $"Auth"
+
+const API_URL = "https://api.twitch.tv/helix"
+var access_token = null
+
+
+# -- Built-in Methods
+
+
+func _ready():
+ print("READY")
+ http_server.started.connect(_on_server_started)
+ http_server.received.connect(_on_server_received)
+ websocket_client.opened.connect(_on_websocket_opened)
+ http_client.request_completed.connect(_on_request_completed)
+
+ http_server.start()
+
+
+# -- Private Methods
+
+
+func _on_server_started():
+ websocket_client.open()
+
+
+func _on_websocket_opened():
+ http_server.auth(
+ {
+ "response_type": "code",
+ "client_id": auth.CLIENT_ID,
+ "redirect_uri": "http://localhost:%d" % [auth.PORT],
+ "scope": auth.SCOPE
+ }
+ )
+
+
+func _on_server_received(auth2):
+ http_client.request(
+ HttpClient.API_TYPE.AUTH,
+ {
+ "grant_type": "authorization_code",
+ "client_id": auth.CLIENT_ID,
+ "redirect_uri": "http://localhost:%d" % [auth.PORT],
+ "code": auth2,
+ "client_secret": auth.CLIENT_SECRET
+ }
+ )
+
+
+func _on_request_completed(type: HttpClient.API_TYPE, data: Dictionary):
+ match type:
+ HttpClient.API_TYPE.AUTH:
+ http_client.request(
+ HttpClient.API_TYPE.VERIFY,
+ {
+ "token": data.access_token
+ }
+ )
+ HttpClient.API_TYPE.VERIFY:
+ if not websocket_client.id:
+ push_error("Tried to use nonexistent websocket id. Please report to Ategon.")
+ return
+
+ for sub in auth.SUBS:
+ var event = TwitchEvents.get_event_from_type(sub)
+ var sub_data = event.generate_sub({
+ "broadcaster": auth.BROADCASTER_ID,
+ "user": auth.USER_ID,
+ "websocket": websocket_client.id
+ })
+
+ http_client.request(
+ HttpClient.API_TYPE.CREATE_EVENTSUB,
+ {},
+ sub_data
+ )
diff --git a/twitch_scopes.gd b/twitch_scopes.gd
new file mode 100644
index 0000000..557be4d
--- /dev/null
+++ b/twitch_scopes.gd
@@ -0,0 +1,12 @@
+@tool
+class_name TwitchScopes
+extends Object
+
+enum Scope {
+ ANALYTICS_READ_EXTENSIONS,
+ ANALYTICS_READ_GAMES,
+ BITS_READ,
+ CHANNEL_MANAGE_ADS,
+ CHANNEL_READ_ADS,
+ CHANNEL_MANAGE_BROADCAST
+}