Initial commit

This commit is contained in:
Ategon 2024-10-21 22:30:26 -04:00
commit 45cb5aa590
56 changed files with 1649 additions and 0 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/**/auth.gd

9
Main.tscn Normal file
View file

@ -0,0 +1,9 @@
[gd_scene load_steps=3 format=3 uid="uid://fh070t2wv8xi"]
[ext_resource type="Script" path="res://main.gd" id="1_2diot"]
[ext_resource type="PackedScene" uid="uid://dsp5r4i4h08v2" path="res://src/Emotes.tscn" id="2_s32fb"]
[node name="Main" type="Node2D"]
script = ExtResource("1_2diot")
[node name="Emotes" parent="." instance=ExtResource("2_s32fb")]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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", "<html><head><title>Login</title><script>window.close()</script></head><body>Success!</body></html>".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();

View file

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

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>

After

Width:  |  Height:  |  Size: 302 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot-message-square"><path d="M12 6V2H8"/><path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z"/><path d="M2 12h2"/><path d="M9 11v2"/><path d="M15 11v2"/><path d="M20 12h2"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cog"><path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/></svg>

After

Width:  |  Height:  |  Size: 607 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-key-round"><path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>

After

Width:  |  Height:  |  Size: 290 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pyramid"><path d="M2.5 16.88a1 1 0 0 1-.32-1.43l9-13.02a1 1 0 0 1 1.64 0l9 13.01a1 1 0 0 1-.32 1.44l-8.51 4.86a2 2 0 0 1-1.98 0Z"/><path d="M12 2v20"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-regex"><path d="M17 3v10"/><path d="m12.67 5.5 8.66 5"/><path d="m12.67 10.5 8.66-5"/><path d="M9 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2z"/></svg>

After

Width:  |  Height:  |  Size: 370 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-server"><rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/></svg>

After

Width:  |  Height:  |  Size: 405 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-twitch"><path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"/></svg>

After

Width:  |  Height:  |  Size: 269 B

View file

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

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unplug"><path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/></svg>

After

Width:  |  Height:  |  Size: 473 B

View file

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

View file

@ -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"]

View file

@ -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"]

View file

@ -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"]

View file

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

View file

@ -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"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

1
icon.svg Normal file
View file

@ -0,0 +1 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

After

Width:  |  Height:  |  Size: 949 B

37
icon.svg.import Normal file
View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyu3h6ohevyaf"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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

5
main.gd Normal file
View file

@ -0,0 +1,5 @@
extends Node2D
func _ready() -> void:
get_viewport().transparent_bg = true

43
project.godot Normal file
View file

@ -0,0 +1,43 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="EmoteWall"
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.3")
config/icon="res://icon.svg"
[autoload]
TwitchGod="*res://addons/TwitchGod/TwitchGod.tscn"
[display]
window/size/transparent=true
window/per_pixel_transparency/allowed=true
[input]
show_whitelist_popup={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
]
}
hide_whitelist_popup={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
]
}
enter_text={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}

18
src/AddedChannel.tscn Normal file
View file

@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://dwsnd5vbnrup"]
[ext_resource type="Script" path="res://src/added_channel.gd" id="1_gtue1"]
[node name="AddedChannel" type="HBoxContainer"]
script = ExtResource("1_gtue1")
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(220, 0)
layout_mode = 2
text = "Test"
horizontal_alignment = 1
[node name="Button" type="Button" parent="."]
layout_mode = 2
text = "X"
[connection signal="pressed" from="Button" to="." method="_on_button_pressed"]

9
src/Emote.tscn Normal file
View file

@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://b40miugn8pf1c"]
[ext_resource type="Script" path="res://src/emote.gd" id="1_6boyd"]
[node name="Emote" type="TextureRect"]
offset_right = 40.0
offset_bottom = 40.0
pivot_offset = Vector2(20, 20)
script = ExtResource("1_6boyd")

122
src/Emotes.tscn Normal file
View file

@ -0,0 +1,122 @@
[gd_scene load_steps=5 format=3 uid="uid://dsp5r4i4h08v2"]
[ext_resource type="Script" path="res://src/emotes.gd" id="1_fhqqu"]
[ext_resource type="PackedScene" uid="uid://b2585bqiywbwv" path="res://addons/TwitchGod/nodes/TwitchEventListener.tscn" id="2_iwst4"]
[ext_resource type="PackedScene" uid="uid://b40miugn8pf1c" path="res://src/Emote.tscn" id="3_52qj6"]
[ext_resource type="Script" path="res://src/add_to_whitelist.gd" id="4_quosf"]
[node name="Emotes" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 1152.0
offset_bottom = 648.0
script = ExtResource("1_fhqqu")
[node name="TwitchEventListener" parent="." instance=ExtResource("2_iwst4")]
event = 9
[node name="Emote" parent="." instance=ExtResource("3_52qj6")]
layout_mode = 0
[node name="AddToWhitelist" type="Control" 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 = -264.0
offset_top = -119.0
offset_right = 264.0
offset_bottom = 119.0
grow_horizontal = 2
grow_vertical = 2
pivot_offset = Vector2(268, 120)
script = ExtResource("4_quosf")
[node name="InsertIdLineEdit" type="LineEdit" parent="AddToWhitelist"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -158.0
offset_top = -35.0
offset_right = 158.0
offset_bottom = -4.0
grow_horizontal = 2
grow_vertical = 2
placeholder_text = "Insert Channel Name"
[node name="SubmitButton" type="Button" parent="AddToWhitelist"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -140.0
offset_top = 9.0
offset_right = -58.0
offset_bottom = 26.0
grow_horizontal = 2
grow_vertical = 2
text = "Submit"
[node name="CloseButton" type="Button" parent="AddToWhitelist"]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -26.0
offset_bottom = 31.0
grow_horizontal = 0
text = "X"
[node name="AddLabel" type="Label" parent="AddToWhitelist"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -134.0
offset_top = -79.0
offset_right = 134.0
offset_bottom = -36.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 24
text = "Add to Whitelist"
horizontal_alignment = 1
[node name="AddedChannels" type="HBoxContainer" parent="AddToWhitelist"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -361.0
offset_top = 71.0
offset_right = 307.0
offset_bottom = 291.0
grow_horizontal = 2
grow_vertical = 2
[node name="AddedChannels1" type="VBoxContainer" parent="AddToWhitelist/AddedChannels"]
layout_mode = 2
[node name="AddedChannels2" type="VBoxContainer" parent="AddToWhitelist/AddedChannels"]
layout_mode = 2
[node name="AddedChannels3" type="VBoxContainer" parent="AddToWhitelist/AddedChannels"]
layout_mode = 2
[connection signal="received" from="TwitchEventListener" to="." method="_on_twitch_event_listener_received"]
[connection signal="new_whitelist_id" from="AddToWhitelist" to="." method="_on_add_to_whitelist_new_whitelist_id"]
[connection signal="focus_entered" from="AddToWhitelist/InsertIdLineEdit" to="AddToWhitelist" method="_on_insert_id_line_edit_focus_entered"]
[connection signal="focus_exited" from="AddToWhitelist/InsertIdLineEdit" to="AddToWhitelist" method="_on_insert_id_line_edit_focus_exited"]
[connection signal="pressed" from="AddToWhitelist/SubmitButton" to="AddToWhitelist" method="_on_submit_button_pressed"]
[connection signal="pressed" from="AddToWhitelist/CloseButton" to="AddToWhitelist" method="_on_close_button_pressed"]

83
src/add_to_whitelist.gd Normal file
View file

@ -0,0 +1,83 @@
extends Control
signal new_whitelist_id(id: String, name: String)
@onready var insert_id_line_edit: LineEdit = $InsertIdLineEdit
var size_tween
var line_focused = false
func _ready() -> void:
scale = Vector2.ZERO
func _process(delta: float) -> void:
if Input.is_action_just_pressed("show_whitelist_popup"):
show_popup()
elif Input.is_action_just_pressed("hide_whitelist_popup") and not line_focused:
hide_popup()
elif Input.is_action_just_pressed("enter_text") and line_focused:
_on_submit_button_pressed()
func show_popup():
if size_tween:
size_tween.kill()
visible = true
size_tween = create_tween()
size_tween.set_ease(Tween.EASE_OUT)
size_tween.set_trans(Tween.TRANS_BACK)
size_tween.tween_property(self, "scale", Vector2.ONE, 0.5)
func hide_popup():
if size_tween:
size_tween.kill()
size_tween = create_tween()
size_tween.set_ease(Tween.EASE_OUT)
size_tween.set_trans(Tween.TRANS_QUAD)
size_tween.tween_property(self, "scale", Vector2.ZERO, 0.25)
size_tween.tween_callback(func():
visible = false
insert_id_line_edit.clear()
)
func _on_close_button_pressed() -> void:
hide_popup()
func _on_lookup_button_pressed() -> void:
OS.shell_open("https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/")
func _on_submit_button_pressed() -> void:
var value = insert_id_line_edit.text
insert_id_line_edit.clear()
if value.is_valid_int():
var data = await TwitchGod.http_client.request(TwitchGod.http_client.API_TYPE.USERS, {
"id": value
})
if not data.data.size() == 0:
new_whitelist_id.emit(data.data[0].id, data.data[0].display_name)
else:
var data = await TwitchGod.http_client.request(TwitchGod.http_client.API_TYPE.USERS, {
"login": value
})
if not data.data.size() == 0:
new_whitelist_id.emit(data.data[0].id, data.data[0].display_name)
func _on_insert_id_line_edit_focus_entered() -> void:
line_focused = true
func _on_insert_id_line_edit_focus_exited() -> void:
line_focused = false

9
src/added_channel.gd Normal file
View file

@ -0,0 +1,9 @@
extends HBoxContainer
signal remove_channel
@onready var label: Label = $Label
func _on_button_pressed() -> void:
remove_channel.emit()

16
src/emote.gd Normal file
View file

@ -0,0 +1,16 @@
extends TextureRect
func _ready():
position.y += 100
var tween = create_tween()
tween.set_ease(Tween.EASE_OUT)
tween.set_trans(Tween.TRANS_BACK)
tween.tween_property(self, "position:y", -100, 1.5).as_relative().from_current()
var tween2 = create_tween()
tween.set_ease(Tween.EASE_IN)
tween2.set_trans(Tween.TRANS_BACK)
tween2.set_parallel()
tween2.tween_property(self, "modulate", Color.TRANSPARENT, 0.5).set_delay(1.25)
tween2.tween_property(self, "scale", Vector2(0, 0), 0.5).set_delay(1.25)

103
src/emotes.gd Normal file
View file

@ -0,0 +1,103 @@
extends Control
@onready var texture_rect = $Emote
@onready var added_channels_1: VBoxContainer = $AddToWhitelist/AddedChannels/AddedChannels1
@onready var added_channels_2: VBoxContainer = $AddToWhitelist/AddedChannels/AddedChannels2
@onready var added_channels_3: VBoxContainer = $AddToWhitelist/AddedChannels/AddedChannels3
const ADDED_CHANNEL = preload("res://src/AddedChannel.tscn")
const EMOTE = preload("res://src/Emote.tscn")
var whitelist = []
var emote_cache = {}
func _ready() -> void:
_load_from_whitelist()
func _save_to_whitelist():
var file = FileAccess.open("user://whitelist.txt", FileAccess.WRITE)
for channel in whitelist:
file.store_line("%s#%s" % [channel.id, channel.name])
func _update_labels():
for child in added_channels_1.get_children():
child.queue_free()
for child in added_channels_2.get_children():
child.queue_free()
for child in added_channels_3.get_children():
child.queue_free()
for i in range(0, whitelist.size()):
if i < 21:
var new_label = ADDED_CHANNEL.instantiate()
new_label.remove_channel.connect(_remove_from_whitelist.bind(whitelist[i].id))
if i < 7:
added_channels_1.add_child(new_label)
elif i < 14:
added_channels_2.add_child(new_label)
elif i < 21:
added_channels_3.add_child(new_label)
new_label.label.text = whitelist[i].name
func _load_from_whitelist():
if FileAccess.file_exists("user://whitelist.txt"):
var file = FileAccess.open("user://whitelist.txt", FileAccess.READ)
var content = file.get_as_text()
var filecontent = content.split("\n")
for i in range(0, filecontent.size()):
var idsplit = filecontent[i].split("#")
if idsplit.size() > 1:
whitelist.push_back({
"id": idsplit[0],
"name": idsplit[1]
})
else:
whitelist = []
_update_labels()
func _on_twitch_event_listener_received(data):
for fragment in data.message.fragments:
if fragment.type != "emote":
continue
if not whitelist.filter(func (x): return x.id == fragment.emote.owner_id).size():
continue
if not emote_cache.has(fragment.emote.id):
var image_buffer = await TwitchGod.http_client.request(HttpClient.API_TYPE.EMOTE, { "id": fragment.emote.id })
var image = Image.new()
image.load_png_from_buffer(image_buffer)
var texture = ImageTexture.create_from_image(image)
emote_cache[fragment.emote.id] = texture
var new_emote = EMOTE.instantiate()
new_emote.position.x = randi_range(50, 900)
new_emote.position.y = 580
new_emote.texture = emote_cache[fragment.emote.id]
add_child(new_emote)
func _remove_from_whitelist(id: String):
whitelist = whitelist.filter(func (x): return x.id != id)
_update_labels()
_save_to_whitelist()
func _on_add_to_whitelist_new_whitelist_id(id: String, name: String) -> void:
if whitelist.filter(func (x): return x.id == id).size():
return
whitelist.push_back({
"id": id,
"name": name
})
_update_labels()
_save_to_whitelist()