Initial Commit
This commit is contained in:
commit
48a5e71e00
1136 changed files with 64347 additions and 0 deletions
7
lib/app_context.gd
Normal file
7
lib/app_context.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Context
|
||||
class_name OverlayContext
|
||||
|
||||
var chatters: DbSet
|
||||
|
||||
func _init() -> void:
|
||||
chatters = DbSet.new(Chatter)
|
||||
1
lib/app_context.gd.uid
Normal file
1
lib/app_context.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d2vf38mh2l435
|
||||
53
lib/chat_manager.gd
Normal file
53
lib/chat_manager.gd
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
@tool
|
||||
extends Node
|
||||
|
||||
signal first_seen_chat(user: TwitchUser, chatter: Chatter, msg: TwitchChatMessage)
|
||||
signal first_chat(user: TwitchUser, chatter: Chatter, msg: TwitchChatMessage)
|
||||
signal chat_message(user: TwitchUser, msg: TwitchChatMessage)
|
||||
|
||||
var chatters: Dictionary[TwitchUser, Chatter] = {}
|
||||
var _log: TwitchLogger = TwitchLogger.new("ChatManager")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if Engine.is_editor_hint(): return
|
||||
while not Globals.twitcher:
|
||||
await get_tree().process_frame
|
||||
|
||||
Globals.twitcher.chat.message_received.connect(_handle_message)
|
||||
|
||||
|
||||
func _handle_message(message: TwitchChatMessage) -> void:
|
||||
var id := message.chatter_user_id
|
||||
var user: TwitchUser
|
||||
var chatter: Chatter
|
||||
var first_msg: bool = false
|
||||
var first_seen: bool = false
|
||||
# Do we have the chatter this session?
|
||||
if chatters.keys().any(func(x: TwitchUser): return x.id == id):
|
||||
var res := chatters.keys().filter(func(x: TwitchUser): return x.id == id)
|
||||
user = res[0]
|
||||
chatter = chatters[user]
|
||||
else:
|
||||
user = await Globals.twitcher.get_user_by_id(id)
|
||||
if not user:
|
||||
_log.e("Failed to fetch user by id: %s" % [id])
|
||||
return
|
||||
# Do we have this chatter in our database?
|
||||
chatter = Globals.context.chatters.find_one(Condition.new().equal("twitch_id", id))
|
||||
if not chatter:
|
||||
chatter = Chatter.new()
|
||||
chatter.twitch_id = id
|
||||
chatter.first_seen = Time.get_unix_time_from_system()
|
||||
chatter.last_seen = chatter.first_seen
|
||||
Globals.context.chatters.append(chatter)
|
||||
first_seen = true
|
||||
else:
|
||||
chatter.last_seen = Time.get_unix_time_from_system()
|
||||
chatter.save()
|
||||
first_msg = true
|
||||
chatters[user] = chatter
|
||||
|
||||
if first_seen: first_seen_chat.emit(user, chatter, message)
|
||||
if first_msg: first_chat.emit(user, chatter, message)
|
||||
chat_message.emit(user, chatter, message)
|
||||
1
lib/chat_manager.gd.uid
Normal file
1
lib/chat_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ct1s5eymb8mns
|
||||
54
lib/debugger_window.gd
Normal file
54
lib/debugger_window.gd
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
extends PanelContainer
|
||||
class_name DebuggerWindow
|
||||
|
||||
static var instance: DebuggerWindow
|
||||
@onready var debug_list: VBoxContainer = %DebugList
|
||||
|
||||
var _debuggers: Dictionary[String, Debugger] = {}
|
||||
|
||||
func _ready() -> void:
|
||||
if not instance:
|
||||
instance = self
|
||||
|
||||
func add_debugger(label: String, proc: Callable, async: bool = false) -> void:
|
||||
var dbgr := Debugger.new(label, proc, async)
|
||||
debug_list.add_child(dbgr)
|
||||
_debuggers[name] = dbgr
|
||||
|
||||
func remove_debugger(label: String) -> void:
|
||||
if not _debuggers.has(label): return
|
||||
_debuggers[label].queue_free()
|
||||
_debuggers.erase(label)
|
||||
|
||||
func enable_debugger(label: String) -> void:
|
||||
if not _debuggers.has(label): return
|
||||
_debuggers[label].enabled = true
|
||||
|
||||
func disable_debugger(label: String) -> void:
|
||||
if not _debuggers.has(label): return
|
||||
_debuggers[label].enabled = false
|
||||
|
||||
class Debugger:
|
||||
extends HBoxContainer
|
||||
var _label: Label
|
||||
var _field: Label
|
||||
var _proc: Callable
|
||||
var _async: bool
|
||||
var enabled: bool = false
|
||||
|
||||
func _init(label: String, proc: Callable, async: bool = false) -> void:
|
||||
_async = async
|
||||
_proc = proc
|
||||
_label = Label.new()
|
||||
_field = Label.new()
|
||||
_label.text = label
|
||||
add_child(_label)
|
||||
add_child(_field)
|
||||
_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if enabled:
|
||||
if _async:
|
||||
_field.text = str(await _proc.call())
|
||||
else:
|
||||
_field.text = str(_proc.call())
|
||||
1
lib/debugger_window.gd.uid
Normal file
1
lib/debugger_window.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dx87h5yv6t5tk
|
||||
25
lib/debugger_window.tscn
Normal file
25
lib/debugger_window.tscn
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[gd_scene format=3 uid="uid://cvbrjmhk4f146"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dx87h5yv6t5tk" path="res://lib/debugger_window.gd" id="1_80gsw"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_rkxbk"]
|
||||
|
||||
[node name="DebuggerWindow" type="PanelContainer" unique_id=2039594777]
|
||||
custom_minimum_size = Vector2(300, 200)
|
||||
script = ExtResource("1_80gsw")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=79592823]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="VBoxContainer" unique_id=1980761690]
|
||||
layout_mode = 2
|
||||
text = "DEBUGGER WINDOW"
|
||||
label_settings = SubResource("LabelSettings_rkxbk")
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer" unique_id=1571656376]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="DebugList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer" unique_id=1758953873]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
23
lib/globals.gd
Normal file
23
lib/globals.gd
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
extends Node
|
||||
|
||||
var twitcher: TwitcherExtended
|
||||
var context: OverlayContext
|
||||
var settings: OverlaySettings
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
context = OverlayContext.new()
|
||||
context.setup()
|
||||
|
||||
context.open_db("user://overlay.db")
|
||||
context.ensure_tables()
|
||||
if FileAccess.file_exists("user://settings.tres"):
|
||||
settings = load("user://settings.tres")
|
||||
else:
|
||||
settings = OverlaySettings.new()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
save_settings()
|
||||
|
||||
func save_settings() -> void:
|
||||
ResourceSaver.save(settings, "user://settings.tres")
|
||||
1
lib/globals.gd.uid
Normal file
1
lib/globals.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cuap0k5jagdtj
|
||||
34
lib/models/chatter.gd
Normal file
34
lib/models/chatter.gd
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
extends SQLiteObject
|
||||
class_name Chatter
|
||||
|
||||
enum ChatterLevel
|
||||
{
|
||||
NEW,
|
||||
NORMAL,
|
||||
REGULAR,
|
||||
DEVOTEE,
|
||||
VIP,
|
||||
MOD,
|
||||
STREAMER
|
||||
}
|
||||
|
||||
@export var id: int
|
||||
@export var twitch_id: String
|
||||
@export var nickname: String = ""
|
||||
@export var known_engine: String = ""
|
||||
@export var game_lists: Dictionary[String, String] = {}
|
||||
@export var is_indie_game_dev: bool = false
|
||||
@export var is_on_team: bool = false
|
||||
@export var level: ChatterLevel = ChatterLevel.NEW
|
||||
@export var auto_shoutout: bool = false
|
||||
@export var shoutout_as_devteam: bool = false
|
||||
@export var notes: String = ""
|
||||
@export var scores: Dictionary[String, int] = {}
|
||||
@export var extra_data: Dictionary[String, Variant] = {}
|
||||
@export var first_seen: float = 0.0
|
||||
@export var last_seen: float = 0.0
|
||||
|
||||
static func _setup() -> void:
|
||||
set_table_name(Chatter, "chatters")
|
||||
set_column_flags(Chatter, "id", Flags.PRIMARY_KEY|Flags.AUTO_INCREMENT|Flags.NOT_NULL)
|
||||
set_column_flags(Chatter, "twitch_id", Flags.NOT_NULL)
|
||||
1
lib/models/chatter.gd.uid
Normal file
1
lib/models/chatter.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://vfrg17drxmif
|
||||
4
lib/overlay_settings.gd
Normal file
4
lib/overlay_settings.gd
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
extends Resource
|
||||
class_name OverlaySettings
|
||||
|
||||
@export var auto_connect: bool = false
|
||||
1
lib/overlay_settings.gd.uid
Normal file
1
lib/overlay_settings.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dj0xfuvwdqryy
|
||||
16
lib/state_machine/state.gd
Normal file
16
lib/state_machine/state.gd
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
extends Node
|
||||
class_name State
|
||||
|
||||
signal transitioned(state: State, new_state_name: String)
|
||||
|
||||
func _enter() -> void:
|
||||
pass
|
||||
|
||||
func _exit() -> void:
|
||||
pass
|
||||
|
||||
func _update(_delta: float) -> void:
|
||||
pass
|
||||
|
||||
func _physics_update(_delta: float) -> void:
|
||||
pass
|
||||
1
lib/state_machine/state.gd.uid
Normal file
1
lib/state_machine/state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cb1uc3nlv2mgj
|
||||
44
lib/state_machine/state_machine.gd
Normal file
44
lib/state_machine/state_machine.gd
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
extends Node
|
||||
class_name StateMachine
|
||||
|
||||
@export var initial_state: State
|
||||
|
||||
signal state_changed(new_state: State)
|
||||
|
||||
var current_state: State
|
||||
var states: Dictionary[String, State] = {}
|
||||
|
||||
func _ready() -> void:
|
||||
for child in get_children():
|
||||
if child is State:
|
||||
states[child.name.to_lower()] = child
|
||||
child.transitioned.connect(_handle_child_transition)
|
||||
|
||||
if initial_state:
|
||||
initial_state._enter()
|
||||
current_state = initial_state
|
||||
|
||||
func _process(delta) -> void:
|
||||
if current_state:
|
||||
current_state._update(delta)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if current_state:
|
||||
current_state._physics_update(delta)
|
||||
|
||||
func _handle_child_transition(state: State, new_state_name: String) -> void:
|
||||
if state != current_state:
|
||||
return
|
||||
|
||||
var new_state: State = states.get(new_state_name.to_lower(), null)
|
||||
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
if current_state:
|
||||
current_state._exit()
|
||||
|
||||
new_state._enter()
|
||||
|
||||
current_state = new_state
|
||||
state_changed.emit(new_state)
|
||||
1
lib/state_machine/state_machine.gd.uid
Normal file
1
lib/state_machine/state_machine.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c16ty2yx4qkvu
|
||||
BIN
lib/twitcher_extended.zip
Normal file
BIN
lib/twitcher_extended.zip
Normal file
Binary file not shown.
117
lib/twitcher_extended/chatbot_authorization.gd
Normal file
117
lib/twitcher_extended/chatbot_authorization.gd
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
@tool
|
||||
extends Twitcher
|
||||
class_name ChatbotAuthorization
|
||||
|
||||
@export var token: OAuthToken:
|
||||
set(value):
|
||||
token = value
|
||||
if auth:
|
||||
auth.token = token
|
||||
auth.token_handler.token = token
|
||||
@export var oauth_setting: OAuthSetting
|
||||
@export var scopes: OAuthScopes = preload("res://addons/twitcher/chat/twitch_bot_scopes.tres")
|
||||
|
||||
var api: TwitchAPI
|
||||
var auth: TwitchAuth
|
||||
|
||||
@export_tool_button("Reset Token") var _reset_token := func() -> void:
|
||||
token.remove_tokens()
|
||||
|
||||
var _config_file: ConfigFile = ConfigFile.new()
|
||||
|
||||
signal _waiting_for_authentication
|
||||
|
||||
func _init() -> void:
|
||||
child_entered_tree.connect(_handle_child_entered)
|
||||
child_exiting_tree.connect(_handle_child_exiting)
|
||||
|
||||
func _handle_child_entered(child: Node) -> void:
|
||||
if child is TwitchAuth: auth = child
|
||||
if child is TwitchAPI: api = child
|
||||
|
||||
func _handle_child_exiting(child: Node) -> void:
|
||||
if child is TwitchAuth: auth = null
|
||||
if child is TwitchAPI: api = null
|
||||
|
||||
func _ensure_nodes() -> void:
|
||||
if (api == null):
|
||||
api = TwitchAPI.new()
|
||||
api.name = "ChatbotAPI"
|
||||
add_child(api)
|
||||
api.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner
|
||||
|
||||
if (auth == null):
|
||||
auth = TwitchAuth.new()
|
||||
auth.name = "ChatbotAuth"
|
||||
add_child(auth)
|
||||
auth.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else owner
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
_ensure_nodes()
|
||||
|
||||
api.token = token
|
||||
api.oauth_setting = oauth_setting
|
||||
auth.token = token
|
||||
auth.oauth_setting = oauth_setting
|
||||
auth.token_handler.token = token
|
||||
auth.scopes = scopes
|
||||
auth.token_handler.token_resolved.connect(_waiting_for_authentication.emit)
|
||||
auth.token_handler.unauthenticated.connect(_waiting_for_authentication.emit)
|
||||
|
||||
func authenticate() -> bool:
|
||||
auth.token._load_tokens()
|
||||
if auth.token.is_token_valid():
|
||||
return true
|
||||
|
||||
if auth.token.has_refresh_token():
|
||||
auth.force_verify = false
|
||||
auth.refresh_token()
|
||||
await _waiting_for_authentication
|
||||
if auth.token.is_token_valid():
|
||||
return true
|
||||
else:
|
||||
return false
|
||||
|
||||
auth.force_verify = true
|
||||
|
||||
return await auth.authorize()
|
||||
|
||||
func _load_from_cache() -> TwitchUser:
|
||||
var user: TwitchUser = TwitchUser.new()
|
||||
if (_config_file.load(token._cache_path) == OK
|
||||
and _config_file.has_section(token._identifier)
|
||||
and _config_file.has_section_key(token._identifier, "bot_id")):
|
||||
|
||||
user.id = _config_file.get_value(token._identifier, "bot_id", "")
|
||||
user.display_name = _config_file.get_value(token._identifier, "bot_display", "")
|
||||
user.login = _config_file.get_value(token._identifier, "bot_login", "")
|
||||
return user
|
||||
return null
|
||||
|
||||
func _save_to_cache(user: TwitchUser) -> void:
|
||||
if _config_file.load(token._cache_path) == OK and _config_file.has_section(token._identifier):
|
||||
_config_file.set_value(token._identifier, "bot_id", user.id)
|
||||
_config_file.set_value(token._identifier, "bot_display", user.display_name)
|
||||
_config_file.set_value(token._identifier, "bot_login", user.login)
|
||||
_config_file.save(token._cache_path)
|
||||
|
||||
func get_user() -> TwitchUser:
|
||||
var user: TwitchUser = _load_from_cache()
|
||||
if not user:
|
||||
if not auth.token.is_token_valid():
|
||||
push_error("Please authenticate first, before calling get_user()")
|
||||
return null
|
||||
var res: TwitchGetUsers.Response = await api.get_users(null)
|
||||
if res.data.size() > 0:
|
||||
user = res.data[0]
|
||||
_save_to_cache(user)
|
||||
else:
|
||||
push_error("Failed to fetch token user information")
|
||||
return null
|
||||
|
||||
return user
|
||||
|
||||
func invalidate_token() -> void:
|
||||
auth.token_handler.revoke_token()
|
||||
auth.token.invalidate()
|
||||
1
lib/twitcher_extended/chatbot_authorization.gd.uid
Normal file
1
lib/twitcher_extended/chatbot_authorization.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://csq2hmf1bsgku
|
||||
473
lib/twitcher_extended/twitcher_extended.gd
Normal file
473
lib/twitcher_extended/twitcher_extended.gd
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
@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
|
||||
1
lib/twitcher_extended/twitcher_extended.gd.uid
Normal file
1
lib/twitcher_extended/twitcher_extended.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d003jb645nrji
|
||||
Loading…
Add table
Add a link
Reference in a new issue