@tool extends Twitcher class_name TwitcherExtended #region Signals signal _waiting_for_authentication #endregion #region Constants const POLL_TIMEOUT_MS: int = 30000 #endregion #region Exports @export_group("Twitcher Settings") @export var oauth_settings: OAuthSetting = preload("res://addons/twitcher/twitch_oauth_setting.tres") @export var scopes: OAuthScopes = preload("res://addons/twitcher/auth/preset_overlay_scopes.tres") @export var streamer_token: OAuthToken @export var chatbot_token: OAuthToken #region Node Exports @export_subgroup("Twitcher Nodes") @export var service: TwitchService @export var chat: TwitchChat @export var bot: TwitchBot @export var chatbot_auth: ChatbotAuthorization #endregion #region Twitcher Child Node Exports @export_subgroup("Twitcher Child Nodes") @export var eventsub: TwitchEventsub: get(): if service: return service.eventsub return null @export var api: TwitchAPI: get(): if service: return service.api return null @export var auth: TwitchAuth: get(): if service: return service.auth return null @export var media: TwitchMediaLoader: get(): if service: return service.media_loader return null #endregion #endregion #region Public Variables var streamer_user: TwitchUser var bot_user: TwitchUser #endregion #region Private Variables var _cache_users: Dictionary[String, TwitchUser] = {} var _log: TwitchLogger = TwitchLogger.new("TwitcherExtended") var _commands: Dictionary[String, TwitchCommand] = {} #endregion enum AuthStatus { UNAUTHORIZED, AUTHORIZED, NEEDS_REFRESH } #region Support Functions / Overriden Methods func _init() -> void: child_entered_tree.connect(_handle_child_entered) child_exiting_tree.connect(_handle_child_exiting) func _ensure_nodes() -> void: if not service: # Ensure Service service = TwitchService.new() service.name = "Service" add_child(service) service.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner var tn: Twitcher = null # Ensure Auth tn = TwitchAuth.new() tn.name = "Auth" service.add_child(tn) tn.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner # Ensure API tn = TwitchAPI.new() tn.name = "API" service.add_child(tn) tn.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner # Ensure Eventsub tn = TwitchEventsub.new() tn.name = "Eventsub" service.add_child(tn) tn.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner # Ensure MediaLoader tn = TwitchMediaLoader.new() tn.name = "Media" service.add_child(tn) tn.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner if not chat: chat = TwitchChat.new() chat.name = "Chat" chat.subscribe_on_ready = false add_child(chat) chat.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner if not bot: bot = TwitchBot.new() bot.name = "Bot" add_child(bot) bot.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner if not chatbot_auth: chatbot_auth = ChatbotAuthorization.new() chatbot_auth.name = "ChatbotAuth" add_child(chatbot_auth) chatbot_auth.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner func _check_nodes() -> bool: if service == null: return false if eventsub == null: return false if api == null: return false if auth == null: return false if media == null: return false return true #endregion #region Event Handlers func _handle_child_entered(child: Node) -> void: if child is TwitchService: service = child if child is TwitchChat: chat = child if child is TwitchBot: bot = child if child is ChatbotAuthorization: chatbot_auth = child func _handle_child_exiting(child: Node) -> void: if child is TwitchService and child == service: service = null if child is TwitchChat and child == chat: chat = null if child is TwitchBot and child == bot: bot = null if child is ChatbotAuthorization and child == chatbot_auth: chatbot_auth = null #endregion #region Godot Overrides func _ready() -> void: _log.d("is ready") _ensure_nodes() # Ensure Settings are properly setup service.oauth_setting = oauth_settings service.scopes = scopes service.token = streamer_token chat.api = service.api chat.eventsub = service.eventsub chat.media_loader = service.media_loader bot.oauth_setting = oauth_settings chatbot_auth.oauth_setting = oauth_settings chatbot_auth.token = chatbot_token auth.token_handler.unauthenticated.connect(_waiting_for_authentication.emit) auth.token_handler.token_resolved.connect(_waiting_for_authentication.emit) #endregion #region Streamer Private Functions func _twitcher_setup() -> void: _log.d("Setting up nodes...") service.propagate_call(&"do_setup") for child in service.get_children(): if child.has_method(&"wait_setup"): await child.wait_setup() #endregion #region Streamer Public Functions func load_streamer_token() -> AuthStatus: _log.d("Loading streamer tokens...") var res = streamer_token._load_tokens() if not res: _log.d("Token doesn't exist") return AuthStatus.UNAUTHORIZED if streamer_token.is_token_valid(): _log.d("Token authorized") return AuthStatus.AUTHORIZED if streamer_token.has_refresh_token(): _log.d("Token needs refreshed") return AuthStatus.NEEDS_REFRESH _log.d("Token invalid") return AuthStatus.UNAUTHORIZED func is_streamer_authed() -> bool: return auth.is_authenticated func is_streamer_token_valid() -> bool: return streamer_token.is_token_valid() func authorize_streamer() -> bool: return await auth.authorize() func setup_streamer() -> bool: _log.d("Setup streamer") load_streamer_token() if is_streamer_authed() and is_streamer_token_valid(): _log.d("Saved tokens validated.") _twitcher_setup() streamer_user = await service.get_current_user() chat.broadcaster_user = streamer_user bot.receiver = streamer_user chat.subscribe() return true var res: bool = false if streamer_token.has_refresh_token(): _log.d("Refreshing tokens...") auth.refresh_token() await _waiting_for_authentication res = is_streamer_token_valid() else: _log.d("Acquiring token...") res = await auth.authorize() if res: _log.d("Tokens validated.") _twitcher_setup() streamer_user = await service.get_current_user() chat.broadcaster_user = streamer_user chat.sender_user = streamer_user bot.receiver = streamer_user chat.subscribe() return res #endregion #region Chatbot Functions func load_chatbot_token() -> AuthStatus: _log.d("Loading chatbot tokens...") var res = chatbot_token._load_tokens() if not res: _log.d("Token doesn't exist") return AuthStatus.UNAUTHORIZED if chatbot_token.is_token_valid(): _log.d("Token authroized") return AuthStatus.AUTHORIZED if chatbot_token.has_refresh_token(): _log.d("Token needs refreshed") return AuthStatus.NEEDS_REFRESH _log.d("Token invalid") return AuthStatus.UNAUTHORIZED func is_chatbot_authed() -> bool: return auth.is_authenticated func is_chatbot_token_valid() -> bool: return streamer_token.is_token_valid() func authorize_chatbot() -> bool: return await chatbot_auth.authenticate() func setup_chatbot() -> bool: _log.d("Setup chatbot") load_chatbot_token() var res: bool = await chatbot_auth.authenticate() if res: _log.d("Chatbot authorized.") bot_user = await chatbot_auth.get_user() bot.sender = bot_user return res #endregion #region Public API shared between both Streamer and Chatbot as needed. func send_message(message: String, as_streamer: bool = false) -> void: if as_streamer: await chat.send_message(message) else: await bot.send_message(message) func reply_message(message: String, msg_id: String, as_streamer: bool = false) -> void: if as_streamer: await chat.send_message(message, msg_id) else: await bot.send_message(message, msg_id) func get_users_by_id(...user_ids: Array) -> Array[TwitchUser]: var tusers: Array[TwitchUser] = [] var qusers: Array[String] = [] for user_id in user_ids: if _cache_users.has(user_id): tusers.append(_cache_users[user_id]) else: qusers.append(user_id) if not qusers.is_empty(): var opt = TwitchGetUsers.Opt.new() opt.id = qusers var tgur: TwitchGetUsers.Response = await api.get_users(opt) if tgur.data.is_empty(): return tusers for tgu in tgur.data: _cache_users[tgu.id] = tgu _cache_users[tgu.login] = tgu tusers.append(tgu) return tusers func get_users(...usernames: Array) -> Array[TwitchUser]: var tusers: Array[TwitchUser] = [] var qusers: Array[String] = [] for i in usernames.size(): usernames[i] = usernames[i].trim_prefix("@") for username in usernames: if _cache_users.has(username): tusers.append(_cache_users[username]) else: qusers.append(username) if not qusers.is_empty(): var opt = TwitchGetUsers.Opt.new() opt.login = qusers var tgur: TwitchGetUsers.Response = await api.get_users(opt) if tgur.data.is_empty(): return tusers for tgu in tgur.data: _cache_users[tgu.id] = tgu _cache_users[tgu.login] = tgu tusers.append(tgu) return tusers func get_user_by_id(user_id: String) -> TwitchUser: if _cache_users.has(user_id): return _cache_users[user_id] var user: TwitchUser = await service.get_user_by_id(user_id) _cache_users[user_id] = user _cache_users[user.login] = user return user func get_user(username: String) -> TwitchUser: username = username.trim_prefix("@") if _cache_users.has(username): return _cache_users[username] var user: TwitchUser = await service.get_user(username) _cache_users[user.id] = user _cache_users[username] = user return user func subscribe_event(definition: TwitchEventsubDefinition, conditions: Dictionary) -> TwitchEventsubConfig: if definition == null: _log.e("TwitchEventsubDefinition is null") return var config = TwitchEventsubConfig.create(definition, conditions) eventsub.subscribe(config) return config func wait_for_eventsub_connection() -> void: if eventsub == null: _log.e("TwitchEventsub Node is missing") return await eventsub.wait_for_connection() func get_subscriptions() -> Array[TwitchEventsubConfig]: if eventsub == null: _log.e("TwitchEventsub Node is missing") return [] return eventsub.get_subscriptions() func shoutout(user: TwitchUser, broadcaster: TwitchUser = null, moderator: TwitchUser = null) -> void: if not streamer_user: return if not broadcaster: broadcaster = streamer_user if not moderator: moderator = bot_user api.send_a_shoutout(broadcaster.id, moderator.id, user.id) func announcement(message: String, color: TwitchAnnouncementColor = TwitchAnnouncementColor.PRIMARY, as_broadcaster: bool = true) -> void: if not streamer_user: return var moderator: TwitchUser = streamer_user if as_broadcaster else bot_user var body = TwitchSendChatAnnouncement.Body.new() body.message = message body.color = color.value api.send_chat_announcement(body, moderator.id, streamer_user.id) func add_command(command: String, callback: Callable, args_min: int = 0, args_max: int = -1, permission_level: TwitchCommand.PermissionFlag = TwitchCommand.PermissionFlag.EVERYONE, where: TwitchCommand.WhereFlag = TwitchCommand.WhereFlag.CHAT, user_cooldown: float = 0, global_cooldown: float = 0) -> TwitchCommand: var command_node = TwitchCommand.new() command_node.command = command command_node.command_received.connect(callback) command_node.args_min = args_min command_node.args_max = args_max command_node.permission_level = permission_level command_node.where = where command_node.user_cooldown = user_cooldown command_node.global_cooldown = global_cooldown add_child(command_node) _commands[command] = command_node _log.i("Register command %s" % command) return command_node func remove_command(command: String) -> void: _log.i("Remove command %s" % command) var command_node: TwitchCommand = _commands.get(command, null) if command_node: command_node.queue_free() _commands.erase(command) func whisper(_message: String, _username: String, _as_streamer: bool = false) -> void: _log.e("Whisper from bots aren't supported by Twitch anymore. See https://dev.twitch.tv/docs/irc/chat-commands/ at /w") func get_emotes_data(channel_id: String = "global") -> Dictionary: return await media.get_cached_emotes(channel_id) func get_badges_data(channel_id: String = "global") -> Dictionary: return await media.get_cached_badges(channel_id) func get_emotes(ids: Array[String]) -> Dictionary[String, SpriteFrames]: return await media.get_emotes(ids) func get_emotes_by_definition(emotes: Array[TwitchEmoteDefinition]) -> Dictionary[TwitchEmoteDefinition, SpriteFrames]: return await media.get_emotes_by_definition(emotes) func poll(title: String, choices: Array[String], duration: int = 60, channel_points_voting_enabled: bool = false, channel_points_per_vote: int = 1000) -> Dictionary: var body_choices: Array[TwitchCreatePoll.BodyChoices] = [] for choice: String in choices: var body_choice = TwitchCreatePoll.BodyChoices.create(choice) body_choices.append(body_choice) duration = clamp(duration, 15, 1800) var poll_body: TwitchCreatePoll.Body = TwitchCreatePoll.Body.create(streamer_user.id, title, body_choices, duration) if channel_points_voting_enabled: poll_body.channel_points_per_vote = channel_points_per_vote poll_body.channel_points_voiting_enabled = channel_points_voting_enabled var poll_response: TwitchCreatePoll.Response = await api.create_poll(poll_body) if poll_response.response.response_code != 200: var error_message: String = poll_response.response.response_data.get_string_from_utf8() push_error("Can't create poll response cause of ", error_message) return {} var poll: TwitchPoll = poll_response.data[0] var poll_end_time: int = Time.get_ticks_msec() + duration * 100 * POLL_TIMEOUT_MS var event: TwitchEventsub.Event if eventsub && eventsub.has_subscription(TwitchEventsubDefinition.CHANNEL_POLL_END, {&"broadcaster_user_id": streamer_user.id}): var poll_ended: bool while not poll_ended: if poll_end_time < Time.get_ticks_msec(): return {} event = await eventsub.event_received if event.Type != TwitchEventsubDefinition.CHANNEL_POLL_END: continue if event.data[&"id"] != poll.id: continue break else: _log.i("Can't wait for poll end. Either eventsub is not set to it not listening to ending polls") return {} return event.data func get_cheermote_data() -> Array[TwitchCheermote]: if media == null: _log.e("TwitchMediaLoader was not set within %s" % get_tree_string()) return [] await media.preload_cheemote() return media.all_cheermotes() func get_cheermote(definition: TwitchCheermoteDefinition) -> Dictionary: return await media.get_cheermotes(definition) #endregion