extends Control # Your client id. You can share this publicly. Default is my own client_id. # Please do not ship your project with my client_id, but feel free to test with it. # Visit https://dev.twitch.tv/console/apps/create to create a new application. # You can then find your client id at the bottom of the application console. # DO NOT SHARE THE CLIENT SECRET. If you do, regenerate it. @export var client_id : String = "9x951o0nd03na7moohwetpjjtds0or" # The name of the channel we want to connect to. @export var channel : String # The username of the bot account. @export var username : String var id : TwitchIDConnection var api : TwitchAPIConnection var irc : TwitchIRCConnection var eventsub : TwitchEventSubConnection var cmd_handler : GIFTCommandHandler = GIFTCommandHandler.new() var iconloader : TwitchIconDownloader func _ready() -> void: # We will login using the Implicit Grant Flow, which only requires a client_id. # Alternatively, you can use the Authorization Code Grant Flow or the Client Credentials Grant Flow. # Note that the Client Credentials Grant Flow will only return an AppAccessToken, which can not be used # for the majority of the Twitch API or to join a chat room. var auth : ImplicitGrantFlow = ImplicitGrantFlow.new() # For the auth to work, we need to poll it regularly. get_tree().process_frame.connect(auth.poll) # You can also use a timer if you don't want to poll on every frame. # Next, we actually get our token to authenticate. We want to be able to read and write messages, # so we request the required scopes. See https://dev.twitch.tv/docs/authentication/scopes/#twitch-access-token-scopes var token : UserAccessToken = await(auth.login(client_id, ["chat:read", "chat:edit"])) if (token == null): # Authentication failed. Abort. return # Store the token in the ID connection, create all other connections. id = TwitchIDConnection.new(token) irc = TwitchIRCConnection.new(id) api = TwitchAPIConnection.new(id) iconloader = TwitchIconDownloader.new(api) # For everything to work, the id connection has to be polled regularly. get_tree().process_frame.connect(id.poll) # Connect to the Twitch chat. if(!await(irc.connect_to_irc(username))): # Authentication failed. Abort. return # Request the capabilities. By default only twitch.tv/commands and twitch.tv/tags are used. # Refer to https://dev.twitch.tv/docs/irc/capabilities/ for all available capapbilities. irc.request_capabilities() # Join the channel specified in the exported 'channel' variable. irc.join_channel(channel) # Add a helloworld command. cmd_handler.add_command("helloworld", hello) # The helloworld command can now also be executed with "hello"! cmd_handler.add_alias("helloworld", "hello") # Add a list command that accepts between 1 and infinite args. cmd_handler.add_command("list", list, -1, 1) # For the cmd handler to receive the messages, we have to forward them. irc.chat_message.connect(cmd_handler.handle_command) # If you also want to accept whispers, connect the signal and bind true as the last arg. irc.whisper_message.connect(cmd_handler.handle_command.bind(true)) # For the chat example to work, we forward the messages received to the put_chat function. irc.chat_message.connect(put_chat) # When we press enter on the chat bar or press the send button, we want to execute the send_message # function. %LineEdit.text_submitted.connect(send_message.unbind(1)) %Button.pressed.connect(send_message) # This part of the example only works if GIFT is logged in to your broadcaster account. # If you are, you can uncomment this to also try receiving follow events. # eventsub = TwitchEventSubConnection.new(api) # eventsub.connect_to_eventsub() # eventsub.event.connect(on_event) # var user_ids : Dictionary = await(api.get_users_by_name([username])) # print(user_ids) # if (user_ids.has("data") && user_ids["data"].size() > 0): # var user_id : String = user_ids["data"][0]["id"] # eventsub.subscribe_event("channel.follow", 2, {"broadcaster_user_id": user_id, "moderator_user_id": user_id}) func hello(cmd_info : CommandInfo) -> void: irc.chat("Hello World!") func list(cmd_info : CommandInfo, arg_ary : PackedStringArray) -> void: irc.chat(", ".join(arg_ary)) func on_event(type : String, data : Dictionary) -> void: match(type): "channel.follow": print("%s followed your channel!" % data["user_name"]) func send_message() -> void: irc.chat(%LineEdit.text) %LineEdit.text = "" func put_chat(senderdata : SenderData, msg : String): var bottom : bool = %ChatScrollContainer.scroll_vertical == %ChatScrollContainer.get_v_scroll_bar().max_value - %ChatScrollContainer.get_v_scroll_bar().get_rect().size.y var label : RichTextLabel = RichTextLabel.new() var time = Time.get_time_dict_from_system() label.fit_content = true label.selection_enabled = true label.push_font_size(12) label.push_color(Color.WEB_GRAY) label.add_text("%02d:%02d " % [time["hour"], time["minute"]]) label.pop() label.push_font_size(14) var badges : Array[Texture2D] for badge in senderdata.tags["badges"].split(",", false): label.add_image(await(iconloader.get_badge(badge, senderdata.tags["room-id"])), 0, 0, Color.WHITE, INLINE_ALIGNMENT_CENTER) label.push_bold() if (senderdata.tags["color"] != ""): label.push_color(Color(senderdata.tags["color"])) label.add_text(" %s" % senderdata.tags["display-name"]) label.push_color(Color.WHITE) label.push_normal() label.add_text(": ") var locations : Array[EmoteLocation] = [] if (senderdata.tags.has("emotes")): for emote in senderdata.tags["emotes"].split("/", false): var data : PackedStringArray = emote.split(":") for d in data[1].split(","): var start_end = d.split("-") locations.append(EmoteLocation.new(data[0], int(start_end[0]), int(start_end[1]))) locations.sort_custom(Callable(EmoteLocation, "smaller")) if (locations.is_empty()): label.add_text(msg) else: var offset = 0 for loc in locations: label.add_text(msg.substr(offset, loc.start - offset)) label.add_image(await(iconloader.get_emote(loc.id)), 0, 0, Color.WHITE, INLINE_ALIGNMENT_CENTER) offset = loc.end + 1 %Messages.add_child(label) await(get_tree().process_frame) if (bottom): %ChatScrollContainer.scroll_vertical = %ChatScrollContainer.get_v_scroll_bar().max_value class EmoteLocation 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 : EmoteLocation, b : EmoteLocation): return a.start < b.start