diff --git a/addons/gift/gift_node.gd b/addons/gift/gift_node.gd index 88c5f2a..971104c 100644 --- a/addons/gift/gift_node.gd +++ b/addons/gift/gift_node.gd @@ -23,11 +23,21 @@ signal cmd_invalid_argcount(cmd_name, sender_data, cmd_data, arg_ary) signal cmd_no_permission(cmd_name, sender_data, cmd_data, arg_ary) # Twitch's ping is about to be answered with a pong. signal pong +# Emote has been downloaded +signal emote_downloaded(emote_id) +# Badge has been downloaded +signal badge_downloaded(badge_name) # Messages starting with one of these symbols are handled. '/' will be ignored, reserved by Twitch. -export(PoolStringArray) var command_prefixes : Array = ["!"] -# Time to wait after each sent chat message. Values below ~0.31 will lead to a disconnect after 100 messages. +export(Array, String) var command_prefixes : Array = ["!"] +# Time to wait after each sent chat message. Values below ~0.31 might lead to a disconnect after 100 messages. export(float) var chat_timeout = 0.32 +export(bool) var get_images : bool = false +# If true, caches emotes/badges to disk, so that they don't have to be redownloaded on every restart. +# This however means that they might not be updated if they change until you clear the cache. +export(bool) var disk_cache : bool = false +# Disk Cache has to be enbaled for this to work +export(String, FILE) var disk_cache_path = "user://gift/cache" var websocket : WebSocketClient = WebSocketClient.new() var user_regex = RegEx.new() @@ -38,6 +48,7 @@ onready var chat_accu = chat_timeout # Mapping of channels to their channel info, like available badges. var channels : Dictionary = {} var commands : Dictionary = {} +var image_cache : ImageCache # Required permission to execute the command enum PermissionFlag { @@ -68,6 +79,9 @@ func _ready() -> void: websocket.connect("connection_closed", self, "connection_closed") websocket.connect("server_close_request", self, "sever_close_request") websocket.connect("connection_error", self, "connection_error") + if(get_images): + image_cache = ImageCache.new(disk_cache, disk_cache_path) + add_child(image_cache) func connect_to_twitch() -> void: if(websocket.connect_to_url("wss://irc-ws.chat.twitch.tv:443") != OK): @@ -180,7 +194,13 @@ func handle_message(message : String, tags : Dictionary) -> void: var sender_data : SenderData = SenderData.new(user_regex.search(msg[0]).get_string(), msg[2], tags) handle_command(sender_data, msg) emit_signal("chat_message", sender_data, msg[3].right(1)) - print("TAGS: " + str(tags)) + if(get_images): + if(!image_cache.badge_map.has(tags["room-id"])): + image_cache.get_badge_mappings(tags["room-id"]) + for emote in tags["emotes"].split("/", false): + image_cache.get_emote(emote.split(":")[0]) + for badge in tags["badges"].split(",", false): + image_cache.get_badge(badge, tags["room-id"]) "WHISPER": var sender_data : SenderData = SenderData.new(user_regex.search(msg[0]).get_string(), msg[2], tags) handle_command(sender_data, msg, true) @@ -195,6 +215,10 @@ func handle_command(sender_data : SenderData, msg : PoolStringArray, whisper : b var command : String = msg[3].right(2) var cmd_data : CommandData = commands.get(command) if(cmd_data): + if(whisper == true && cmd_data.where & WhereFlag.WHISPER != WhereFlag.WHISPER): + return + elif(whisper == false && cmd_data.where & WhereFlag.CHAT != WhereFlag.CHAT): + return var args = "" if msg.size() < 5 else msg[4] var arg_ary : PoolStringArray = PoolStringArray() if args == "" else args.split(" ") if(arg_ary.size() > cmd_data.max_args && cmd_data.max_args != -1 || arg_ary.size() < cmd_data.min_args): @@ -264,9 +288,3 @@ func connection_error() -> void: func server_close_request(code : int, reason : String) -> void: pass - -func _enter_tree() -> void: - pass - -func _exit_tree() -> void: - pass \ No newline at end of file diff --git a/addons/gift/plugin.cfg b/addons/gift/plugin.cfg index f72b0f2..47b54ee 100644 --- a/addons/gift/plugin.cfg +++ b/addons/gift/plugin.cfg @@ -3,5 +3,5 @@ name="Godot IRC For Twitch" description="Godot websocket implementation for Twitch IRC." author="MennoMax" -version="0.1.0" +version="0.1.1" script="gift.gd" diff --git a/addons/gift/util/image_cache.gd b/addons/gift/util/image_cache.gd new file mode 100644 index 0000000..f4c587d --- /dev/null +++ b/addons/gift/util/image_cache.gd @@ -0,0 +1,111 @@ +extends Node +class_name ImageCache + +signal badge_mapping_available + +var cached_images : Dictionary = {"emotes": {}, "badges": {}} +var cache_mutex = Mutex.new() +var badge_map : Dictionary = {} +var badge_mutex = Mutex.new() +var dl_queue : PoolStringArray = [] +var disk_cache : bool +var disk_cache_path : String + +var file : File = File.new() +var dir : Directory = Directory.new() + +func _init(do_disk_cache : bool, cache_path : String) -> void: + disk_cache = do_disk_cache + disk_cache_path = cache_path + +func _ready() -> void: + if(disk_cache): + for cache_dir in cached_images.keys(): + cached_images[cache_dir] = {} + dir.make_dir_recursive(disk_cache_path + "/" + cache_dir) + dir.open(disk_cache_path + "/" + cache_dir) + dir.list_dir_begin(true) + var current = dir.get_next() + while current != "": + if(!dir.current_is_dir()): + file.open(dir.get_current_dir() + "/" + current, File.READ) + var img : Image = Image.new() + img.load_png_from_buffer(file.get_buffer(file.get_len())) + file.close() + var img_texture : ImageTexture = ImageTexture.new() + img_texture.create_from_image(img, 0) + cache_mutex.lock() + cached_images[cache_dir][current.get_basename()] = img_texture + cache_mutex.unlock() + current = dir.get_next() + dir.open(disk_cache_path) + dir.list_dir_begin(true) + var current = dir.get_next() + while current != "": + if(!dir.current_is_dir()): + file.open(disk_cache_path + "/" + current, File.READ) + badge_map[current.get_basename()] = parse_json(file.get_as_text())["badge_sets"] + file.close() + current = dir.get_next() + get_badge_mappings() + yield(self, "badge_mapping_available") + +func create_request(url : String, resource : String, res_type : String) -> void: + var http_request = HTTPRequest.new() + http_request.connect("request_completed", self, "downloaded", [http_request, resource, res_type], CONNECT_ONESHOT) + add_child(http_request) + http_request.download_file = disk_cache_path + "/" + res_type + "/" + resource + ".png" + http_request.request(url, [], false, HTTPClient.METHOD_GET) + +# Gets badge mappings for the specified channel. Empty String will get the mappings for global badges instead. +func get_badge_mappings(channel_id : String = "") -> void: + var url : String + if(channel_id == ""): + channel_id = "_global" + url = "https://badges.twitch.tv/v1/badges/global/display" + else: + url = "https://badges.twitch.tv/v1/badges/channels/" + channel_id + "/display" + if(!badge_map.has(channel_id)): + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request(url, [], false, HTTPClient.METHOD_GET) + http_request.connect("request_completed", self, "badge_mapping_received", [http_request, channel_id], CONNECT_ONESHOT) + else: + emit_signal("badge_mapping_available") + +func get_emote(id : String) -> ImageTexture: + cache_mutex.lock() + if(cached_images["emotes"].has(id)): + return cached_images["emotes"][id] + else: + create_request("http://static-cdn.jtvnw.net/emoticons/v1/" + id + "/1.0", id, "emotes") + cache_mutex.unlock() + return null + +func get_badge(badge_name : String, channel_id : String = "") -> ImageTexture: + cache_mutex.lock() + var badge_data : PoolStringArray = badge_name.split("/") + if(cached_images["badges"].has(badge_data[0])): + return cached_images["badges"][badge_data[0]] + var channel : String + if(!badge_map[channel_id].has(badge_data[0])): + channel_id = "_global" + create_request(badge_map[channel_id][badge_data[0]]["versions"][badge_data[1]]["image_url_1x"], badge_data[0], "badges") + cache_mutex.unlock() + return null + +func downloaded(result : int, response_code : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest, id : String, type : String) -> void: + if(type == "emotes"): + get_parent().emit_signal("emote_downloaded", id) + elif(type == "badges"): + get_parent().emit_signal("badge_downloaded", id) + request.queue_free() + +func badge_mapping_received(result : int, response_copde : int, headers : PoolStringArray, body : PoolByteArray, request : HTTPRequest, id : String) -> void: + badge_map[id] = parse_json(body.get_string_from_utf8())["badge_sets"] + if(disk_cache): + file.open(disk_cache_path + "/" + id + ".json", File.WRITE) + file.store_buffer(body) + file.close() + emit_signal("badge_mapping_available") + request.queue_free()