392 lines
13 KiB
GDScript
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
|