diff --git a/addons/very-simple-twitch/auth_server.gd b/addons/very-simple-twitch/auth_server.gd new file mode 100644 index 0000000..e2262eb --- /dev/null +++ b/addons/very-simple-twitch/auth_server.gd @@ -0,0 +1,101 @@ +class_name VSTAuthServer + +extends Node + +signal OnTokenReceived(token: String) + +var SERVER_IDENTITY = 'AUTH_SERVER' +var TOKEN_KEY = 'token' +var AUTHENTICATION_REDIRECT_FILE_PATH = 'res://addons/very-simple-twitch/public/index.html' + +var _clients: Array[StreamPeerTCP] = [] +var _server: TCPServer + + +func start_server(port: int): + _server = TCPServer.new() + _server.listen(port) + print("Server started") + + +func stop_server(): + if _server: + _server.stop() + _server = null + + +func _process(_delta: float) -> void: + if !_server: + return + + var newConnection = _server.take_connection() + if newConnection: + _clients.push_back(newConnection) + + for client in _clients: + if client.get_status() != StreamPeerTCP.STATUS_CONNECTED: + continue + + var bytes = client.get_available_bytes() + # after finisher reading bytes clear connection + _clients = _clients.filter(func (item): return item != client) + var requestAsString = client.get_string(bytes) + if requestAsString.length() < 1: + continue + + var requestInformation = requestAsString.split('\n')[0].split(' ') + var method = requestInformation[0] + var url = requestInformation[1] + + match method: + 'GET': + # send html login page + handleGet(client) + 'POST': + # handle token extraction + handlePost(client, url) + + +func handlePost(client: StreamPeer, url: String): + var urlSplitted:PackedStringArray = url.split('?') + if (len(urlSplitted) == 1): + return + var query = urlSplitted[1] + var token = getTokenFromQuery(query) + if !token: + return + OnTokenReceived.emit(token) + send200(client) + stop_server() + queue_free() + + +func handleGet(client: StreamPeer): + var pageAsString = loadLoginPage() + send200(client, pageAsString) + + +func getTokenFromQuery(query: String): + var queryKeyValues = query.split('&') + for keyValue in queryKeyValues: + var splittedKeyValue = keyValue.split('=') + var key = splittedKeyValue[0] + if key == TOKEN_KEY: + return splittedKeyValue[1] + + +func send200(client: StreamPeer, data: String = "", content_type: String = "text/html"): + var dataAsBuffer = data.to_ascii_buffer() + client.put_data(("HTTP/1.1 %d %s\r\n" % [200, 'OK']).to_ascii_buffer()) + client.put_data(("Server: %s\r\n" % SERVER_IDENTITY).to_ascii_buffer()) + client.put_data(("Content-Length: %d\r\n" % dataAsBuffer.size()).to_ascii_buffer()) + client.put_data("Connection: close\r\n".to_ascii_buffer()) + client.put_data(("Content-Type: %s\r\n\r\n" % content_type).to_ascii_buffer()) + client.put_data(dataAsBuffer) + + +func loadLoginPage() -> String: + var file = FileAccess.open(AUTHENTICATION_REDIRECT_FILE_PATH, FileAccess.READ) + var loadedFile: String = file.get_as_text() + file.close() + return loadedFile diff --git a/addons/very-simple-twitch/auth_server.gd.uid b/addons/very-simple-twitch/auth_server.gd.uid new file mode 100644 index 0000000..67e492b --- /dev/null +++ b/addons/very-simple-twitch/auth_server.gd.uid @@ -0,0 +1 @@ +uid://cfwdtrrd8w61s diff --git a/addons/very-simple-twitch/chat/vst_chat_dock.gd b/addons/very-simple-twitch/chat/vst_chat_dock.gd new file mode 100644 index 0000000..d0af23d --- /dev/null +++ b/addons/very-simple-twitch/chat/vst_chat_dock.gd @@ -0,0 +1,145 @@ +@tool +extends Control + +const MAX_MESSAGES:int = 50 +var line: PackedScene = load("res://addons/very-simple-twitch/chat/vst_chat_dock_line.tscn") + + +var twitch_chat: VSTChat: + get: + if twitch_chat == null: + twitch_chat = VSTChat.new() + add_child(twitch_chat) + return twitch_chat + +@onready var support_button: Button = %SupportButton +@onready var channel_line_edit: LineEdit = %ChannelLineEdit + +@onready var connect_button: Button = %ConnectButton + +@onready var chat_layout: Control = %ChatLayout + +@onready var chat_scroll:ScrollContainer = %ChatScroll + +@onready var clear_button:Button = %ClearButton + +@onready var disconnect_button:Button = %DisconnectButton + +func _ready(): + support_button.icon = get_theme_icon("Heart", "EditorIcons") + support_button.tooltip_text = "Support me on Ko-fi" + +func _on_button_pressed(): + twitch_chat.Connected.connect(on_chat_connected) + twitch_chat.OnMessage.connect(create_chatter_msg) + + twitch_chat.login_anon(channel_line_edit.text) + connect_button.disabled = true + channel_line_edit.editable = false + +func _on_clear_button_pressed(): + clear_all_messages() + +func _on_line_edit_text_changed(new_text): + connect_button.disabled = len(new_text) == 0 + +func _on_disconnect_button_pressed(): + twitch_chat.disconnect_api() + clear_all_messages() + show_connect_layout() + if twitch_chat.Connected.is_connected(on_chat_connected): + twitch_chat.Connected.disconnect(on_chat_connected) + if twitch_chat.OnMessage.is_connected(create_chatter_msg): + twitch_chat.OnMessage.disconnect(create_chatter_msg) + + +func _on_support_button_pressed() -> void: + OS.shell_open("https://ko-fi.com/rothiotome?ref=VST") + + +func on_chat_connected(): + create_system_msg("Connected to chat") + show_chat_layout() + +func create_system_msg(message: String): + var msg = line.instantiate() + msg.set_chatter_string("[i]"+message+"[/i]") + chat_layout.add_child(msg) + check_scroll() + +func create_chatter_msg(chatter: VSTChatter): + var msg = line.instantiate() + + var badges: String = await get_badges(chatter) + chatter.message = escape_bbcode(chatter.message) + await add_emotes(chatter) + + msg.set_chatter_msg(badges, chatter) + chat_layout.add_child(msg) + + check_scroll() + +func check_scroll(): + var bottom: bool = is_scroll_bottom() + check_number_messages() + await get_tree().process_frame + if bottom: chat_scroll.scroll_vertical = chat_scroll.get_v_scroll_bar().max_value + +func check_number_messages(): + if chat_layout.get_child_count() > MAX_MESSAGES: + chat_layout.remove_child(chat_layout.get_children()[0]) + +# TODO: Can't get badges when the connection is annonymous, we should clear this method +func get_badges(chatter: VSTChatter) -> String: + var badges:= "" + for badge in chatter.tags.badges: + var result = await twitch_chat.get_badge(badge, chatter.tags.badges[badge], chatter.tags.user_id) + if result: + badges += "[img=center]" + result.resource_path + "[/img] " + return badges + +func add_emotes(chatter: VSTChatter): + if chatter.tags.emotes.is_empty(): return + + var locations: Array = [] + for emote in chatter.tags.emotes: + for data in chatter.tags.emotes[emote].split(","): + var start_end = data.split("-") + locations.append(VSTEmoteLocation.new(emote, int(start_end[0]), int(start_end[1]))) + + locations.sort_custom(Callable(VSTEmoteLocation, "smaller")) + var offset = 0 + for loc in locations: + var result = await twitch_chat.get_emote(loc.id) + var emote_string = "[img=center]" + result.resource_path +"[/img]" + var pre: String = chatter.message.substr(0, loc.start + offset) + var post: String = chatter.message.substr(loc.end + offset + 1) + chatter.message = pre + emote_string + post + offset += emote_string.length() + loc.start - loc.end - 1 + +func is_scroll_bottom() -> bool: + var scroll_bar = chat_scroll.get_v_scroll_bar() + return chat_scroll.scroll_vertical >= scroll_bar.max_value - scroll_bar.get_rect().size.y + + +# Returns escaped BBCode that won't be parsed by RichTextLabel as tags. +func escape_bbcode(bbcode_text) -> String: + return bbcode_text.replace("[", "[lb]") + +func clear_all_messages(): + for childen in chat_layout.get_children(): + chat_layout.remove_child(childen) + +func show_chat_layout(): + disconnect_button.visible = true + clear_button.visible = true + channel_line_edit.visible = false + connect_button.visible = false + +func show_connect_layout(): + disconnect_button.visible = false + clear_button.visible = false + channel_line_edit.editable = true + channel_line_edit.visible = true + connect_button.visible = true + connect_button.disabled = false diff --git a/addons/very-simple-twitch/chat/vst_chat_dock.gd.uid b/addons/very-simple-twitch/chat/vst_chat_dock.gd.uid new file mode 100644 index 0000000..3655123 --- /dev/null +++ b/addons/very-simple-twitch/chat/vst_chat_dock.gd.uid @@ -0,0 +1 @@ +uid://lsv481dwwwpu diff --git a/addons/very-simple-twitch/chat/vst_chat_dock.tscn b/addons/very-simple-twitch/chat/vst_chat_dock.tscn new file mode 100644 index 0000000..11c7d48 --- /dev/null +++ b/addons/very-simple-twitch/chat/vst_chat_dock.tscn @@ -0,0 +1,111 @@ +[gd_scene load_steps=4 format=3 uid="uid://c8jard0jvmfmt"] + +[ext_resource type="Script" path="res://addons/very-simple-twitch/chat/vst_chat_dock.gd" id="1_fkddh"] + +[sub_resource type="Image" id="Image_dfhsm"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_lqqxv"] +image = SubResource("Image_dfhsm") + +[node name="VstChatDock" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_fkddh") + +[node name="TabContainer" type="TabContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +current_tab = 0 + +[node name="Chat" type="VBoxContainer" parent="TabContainer"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/Chat"] +layout_mode = 2 + +[node name="ChannelLineEdit" type="LineEdit" parent="TabContainer/Chat/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Channel name" + +[node name="ConnectButton" type="Button" parent="TabContainer/Chat/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +disabled = true +text = "CONNECT" + +[node name="ClearButton" type="Button" parent="TabContainer/Chat/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +text = "CLEAR CHAT" + +[node name="DisconnectButton" type="Button" parent="TabContainer/Chat/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +text = "DISCONNECT" + +[node name="VBoxContainer" type="VBoxContainer" parent="TabContainer/Chat"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Chat" type="Panel" parent="TabContainer/Chat/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ChatScroll" type="ScrollContainer" parent="TabContainer/Chat/VBoxContainer/Chat"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ChatLayout" type="VBoxContainer" parent="TabContainer/Chat/VBoxContainer/Chat/ChatScroll"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -8.0 +offset_bottom = 33.0 +grow_horizontal = 0 +alignment = 2 + +[node name="SupportButton" type="Button" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 8 +tooltip_text = "Support me on Ko-fi" +icon = SubResource("ImageTexture_lqqxv") +flat = true + +[connection signal="text_changed" from="TabContainer/Chat/HBoxContainer/ChannelLineEdit" to="." method="_on_line_edit_text_changed"] +[connection signal="pressed" from="TabContainer/Chat/HBoxContainer/ConnectButton" to="." method="_on_button_pressed"] +[connection signal="pressed" from="TabContainer/Chat/HBoxContainer/ClearButton" to="." method="_on_clear_button_pressed"] +[connection signal="pressed" from="TabContainer/Chat/HBoxContainer/DisconnectButton" to="." method="_on_disconnect_button_pressed"] +[connection signal="pressed" from="HBoxContainer/SupportButton" to="." method="_on_support_button_pressed"] diff --git a/addons/very-simple-twitch/chat/vst_chat_dock_line.gd b/addons/very-simple-twitch/chat/vst_chat_dock_line.gd new file mode 100644 index 0000000..19cce18 --- /dev/null +++ b/addons/very-simple-twitch/chat/vst_chat_dock_line.gd @@ -0,0 +1,9 @@ +@tool +extends HBoxContainer + +func set_chatter_msg(badges: String, chatter: VSTChatter): + set_chatter_string("%02d:%02d" %[chatter.date_time_dict["hour"], chatter.date_time_dict["minute"]] + " " + badges + " [b][color="+ chatter.tags.color_hex + "]" +chatter.tags.display_name +"[/color][/b]: " + chatter.message) + +func set_chatter_string(message:String): + $RichTextLabel.text = message + queue_sort() diff --git a/addons/very-simple-twitch/chat/vst_chat_dock_line.gd.uid b/addons/very-simple-twitch/chat/vst_chat_dock_line.gd.uid new file mode 100644 index 0000000..e61f97e --- /dev/null +++ b/addons/very-simple-twitch/chat/vst_chat_dock_line.gd.uid @@ -0,0 +1 @@ +uid://u504g1u250up diff --git a/addons/very-simple-twitch/chat/vst_chat_dock_line.tscn b/addons/very-simple-twitch/chat/vst_chat_dock_line.tscn new file mode 100644 index 0000000..6634342 --- /dev/null +++ b/addons/very-simple-twitch/chat/vst_chat_dock_line.tscn @@ -0,0 +1,16 @@ +[gd_scene load_steps=2 format=3 uid="uid://bo1un8cwcrpqr"] + +[ext_resource type="Script" path="res://addons/very-simple-twitch/chat/vst_chat_dock_line.gd" id="1_h0lnd"] + +[node name="VstChatDockLine" type="HBoxContainer"] +offset_right = 1.0 +offset_bottom = 115.0 +size_flags_horizontal = 3 +script = ExtResource("1_h0lnd") + +[node name="RichTextLabel" type="RichTextLabel" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +bbcode_enabled = true +fit_content = true +scroll_active = false diff --git a/addons/very-simple-twitch/doc/Errors.md b/addons/very-simple-twitch/doc/Errors.md new file mode 100644 index 0000000..9d77047 --- /dev/null +++ b/addons/very-simple-twitch/doc/Errors.md @@ -0,0 +1,32 @@ +# Errors +## Information +This page is to explain the use and implementation of errors in VST. Errors have 3 parameters for code, description and extra information. + +* error_code (enum @see VST_Error.VST_Code_Error) -> This parameter groups the types of errors in a general way. +* description (String) -> The description of the error is a general description of the general grouping in human language. +* info (String) -> The information is the particular description of the error. It should be used to add extra information about the particular error. + +## New errors +To add new errors you must first ask yourself if it is necessary to add a new code or not. If necessary add the code in the VST_Error.VST_Code_Error enumeration. +To illustrate the error let's imagine that we have a function that updates the area of a triangle given a base and a height. This function updates an area parameter only if it is possible to calculate the area (the base or the height is greater than 0). + +```GDScript +func calculateArea(base:int, heigth:int): + if base <= 0: + var error:VST_Error = VST_Error.new(VST_Error.VST_Code_Error.PARAM_ERROR, "base can't be less than or equals 0") + push_warning(str(error)) + area = 0 + elif height <= 0: + var error:VST_Error = VST_Error.new(VST_Error.VST_Code_Error.PARAM_ERROR, "height can't be less than or equals 0") + push_warning(str(error)) + area = 0 + else + area = (base*height)/2 +``` + +## Error Codes + +* PARAM_ERROR Use this code for errors that have to do with invalid parameters. An int that should be a String or a String that cannot be empty. +* TIMEOUT_ERROR Use this code when a timer runs out or there is no response from a system. +* NETWORK_ERROR Use this code when there is a network error ( > 400 and < 500) +* SERVER_ERROR Use this code when, in a network communication, the server is down or has a problem ( > 500 ). diff --git a/addons/very-simple-twitch/doc/Network.md b/addons/very-simple-twitch/doc/Network.md new file mode 100644 index 0000000..ca58921 --- /dev/null +++ b/addons/very-simple-twitch/doc/Network.md @@ -0,0 +1,46 @@ +# Network +## Information +The motivation for using this wrapper is to make easier the call APIs and gather responses and errors from the server. The idea is to make it as verbose as possible using a builder pattern so the request can be read at first glance. + +Before making the network call, the wrapper will check a number of conditions within the "check_request_data" method which will throw a number of errors or warnings if the request is not well built. The wrapper is permissive with the definition of REST APIs, so you can have a GET call with body or a POST with GET parameters in the url (TBD). + +Note: To achieve this effect, it is necessary to call new() and pass a node to the "launch_request" method, which will self manage all that's necessary for it to work. + +## Usage +To use the wrapper for network requests simply build a Network_Call object and start configuring it with the to, with, etc... methods. + +* to (String) -> url destination +* with (String) -> request body +* add_get_param (String,String) y add_all_get_param(Dictionary) -> add get params to request +* add_header(String,String) y add_all_header(Dictionary) -> add headers to request +* verb(Http_Method) -> set the method for REST request +* in_time(int) -> set the timeout time to the request +* set_on_call_success (Callable) -> call this function if the request was ok (200) +* set_on_call_fail (Callable) -> call this function if the request was fail (>400) +* no_cache -> set no cache for request ( only used in GET requests ) + +## Example +```GDScript + func _onReady(): + Network_Call.new().to("https://catfact.ninja/fact").set_on_call_success(on_cat_fact).set_on_call_fail(on_error).launch_request(self) + + func on_cat_fact(result): + $Label.text = str(result) + + func on_error(error): + $Label.text = "Error requesting fact about cats :(" +``` +## Cache +The network module has a cache for GET requests to save time and bandwidth for similar requests in 'short' time spans. + +Since nodes are ephemeral (network requests are nodes that disappear) the cache is stored on disk and not in memory (this resposability was left for the upper layers). + +The cache works as follows: +* 1. The request is hashed ( using the url ) +* 2. A file with the specific name ( the hash ) is searched for on disk. +* 3.a If it exists and it has not passed 300 seconds ( arbitrary and improvable time using an etag? ) the request is resolved and returns the file content. +* 3.b If the file does not exist or it has been more than 300 seconds, the request is made and cached using the same hash. + +The time validity of this cache is on CACHE_TIME_IN_SECONDS constant in network_call.gd + +The cached content is an array of bytes due to the heterogeneous nature of the possible requests (text, images, sound...). diff --git a/addons/very-simple-twitch/doc/Testing.md b/addons/very-simple-twitch/doc/Testing.md new file mode 100644 index 0000000..45a1173 --- /dev/null +++ b/addons/very-simple-twitch/doc/Testing.md @@ -0,0 +1,21 @@ +# Testing + +## Adding test + +Use "assert_eq" for "primitive" values and "assert_eq_deep" for complex data + +For add some test be sure: +1. Your test are inside /test folder +2. Your test file starts with test name +3. Your test script extends from "GutTest" +4. Your test methods starts also with test + +You can use some useful methods for testing like: +* before_all -> Excecuted before all tests in the script +* before_each -> Excecuted before each test in the script +* after_each -> Excecuted after each test in the script +* after_all -> Excecuted after all tests in the script + + +## GUT Documentation +https://gut.readthedocs.io/en/latest/ diff --git a/addons/very-simple-twitch/doc/develop.md b/addons/very-simple-twitch/doc/develop.md new file mode 100644 index 0000000..c7eebd1 --- /dev/null +++ b/addons/very-simple-twitch/doc/develop.md @@ -0,0 +1,32 @@ +# Develop + +## GDLint +The idea behind installing a linter in this plugin is mainly code readability. That can make collaboration easier for other developers. Using the same consistent coding style makes it easier to collaborate with others and easier to understand what the plugin is doing. + +### Installation + +There are a few steps before you can use GDLint. You can consult the official documentation, but here's a summary + +1. GDLinter is installed on addons folders. You don't need do nothing with it. +2. Deactivate GDLint from pluggins ( project -> configuration -> plugins ) +3. Check your python version using 'python --version' or 'py --version' + - no version installed? + - Check windows store for windows ( best option in my opinion ) + - On Mac 'brew install python' using Homebrew + - On Linux, you can use APT 'sudo apt install python3' +4. Install godot toolkit using 'pip3 install "gdtoolkit==4.*"' + - No pip installed? Download the script, from https://bootstrap.pypa.io/get-pip.py and type 'python get-pip.py' to install it. +5. Check gdlint version with 'gdlint --version' + - Nothing or error showed? try repeating the steps from 2 +6. Activate GdLint again. + - If GDLint menu at the bottom doesnt appear, relaunch godot. + + +GDScript Toolkit Documentation -> https://github.com/Scony/godot-gdscript-toolkit +GDLinter Addon Documentation -> https://github.com/el-falso/gdlinter/ + +### Usage + +As soon as you install the plugin and the toolkit you will see a menu at the bottom called GDLint. There it will show the problems with the code :) + + diff --git a/addons/very-simple-twitch/doc/img/gdlint-usage-1.png b/addons/very-simple-twitch/doc/img/gdlint-usage-1.png new file mode 100644 index 0000000..a3ce334 Binary files /dev/null and b/addons/very-simple-twitch/doc/img/gdlint-usage-1.png differ diff --git a/addons/very-simple-twitch/doc/img/gdlint-usage-1.png.import b/addons/very-simple-twitch/doc/img/gdlint-usage-1.png.import new file mode 100644 index 0000000..1fa3a87 --- /dev/null +++ b/addons/very-simple-twitch/doc/img/gdlint-usage-1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://beqmxv0pbk5dp" +path="res://.godot/imported/gdlint-usage-1.png-06a3e1d7a5f85d1fd83e80b21ea14d73.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/very-simple-twitch/doc/img/gdlint-usage-1.png" +dest_files=["res://.godot/imported/gdlint-usage-1.png-06a3e1d7a5f85d1fd83e80b21ea14d73.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/very-simple-twitch/dock/vst-dock.gd b/addons/very-simple-twitch/dock/vst-dock.gd new file mode 100644 index 0000000..32571e8 --- /dev/null +++ b/addons/very-simple-twitch/dock/vst-dock.gd @@ -0,0 +1,62 @@ +@tool +extends Control + +@onready var copy_button: Button = %CopyButton +@onready var redirect_uri = %RedirectURI +@onready var client_id_line_edit = %ClientIDLineEdit +@onready var client_id_warning = %ClientIDWarning +@onready var help_icon = %HelpIcon +@onready var warning_icon = %WarningIcon + + +func _ready(): + help_icon.texture = get_theme_icon("Help", "EditorIcons") + warning_icon.texture = get_theme_icon("StatusWarning", "EditorIcons") + copy_button.icon = get_theme_icon("ActionCopy", "EditorIcons") + copy_button.tooltip_text = "Copy Redirect URL to clipboard" + client_id_warning.add_theme_color_override( + "font_color", + EditorInterface.get_editor_settings()\ + .get_setting("text_editor/theme/highlighting/comment_markers/warning_color") + ) + + visibility_changed.connect(on_visibility_changed) + + +func on_visibility_changed(): + if visible: + update_visuals() + ProjectSettings.settings_changed.connect(update_visuals) + else: + if ProjectSettings.settings_changed.is_connected(update_visuals): + ProjectSettings.settings_changed.disconnect(update_visuals) + + +func update_visuals(): + var client_id = VSTSettings.get_setting(VSTSettings.settings.client_id) + var redirect_host = VSTSettings.get_setting(VSTSettings.settings.redirect_host) + var redirect_port = VSTSettings.get_setting(VSTSettings.settings.redirect_port) + var uuid = VSTSettings.get_setting(VSTSettings.settings.uuid) + redirect_uri.text = redirect_host + str(redirect_port) + "/" + uuid + client_id_line_edit.text = client_id + if client_id == "": + client_id_warning.show() + warning_icon.show() + else: + client_id_warning.hide() + warning_icon.hide() + + +func copy_redirect_uri(): + DisplayServer.clipboard_set(redirect_uri.text) + + +func client_id_submitted(): + ProjectSettings.set_setting( + "very_simple_twitch/"+VSTSettings.settings.client_id.path, + client_id_line_edit.text + ) + ProjectSettings.save() + +func open_url(meta): + OS.shell_open(meta) diff --git a/addons/very-simple-twitch/dock/vst-dock.gd.uid b/addons/very-simple-twitch/dock/vst-dock.gd.uid new file mode 100644 index 0000000..0395153 --- /dev/null +++ b/addons/very-simple-twitch/dock/vst-dock.gd.uid @@ -0,0 +1 @@ +uid://bn73uslhjp8aj diff --git a/addons/very-simple-twitch/dock/vst-dock.tscn b/addons/very-simple-twitch/dock/vst-dock.tscn new file mode 100644 index 0000000..98da955 --- /dev/null +++ b/addons/very-simple-twitch/dock/vst-dock.tscn @@ -0,0 +1,111 @@ +[gd_scene load_steps=4 format=3 uid="uid://m77ohpfa7qef"] + +[ext_resource type="Script" path="res://addons/very-simple-twitch/dock/vst-dock.gd" id="1_kyfdh"] + +[sub_resource type="Image" id="Image_cb0pc"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_oh773"] +image = SubResource("Image_cb0pc") + +[node name="vst-dock" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_kyfdh") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="HelpIcon" type="TextureRect" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +texture = SubResource("ImageTexture_oh773") +stretch_mode = 5 + +[node name="RichTextLabel2" type="RichTextLabel" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 2 +bbcode_enabled = true +text = " [url=https://github.com/rothiotome/godot-very-simple-twitch?tab=readme-ov-file#quick-setup]Quick Setup section in the readme[/url] and follow the steps in there. +" +fit_content = true +selection_enabled = true + +[node name="Label" type="Label" parent="."] +layout_mode = 2 +text = "Quick Start Guide" + +[node name="RedirectURI" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="Label" type="Label" parent="RedirectURI"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Redirect URI: " + +[node name="RedirectURIContainer" type="HBoxContainer" parent="RedirectURI"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 2.0 + +[node name="RedirectURI" type="RichTextLabel" parent="RedirectURI/RedirectURIContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 2 +text = "Value" +fit_content = true +selection_enabled = true + +[node name="CopyButton" type="Button" parent="RedirectURI/RedirectURIContainer"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Copy Redirect URL to clipboard" +icon = SubResource("ImageTexture_oh773") + +[node name="ClientID" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="Label" type="Label" parent="ClientID"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Client ID:" + +[node name="ClientIDLineEdit" type="LineEdit" parent="ClientID"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 2.0 +placeholder_text = "YOUR CLIENT ID HERE" + +[node name="Warning" type="HBoxContainer" parent="."] +layout_mode = 2 +alignment = 1 + +[node name="WarningIcon" type="TextureRect" parent="Warning"] +unique_name_in_owner = true +layout_mode = 2 +texture = SubResource("ImageTexture_oh773") +stretch_mode = 5 + +[node name="ClientIDWarning" type="Label" parent="Warning"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(0.72, 0.61, 0.48, 1) +text = "To use VST, you need a Client ID. Check the readme above for how to get one." + +[connection signal="meta_clicked" from="HBoxContainer/RichTextLabel2" to="." method="open_url"] +[connection signal="pressed" from="RedirectURI/RedirectURIContainer/CopyButton" to="." method="copy_redirect_uri"] +[connection signal="focus_exited" from="ClientID/ClientIDLineEdit" to="." method="client_id_submitted"] +[connection signal="text_submitted" from="ClientID/ClientIDLineEdit" to="." method="client_id_submitted"] diff --git a/addons/very-simple-twitch/emote_location.gd b/addons/very-simple-twitch/emote_location.gd new file mode 100644 index 0000000..e09ab99 --- /dev/null +++ b/addons/very-simple-twitch/emote_location.gd @@ -0,0 +1,15 @@ +class_name VSTEmoteLocation + +extends RefCounted + +var id : String +var start : int +var end : int + +func _init(emote_id, start_idx, end_idx): + self.id = emote_id + self.start = start_idx + self.end = end_idx + +static func smaller(a: VSTEmoteLocation, b: VSTEmoteLocation): + return a.start < b.start diff --git a/addons/very-simple-twitch/emote_location.gd.uid b/addons/very-simple-twitch/emote_location.gd.uid new file mode 100644 index 0000000..f337757 --- /dev/null +++ b/addons/very-simple-twitch/emote_location.gd.uid @@ -0,0 +1 @@ +uid://dc4gv3t0sj482 diff --git a/addons/very-simple-twitch/icon.png b/addons/very-simple-twitch/icon.png new file mode 100644 index 0000000..35f91d3 Binary files /dev/null and b/addons/very-simple-twitch/icon.png differ diff --git a/addons/very-simple-twitch/icon.png.import b/addons/very-simple-twitch/icon.png.import new file mode 100644 index 0000000..c72375e --- /dev/null +++ b/addons/very-simple-twitch/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://crhtjoakp6j05" +path="res://.godot/imported/icon.png-952ca81aead1a8efe9432e636ee0d5ac.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/very-simple-twitch/icon.png" +dest_files=["res://.godot/imported/icon.png-952ca81aead1a8efe9432e636ee0d5ac.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 diff --git a/addons/very-simple-twitch/models/chatter.gd b/addons/very-simple-twitch/models/chatter.gd new file mode 100644 index 0000000..fb2a9ad --- /dev/null +++ b/addons/very-simple-twitch/models/chatter.gd @@ -0,0 +1,19 @@ +class_name VSTChatter + +var date_time_dict: Dictionary +var login: String +var channel: String +var message: String +var tags: VSTIRCTags + + +func is_mod() -> bool: + return tags.badges.has("moderator") + + +func is_sub() -> bool: + return tags.badges.has("subscriber") + + +func is_broadcaster() -> bool: + return tags.badges.has("broadcaster") diff --git a/addons/very-simple-twitch/models/chatter.gd.uid b/addons/very-simple-twitch/models/chatter.gd.uid new file mode 100644 index 0000000..bfa6063 --- /dev/null +++ b/addons/very-simple-twitch/models/chatter.gd.uid @@ -0,0 +1 @@ +uid://b5jggcdr67bax diff --git a/addons/very-simple-twitch/models/irc_tags.gd b/addons/very-simple-twitch/models/irc_tags.gd new file mode 100644 index 0000000..0bd34b1 --- /dev/null +++ b/addons/very-simple-twitch/models/irc_tags.gd @@ -0,0 +1,13 @@ +class_name VSTIRCTags + +# Model for a IRC twitch chat +var color_hex: String # color of user used in twtich chat +var display_name: String # name of a user +var channel_id: String # not used +var user_id: String # numeric id of the user used in twitch + +var badges: Dictionary # badges of the user in message +var emotes: Dictionary # emotes writed by user in message + +func _to_string(): + return "color_hex: %s, display_name: %s, channel_id: %s, user_id: %s, badges: %s, emotes: %s" % [color_hex, display_name, str(channel_id), str(user_id), badges, emotes] diff --git a/addons/very-simple-twitch/models/irc_tags.gd.uid b/addons/very-simple-twitch/models/irc_tags.gd.uid new file mode 100644 index 0000000..ae411bb --- /dev/null +++ b/addons/very-simple-twitch/models/irc_tags.gd.uid @@ -0,0 +1 @@ +uid://dngsmhmc1s3ts diff --git a/addons/very-simple-twitch/models/twitch_channel.gd b/addons/very-simple-twitch/models/twitch_channel.gd new file mode 100644 index 0000000..887db4d --- /dev/null +++ b/addons/very-simple-twitch/models/twitch_channel.gd @@ -0,0 +1,5 @@ +class_name VSTChannel + +var login: String +var id: String +var token: String diff --git a/addons/very-simple-twitch/models/twitch_channel.gd.uid b/addons/very-simple-twitch/models/twitch_channel.gd.uid new file mode 100644 index 0000000..c3131be --- /dev/null +++ b/addons/very-simple-twitch/models/twitch_channel.gd.uid @@ -0,0 +1 @@ +uid://b5hhkjxmiuh35 diff --git a/addons/very-simple-twitch/network_call.gd b/addons/very-simple-twitch/network_call.gd new file mode 100644 index 0000000..07cc4c3 --- /dev/null +++ b/addons/very-simple-twitch/network_call.gd @@ -0,0 +1,198 @@ +@tool +class_name VSTNetwork_Call extends Node + +const CACHE_TIME_IN_SECONDS = 300 + +# TODO: add dynamic version here +const USER_AGENT = {"User-Agent": "VSTC/0.1.0 (Godot Engine)"} + +const CACHE_PATH = "user://very-simple-chat/cache" + +var url: String +var timeout: float +var body: String +var headers: Dictionary +var get_params: Dictionary +var on_call_success: Callable +var on_call_fail: Callable +var method: HTTPClient.Method +var use_cache: bool + + +func _init(): + headers = {} + get_params = {} + timeout = 0.0 + use_cache = true + + +func to(url_request: String) -> VSTNetwork_Call: + url = url_request + return self + + +func with(body_object) -> VSTNetwork_Call: + body = JSON.stringify(body_object) + return self + + +func in_time(timeout_request: float) -> VSTNetwork_Call: + timeout = timeout_request + return self + + +func verb(method_request: HTTPClient.Method) -> VSTNetwork_Call: + method = method_request + return self + + +func no_cache() -> VSTNetwork_Call: + use_cache = false + return self + + +func add_header(key_header: String, value_header: String) -> VSTNetwork_Call: + headers[key_header] = value_header + return self + + +func add_all_headers(headers_dic: Dictionary) -> VSTNetwork_Call: + for key in headers_dic: + headers[key] = headers_dic[key] + return self + + +func add_get_param(key_get_param:String, value_get_param) -> VSTNetwork_Call: + get_params[key_get_param] = str(value_get_param) + return self + + +func add_all_get_params(get_params_dic: Dictionary) -> VSTNetwork_Call: + for key in get_params_dic: + get_params[key] = get_params_dic[key] + return self + + +func set_on_call_success(on_call_success_request: Callable) -> VSTNetwork_Call: + on_call_success = on_call_success_request + return self + + +func set_on_call_fail(on_call_fail_request: Callable) -> VSTNetwork_Call: + on_call_fail = on_call_fail_request + return self + + +func _pile_headers(headers_to_pile: Dictionary) -> PackedStringArray: + var array:PackedStringArray = [] + for key in headers_to_pile: + array.append("%s: %s" % [key, headers_to_pile[key]]) + for key in USER_AGENT: + array.append("%s: %s" % [key, USER_AGENT[key]]) + return array + + +func _compile_url(host: String, params_to_pile: Dictionary) -> String: + var final_url:String = host.strip_edges()+"?" + for key in params_to_pile: + var value_safe = str(params_to_pile[key]).uri_encode() + var key_safe = str(key).uri_encode() + final_url += "&%s=%s" % [key_safe, value_safe] + return final_url + + +func launch_request(parent: Node): + if method == HTTPClient.Method.METHOD_GET: + var final_url = _compile_url(url, get_params) + if use_cache: + var cached_data = read_from_cache(get_key_from_url(final_url)) + if !cached_data.is_empty() && on_call_success != null: + on_call_success.call(cached_data) + return + _launch_network_request(parent) + + +func _launch_network_request(parent: Node): + var data_error = check_request_data() + if data_error != "": + var error:VSTError = VSTError.new(VSTError.VSTCodeError.PARAM_ERROR, data_error) + on_call_fail.call(error) + return + + parent.add_child(self) + await get_tree().process_frame + + var final_url = _compile_url(url, get_params) + var client = HTTPRequest.new() + client.timeout = timeout + client.request_completed.connect(func(): + on_request_completed.bind(final_url) + client.queue_free() + ) + + add_child(client) + await get_tree().process_frame + + var request_error = client.request(final_url, _pile_headers(headers), method, body) + if request_error != OK: + var error:VSTError = VSTError.new(VSTError.VSTCodeError.PARAM_ERROR, "The request can't be archieved reason: "+str(request_error)) + on_call_fail.call(error) + client.queue_free() + return + + +func check_request_data() -> String: + if timeout < 0.0: + push_warning("Timeout can't be less than 0. Setted to 0") + timeout = 0 + + if !method: + method = HTTPClient.Method.METHOD_GET + + if url == null or url.strip_edges() == "": + return "Url can't be empty" + return "" + + +func get_key_from_url(url:String) -> String: + var last_part_url:String = url.substr(url.rfind("/")) + return last_part_url.sha256_text() + + +func on_request_completed(_result: int, status: int, _headers: PackedStringArray, body: PackedByteArray, url_request: String): + if (status >= 200 and status < 400): + if method == HTTPClient.Method.METHOD_GET: update_cache(body, get_key_from_url(url_request)) + if on_call_success: on_call_success.call(body) + elif (status >= 400 and status < 500): + if on_call_fail: + var info = "%s -> %s" % [str(status), body.get_string_from_utf8()] + var error:VSTError = VSTError.new(VSTError.VSTCodeError.NETWORK_ERROR, info) + on_call_fail.call(error) + elif (status >= 500): + if on_call_fail: + var info = "%s -> %s" % [str(status), body.get_string_from_utf8()] + var error:VSTError = VSTError.new(VSTError.VSTCodeError.SERVER_ERROR, info) + on_call_fail.call(error) + call_deferred("queue_free") + +func read_from_cache(key:String) -> PackedByteArray: + var filename: String = CACHE_PATH.path_join(key) + if FileAccess.file_exists(filename): # is a hit on cache? + if FileAccess.get_modified_time(filename)+CACHE_TIME_IN_SECONDS > Time.get_unix_time_from_system(): # is expired? + return FileAccess.get_file_as_bytes(filename) + return [] + + +func update_cache(content:PackedByteArray, key:String): + var filename: String = CACHE_PATH.path_join(key) + DirAccess.make_dir_recursive_absolute(filename.get_base_dir()) + var file = FileAccess.open(filename, FileAccess.WRITE) + file.store_buffer(content) + file.close() + + +func clear_cache(): + DirAccess.make_dir_recursive_absolute(CACHE_PATH) + var files:PackedStringArray = DirAccess.get_files_at(CACHE_PATH) + for file in files: + DirAccess.remove_absolute(CACHE_PATH.path_join(file)) diff --git a/addons/very-simple-twitch/network_call.gd.uid b/addons/very-simple-twitch/network_call.gd.uid new file mode 100644 index 0000000..91c722a --- /dev/null +++ b/addons/very-simple-twitch/network_call.gd.uid @@ -0,0 +1 @@ +uid://eq8m4h87n1l5 diff --git a/addons/very-simple-twitch/parse_helper.gd b/addons/very-simple-twitch/parse_helper.gd new file mode 100644 index 0000000..0854702 --- /dev/null +++ b/addons/very-simple-twitch/parse_helper.gd @@ -0,0 +1,74 @@ +class_name VSTParseHelper + +# Parse login name from payload substring of twitch irc chat +static func parse_login(input_string:String) -> String: + return get_substring(input_string, ":", "!") + + +# Parse channel name from payload substring of twitch irc chat +static func parse_channel(input_string:String) -> String: + return input_string.trim_prefix("#") + + +# Parse message from payload substring of twitch irc chat +static func parse_message(input_string:String) -> String: + return input_string.trim_prefix(":").strip_edges() + + +static func parse_tags(input_string:String) -> VSTIRCTags: + var irc_tags = VSTIRCTags.new() + var tags:PackedStringArray = input_string.split(";") + + for i in len(tags): + var splitted_tag:PackedStringArray = tags[i].split("=") + + if splitted_tag.size() <= 1: continue + + match(splitted_tag[0].strip_edges()): + "badges": + irc_tags.badges = parse_badges(splitted_tag[1].split(",")) + "color": + irc_tags.color_hex = splitted_tag[1] + "display-name": + irc_tags.display_name = splitted_tag[1] + "emotes": + irc_tags.emotes = parse_emotes(splitted_tag[1].split("/")) + "room-id": + irc_tags.user_id = splitted_tag[1] + + return irc_tags + + +# Parse badges from payload substring of twitch irc chat. Returns a dictionary with the badge itself +# and the position of the badge +static func parse_badges(input:PackedStringArray) -> Dictionary: + var badges: Dictionary = {} + if input.is_empty() || input[0].is_empty(): return badges + + for i in len(input): + var substrings = input[i].split("/") + if len(substrings) > 1: + badges[substrings[0]] = substrings[1] + return badges + + +# Parse emotes from payload substring of twitch irc chat. Returns a dictionary with the emote +# itself and the position in the user message in order to replace the text with the image emote +static func parse_emotes(input:PackedStringArray) -> Dictionary: + var emotes: Dictionary = {} + if input.is_empty() || input[0].is_empty(): return emotes + for emote in input: + var substring: PackedStringArray = emote.split(":") + if len(substring) > 1: + emotes[substring[0]] = substring[1] + return emotes + + + +static func get_substring(input_string:String, starting_char:String, ending_char:String) -> String: + var first_index = input_string.find(starting_char) + var last_index = input_string.find(ending_char) + + if first_index == -1 or last_index == -1 or last_index < first_index: + return input_string + return input_string.substr(first_index + 1, last_index - first_index - 1) diff --git a/addons/very-simple-twitch/parse_helper.gd.uid b/addons/very-simple-twitch/parse_helper.gd.uid new file mode 100644 index 0000000..5bf59dc --- /dev/null +++ b/addons/very-simple-twitch/parse_helper.gd.uid @@ -0,0 +1 @@ +uid://ccyfmynot5j4y diff --git a/addons/very-simple-twitch/plugin.cfg b/addons/very-simple-twitch/plugin.cfg new file mode 100644 index 0000000..dd7078c --- /dev/null +++ b/addons/very-simple-twitch/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Very Simple Twitch" +description="Godot websocket implementation for Twitch Chat using OICD Authentication flow" +author="RothioTome & collaborators" +version="0.1.0" +script="vst.gd" diff --git a/addons/very-simple-twitch/public/index.html b/addons/very-simple-twitch/public/index.html new file mode 100644 index 0000000..d86e6c5 --- /dev/null +++ b/addons/very-simple-twitch/public/index.html @@ -0,0 +1,26 @@ + + +
+ + + + + + + + +