pokepurple/addons/twitcher/twitch_service.gd
Mario Steele c11a4ebbc2 Initial Commit
Initial commit of Code Base.
2025-06-12 14:31:14 -05:00

392 lines
13 KiB
GDScript

@icon("res://addons/twitcher/assets/service-icon.svg")
@tool
extends Twitcher
## Access to the Twitch API. Combines all the stuff the library provides.
## Makes some actions easier to use.
class_name TwitchService
const TwitchEditorSettings = preload("res://addons/twitcher/editor/twitch_editor_settings.gd")
## When the poll doesn't end after the offical endtime + POLL_TIMEOUT_MS. The wait loop for poll end
## event will be stopped to prevent endless loops.
const POLL_TIMEOUT_MS: int = 30000
static var _log: TwitchLogger = TwitchLogger.new("TwitchService")
static var instance: TwitchService
@export var oauth_setting: OAuthSetting:
set(val):
if oauth_setting != null:
oauth_setting.changed.disconnect(update_configuration_warnings)
oauth_setting = val
if val != null:
oauth_setting.changed.connect(update_configuration_warnings)
_set_in_child("oauth_setting", val)
update_configuration_warnings()
@export var scopes: OAuthScopes:
set(val):
scopes = val
if val != null:
_set_in_child("scopes", val)
update_configuration_warnings()
@export var token: OAuthToken:
set(val):
token = val
if val != null:
_set_in_child("token", val)
update_configuration_warnings()
@onready var auth: TwitchAuth
@onready var eventsub: TwitchEventsub
@onready var api: TwitchAPI
@onready var irc: TwitchIRC
@onready var media_loader: TwitchMediaLoader
var _user_cache: Dictionary[String, TwitchUser] = {}
## Cache for the current user so that no roundtrip has to be done every time get_current_user will be called
var _current_user: TwitchUser
var _commands: Dictionary[String, TwitchCommand] = {}
func _init() -> void:
child_entered_tree.connect(_on_child_entered)
child_exiting_tree.connect(_on_child_exiting)
func _ready() -> void:
_log.d("is ready")
if not is_instance_valid(token): token = TwitchEditorSettings.game_oauth_token
if not is_instance_valid(oauth_setting): oauth_setting = TwitchEditorSettings.game_oauth_setting
func _enter_tree() -> void:
if instance == null: instance = self
func _exit_tree() -> void:
if instance == self: instance = null
func _on_child_entered(node: Node) -> void:
if node is TwitchAuth: auth = node
if node is TwitchAPI: api = node
if node is TwitchEventsub: eventsub = node
if node is TwitchIRC: irc = node
if node is TwitchMediaLoader: media_loader = node
if "token" in node && token != null:
node.token = token
if "scopes" in node && scopes != null:
node.scopes = scopes
if "oauth_setting" in node && oauth_setting != null:
node.oauth_setting = oauth_setting
if node.has_signal(&"unauthenticated"):
node.unauthenticated.connect(_on_unauthenticated)
update_configuration_warnings()
func _set_in_child(property: String, value: Variant) -> void:
for child in get_children():
if property in child: child[property] = value
func _on_child_exiting(node: Node) -> void:
if node is TwitchAuth: auth = null
if node is TwitchAPI: api = null
if node is TwitchEventsub: eventsub = null
if node is TwitchIRC: irc = null
if node is TwitchMediaLoader: media_loader = null
if node.has_signal(&"unauthenticated"):
node.unauthenticated.disconnect(_on_unauthenticated)
update_configuration_warnings()
## Call this to setup the complete Twitch integration whenever you need.
## It boots everything up this Lib supports.
func setup() -> bool:
if is_instance_valid(auth):
if not await auth.authorize(): return false
else:
push_error("Authorization Node got removed, can't setup twitch service")
return false
await propagate_call(&"do_setup")
for child in get_children():
if child.has_method(&"wait_setup"):
await child.wait_setup()
_log.i("TwitchService setup")
return true
## Checks if the correctly setup
func is_configured() -> bool:
return _get_configuration_warnings().is_empty()
func _get_configuration_warnings() -> PackedStringArray:
var result: PackedStringArray = []
if oauth_setting == null:
result.append("OAuthSetting Resource is missing")
else:
var oauth_setting_problems : PackedStringArray = oauth_setting.get_valididation_problems()
if not oauth_setting_problems.is_empty():
result.append("OAuthSetting Resource is invalid")
result.append_array(oauth_setting_problems)
if scopes == null:
result.append("OAuthScopes Resource is missing")
if token == null:
result.append("OAuthToken Resource is missing")
return result
func _on_unauthenticated() -> void:
auth.authorize()
#
# Convinient Proxy Methods
#
#region User
## Get data about a user by USER_ID see get_user for by username
func get_user_by_id(user_id: String) -> TwitchUser:
if _user_cache.has(user_id): return _user_cache[user_id]
if api == null:
_log.e("Please setup a TwitchAPI Node into TwitchService.")
return null
if user_id == null || user_id == "": return null
var opt = TwitchGetUsers.Opt.new()
opt.id = [user_id] as Array[String]
var user_data : TwitchGetUsers.Response = await api.get_users(opt)
if user_data.data.is_empty(): return null
var user: TwitchUser = user_data.data[0]
_user_cache[user_id] = user
return user
## Get data about a user by USERNAME see get_user_by_id for by user_id
func get_user(username: String) -> TwitchUser:
username = username.trim_prefix("@")
if _user_cache.has(username): return _user_cache[username]
if api == null:
_log.e("Please setup a TwitchAPI Node into TwitchService.")
return null
var opt = TwitchGetUsers.Opt.new()
opt.login = [username] as Array[String]
var user_data : TwitchGetUsers.Response = await api.get_users(opt)
if user_data.data.is_empty():
_log.e("Username was not found: %s" % username)
return null
var user: TwitchUser = user_data.data[0]
_user_cache[username] = user
return user
## Get data about a currently authenticated user (caches the value)
func get_current_user() -> TwitchUser:
if _current_user != null:
return _current_user
if api == null:
_log.e("Please setup a TwitchAPI Node into TwitchService.")
return null
var user_data : TwitchGetUsers.Response = await api.get_users(null)
_current_user = user_data.data[0]
return _current_user
## Get the image of an user
func load_profile_image(user: TwitchUser) -> ImageTexture:
return await media_loader.load_profile_image(user)
#endregion
#region EventSub
## Refer to https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/ for details on
## which API versions are available and which conditions are required.
func subscribe_event(definition: TwitchEventsubDefinition, conditions: Dictionary) -> TwitchEventsubConfig:
if definition == null:
_log.e("TwitchEventsubDefinition is null")
return
var config = TwitchEventsubConfig.create(definition, conditions)
await eventsub.subscribe(config)
return config
## Waits for connection to eventsub. Eventsub is ready to subscribe events.
func wait_for_eventsub_connection() -> void:
if eventsub == null:
_log.e("TwitchEventsub Node is missing")
return
await eventsub.wait_for_connection()
## Returns all of the eventsub subscriptions (variable is a copy so you can freely modify it)
func get_subscriptions() -> Array[TwitchEventsubConfig]:
if eventsub == null:
_log.e("TwitchEventsub Node is missing")
return []
return eventsub.get_subscriptions()
#endregion
#region Chat
func chat(message: String, broadcaster: TwitchUser = null, sender: TwitchUser = null) -> void:
var current_user = await get_current_user()
if not sender:
if not current_user: return
sender = current_user
if not broadcaster:
if not current_user: return
broadcaster = current_user
var body = TwitchSendChatMessage.Body.create(broadcaster.id, sender.id, message)
api.send_chat_message(body)
## Sends out a shoutout to a specific user
func shoutout(user: TwitchUser, broadcaster: TwitchUser = null, moderator: TwitchUser = null) -> void:
var current_user: TwitchUser = await get_current_user()
if not broadcaster:
if not current_user: return
broadcaster = current_user
if not moderator:
if not current_user: return
moderator = current_user
api.send_a_shoutout(broadcaster.id, moderator.id, user.id)
## Sends a announcement message to the chat
func announcment(message: String, color: TwitchAnnouncementColor = TwitchAnnouncementColor.PRIMARY, broadcaster: TwitchUser = null, moderator: TwitchUser = null):
var current_user: TwitchUser = await get_current_user()
if not broadcaster:
if not current_user: return
broadcaster = current_user
if not moderator:
if not current_user: return
moderator = current_user
var body = TwitchSendChatAnnouncement.Body.new()
body.message = message
body.color = color.value
api.send_chat_announcement(body, moderator.id, broadcaster.id)
## Add a new command handler and register it for a command.
## The callback will receive [code]from_username: String, info: TwitchCommandInfo, args: PackedStringArray[/code][br]
## Args are optional depending on the configuration.[br]
## args_max == -1 => no upper limit for arguments
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) -> 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
add_child(command_node)
_log.i("Register command %s" % command)
return command_node
## Removes a command
func remove_command(command: String) -> void:
_log.i("Remove command %s" % command)
var command_node: TwitchCommand = _commands.get(command, null)
if command_node != null:
command_node.queue_free()
_commands.erase(command)
## Whispers to another user.
## @deprecated not supported by twitch anymore
func whisper(message: String, username: String) -> void:
_log.e("Whipser from bots aren't supported by Twitch anymore. See https://dev.twitch.tv/docs/irc/chat-commands/ at /w")
## Returns the definition of emotes for given channel or for the global emotes.
## Key: EmoteID as String | Value: TwitchGlobalEmote / TwitchChannelEmote
func get_emotes_data(channel_id: String = "global") -> Dictionary:
return await media_loader.get_cached_emotes(channel_id)
## Returns the definition of badges for given channel or for the global bages.
## Key: category / versions / badge_id | Value: TwitchChatBadge
func get_badges_data(channel_id: String = "global") -> Dictionary[String, TwitchChatBadge]:
return await media_loader.get_cached_badges(channel_id)
## Gets the requested emotes.
## Key: EmoteID as String | Value: SpriteFrame
func get_emotes(ids: Array[String]) -> Dictionary[String, SpriteFrames]:
return await media_loader.get_emotes(ids)
## Gets the requested emotes in the specified theme, scale and type.
## Loads from cache if possible otherwise downloads and transforms them.
## Key: TwitchEmoteDefinition | Value SpriteFrames
func get_emotes_by_definition(emotes: Array[TwitchEmoteDefinition]) -> Dictionary[TwitchEmoteDefinition, SpriteFrames]:
return await media_loader.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, broadcaster_id: String = "") -> Dictionary:
if broadcaster_id == "": broadcaster_id = _current_user.id
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(broadcaster_id, title, body_choices, duration)
if channel_points_voting_enabled:
poll_body.channel_points_per_vote = channel_points_per_vote
poll_body.channel_points_voting_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 * 1000 + POLL_TIMEOUT_MS
var event: TwitchEventsub.Event
if eventsub && eventsub.has_subscription(TwitchEventsubDefinition.CHANNEL_POLL_END, {&"broadcaster_user_id": broadcaster_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 ot it is not listenting to ending polls")
return {}
return event.data
#endregion
#region Cheermotes
## Returns the data of the Cheermotes.
func get_cheermote_data() -> Array[TwitchCheermote]:
if media_loader == null:
_log.e("TwitchMediaLoader was not set within %s" % get_tree_string())
return []
await media_loader.preload_cheemote()
return media_loader.all_cheermotes()
## Returns all cheertiers in form of:
## Key: TwitchCheermote.Tiers | Value: SpriteFrames
func get_cheermotes(definition: TwitchCheermoteDefinition) -> Dictionary:
return await media_loader.get_cheermotes(definition)
#endregion