285 lines
10 KiB
GDScript
285 lines
10 KiB
GDScript
@tool
|
|
class_name VSTChat extends Node
|
|
|
|
# TODO: rename to past simple?
|
|
signal Connected(_channel)
|
|
signal OnMessage(chatter: VSTChatter)
|
|
|
|
var _channel: VSTChannel
|
|
|
|
var _chatClient: WebSocketPeer
|
|
var _hasConnected:= false
|
|
|
|
enum RequestType {
|
|
EMOTE,
|
|
BADGE,
|
|
BADGE_MAPPING
|
|
}
|
|
|
|
var caches := {
|
|
RequestType.EMOTE: {},
|
|
RequestType.BADGE: {},
|
|
RequestType.BADGE_MAPPING: {}
|
|
}
|
|
|
|
var _client_id: String
|
|
var _twitch_chat_url: String
|
|
var _twitch_chat_port: int
|
|
|
|
var _use_cache: bool
|
|
var _cache_path: String
|
|
|
|
var _use_anon_connection:= false
|
|
|
|
var _chat_queue : Array[String] = []
|
|
var _last_msg : int = Time.get_ticks_msec()
|
|
var _chat_timeout_ms: int
|
|
|
|
const USER_AGENT : String = "User-Agent: VSTC/0.1.0 (Godot Engine)"
|
|
|
|
func _process(_delta: float):
|
|
if !_chatClient:
|
|
return
|
|
|
|
_chatClient.poll()
|
|
var state = _chatClient.get_ready_state()
|
|
match state:
|
|
WebSocketPeer.STATE_OPEN:
|
|
if (!_hasConnected):
|
|
onChatConnected()
|
|
while _chatClient.get_available_packet_count():
|
|
onReceivedData(_chatClient.get_packet())
|
|
if !_chat_queue.is_empty() and _last_msg + (_last_msg + _chat_timeout_ms) <= Time.get_ticks_msec():
|
|
_chatClient.send_text(_chat_queue.pop_front())
|
|
_last_msg = Time.get_ticks_msec()
|
|
WebSocketPeer.STATE_CLOSED:
|
|
if _hasConnected:
|
|
_hasConnected = false
|
|
var code = _chatClient.get_close_code()
|
|
var reason = _chatClient.get_close_reason()
|
|
print('Disconnected from twitch chat')
|
|
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
|
|
print("Reconnecting")
|
|
start_chat_client()
|
|
|
|
func start_chat_client():
|
|
get_settings()
|
|
if _chatClient:
|
|
_chatClient.close()
|
|
_chatClient = WebSocketPeer.new()
|
|
_chatClient.connect_to_url("%s:%d" % [_twitch_chat_url, _twitch_chat_port])
|
|
|
|
|
|
func login_anon(channel_name: String):
|
|
_channel = VSTChannel.new()
|
|
_channel.login = channel_name.to_lower()
|
|
_use_anon_connection = true
|
|
start_chat_client()
|
|
|
|
|
|
func login(twitch_channel: VSTChannel):
|
|
_channel = twitch_channel
|
|
start_chat_client()
|
|
|
|
|
|
func onChatConnected():
|
|
if !_channel:
|
|
return
|
|
_hasConnected = true
|
|
|
|
_chatClient.send_text("CAP REQ :twitch.tv/tags twitch.tv/commands")
|
|
|
|
if _use_anon_connection:
|
|
_chatClient.send_text('PASS ' + VSTSettings.get_setting(VSTSettings.settings.twitch_anon_pass))
|
|
_chatClient.send_text('NICK ' + VSTSettings.get_setting(VSTSettings.settings.twitch_anon_user))
|
|
else:
|
|
_chatClient.send_text('PASS oauth:' + _channel.token)
|
|
_chatClient.send_text('NICK ' + _channel.login.to_lower())
|
|
pass
|
|
|
|
_chatClient.send_text('JOIN ' + '#' + _channel.login.to_lower())
|
|
Connected.emit()
|
|
|
|
func send_message(message: String):
|
|
_chat_queue.append("PRIVMSG #" + _channel.login.to_lower() + " :" + message + "\r\n")
|
|
|
|
func onReceivedData(payload: PackedByteArray):
|
|
var message = payload.get_string_from_utf8()
|
|
var splittled_messages = message.split("\n")
|
|
for n in splittled_messages:
|
|
handle_message(n)
|
|
|
|
#TODO: move this to parse helper?
|
|
func parse_message_from_twtich_IRC(message: String) -> PackedStringArray:
|
|
return message.split(" ", true, 4) # We might need more than 3
|
|
|
|
func handle_message(message: String):
|
|
if message.begins_with("PING"):
|
|
_chatClient.send_text(message.replace("PING", "PONG"))
|
|
return
|
|
|
|
var parsed_message: PackedStringArray = parse_message_from_twtich_IRC(message)
|
|
|
|
if parsed_message.size() < 2: return
|
|
|
|
match parsed_message[2]:
|
|
"NOTICE":
|
|
var info : String = parsed_message[3].right(-1)
|
|
if (info == "Login authentication failed" || info == "Login unsuccessful"):
|
|
print_debug("Authentication failed.")
|
|
#login_attempt.emit(false)
|
|
elif (info == "You don't have permission to perform that action"):
|
|
print_debug("No permission. Check if access token is still valid. Aborting.")
|
|
#user_token_invalid.emit()
|
|
set_process(false)
|
|
else:
|
|
pass
|
|
#unhandled_message.emit(message, tags)
|
|
"001":
|
|
print_debug("Authentication successful.")
|
|
_chatClient.send_text('ROOMSTATE '+ '#' + _channel.login.to_lower())
|
|
#login_attempt.emit(true)
|
|
"PRIVMSG":
|
|
handle_privmsg(parsed_message)
|
|
#handle_command(sender_data, msg[3].split(" ", true, 1))
|
|
#chat_message.emit(sender_data, msg[3].right(-1))
|
|
"ROOMSTATE":
|
|
if _use_anon_connection:
|
|
var parsed_tags:VSTIRCTags = VSTParseHelper.parse_tags(parsed_message[0])
|
|
_channel.id = parsed_tags.user_id
|
|
|
|
func parse_message_to_chatter(message: PackedStringArray) -> VSTChatter:
|
|
var chatter = VSTChatter.new()
|
|
chatter.login = VSTParseHelper.parse_login(message[1])
|
|
chatter.channel = VSTParseHelper.parse_channel(message[3])
|
|
chatter.message = VSTParseHelper.parse_message(message[4])
|
|
chatter.tags = VSTParseHelper.parse_tags(message[0])
|
|
chatter.date_time_dict = Time.get_datetime_dict_from_system()
|
|
|
|
if chatter.tags.color_hex.is_empty():
|
|
chatter.tags.color_hex = VSTUtils.get_random_name_color(chatter.login)
|
|
return chatter
|
|
|
|
|
|
func handle_privmsg(msg: PackedStringArray):
|
|
var chatter = parse_message_to_chatter(msg)
|
|
OnMessage.emit(chatter)
|
|
|
|
|
|
func get_emote(emote_id: String, scale: String = "1.0") -> Texture2D:
|
|
var texture: Texture2D
|
|
var cachename: String = emote_id + "_" + scale
|
|
var filename: String = _cache_path + "/" + RequestType.keys()[RequestType.EMOTE] + "/" + cachename + ".png"
|
|
|
|
if !caches[RequestType.EMOTE].has(cachename):
|
|
if _use_cache && FileAccess.file_exists(filename):
|
|
var img: Image = Image.new()
|
|
img.load_png_from_buffer(FileAccess.get_file_as_bytes(filename))
|
|
texture = ImageTexture.create_from_image(img)
|
|
texture.take_over_path(filename)
|
|
else:
|
|
var request: HTTPRequest = HTTPRequest.new()
|
|
add_child(request)
|
|
request.request("https://static-cdn.jtvnw.net/emoticons/v1/" + emote_id + "/" + scale, [USER_AGENT,"Accept: */*"])
|
|
var data = await(request.request_completed)
|
|
var img: Image = Image.new()
|
|
img.load_png_from_buffer(data[3])
|
|
texture = ImageTexture.create_from_image(img)
|
|
if _use_cache:
|
|
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
|
|
texture.get_image().save_png(filename)
|
|
request.queue_free()
|
|
texture.take_over_path(filename)
|
|
caches[RequestType.EMOTE][cachename] = texture
|
|
return caches[RequestType.EMOTE][cachename]
|
|
|
|
func get_badge(badge_name: String, badge_level: String, channel_id: String = "_global", scale: String = "1") -> Texture2D:
|
|
if _use_anon_connection: return
|
|
|
|
var texture: Texture2D
|
|
var cachename = badge_name + "_" + badge_level + "_" + scale
|
|
var filename: String = _cache_path + "/" + RequestType.keys()[RequestType.BADGE] + "/" + channel_id + "/" + cachename + ".png"
|
|
if !caches[RequestType.BADGE].has(channel_id):
|
|
caches[RequestType.BADGE][channel_id] = {}
|
|
if !caches[RequestType.BADGE][channel_id].has(cachename):
|
|
if _use_cache && FileAccess.file_exists(filename):
|
|
var img : Image = Image.new()
|
|
img.load_png_from_buffer(FileAccess.get_file_as_bytes(filename))
|
|
texture = ImageTexture.create_from_image(img)
|
|
texture.take_over_path(filename)
|
|
else:
|
|
var map: Dictionary = caches[RequestType.BADGE_MAPPING].get(channel_id, await(get_badge_mapping(channel_id)))
|
|
if !map.is_empty():
|
|
if map.has(badge_name):
|
|
var request: HTTPRequest = HTTPRequest.new()
|
|
add_child(request)
|
|
request.request(map[badge_name]["versions"][badge_level]["image_url_" + scale + "x"], [USER_AGENT,"Accept: */*"])
|
|
var data = await(request.request_completed)
|
|
var img: Image = Image.new()
|
|
img.load_png_from_buffer(data[3])
|
|
texture = ImageTexture.create_from_image(img)
|
|
texture.take_over_path(filename)
|
|
request.queue_free()
|
|
elif channel_id != "_global":
|
|
return await(get_badge(badge_name, badge_level, "_global", scale))
|
|
elif channel_id != "_global":
|
|
return await(get_badge(badge_name, badge_level, "_global", scale))
|
|
if _use_cache:
|
|
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
|
|
texture.get_image().save_png(filename)
|
|
texture.take_over_path(filename)
|
|
caches[RequestType.BADGE][channel_id][cachename] = texture
|
|
return caches[RequestType.BADGE][channel_id][cachename]
|
|
|
|
func get_badge_mapping(channel_id: String = "_global") -> Dictionary:
|
|
|
|
if _use_anon_connection: return {}
|
|
|
|
if caches[RequestType.BADGE_MAPPING].has(channel_id):
|
|
return caches[RequestType.BADGE_MAPPING][channel_id]
|
|
|
|
var filename: String = _cache_path + "/" + RequestType.keys()[RequestType.BADGE_MAPPING] + "/" + channel_id + ".json"
|
|
if _use_cache && FileAccess.file_exists(filename):
|
|
var cache = JSON.parse_string(FileAccess.get_file_as_string(filename))
|
|
if "badge_sets" in cache:
|
|
return cache["badge_sets"]
|
|
|
|
var request : HTTPRequest = HTTPRequest.new()
|
|
add_child(request)
|
|
|
|
request.request("https://api.twitch.tv/helix/chat/badges" + ("/global" if channel_id == "_global" else "?broadcaster_id=" + _channel.id), [USER_AGENT, "Authorization: Bearer " + _channel.token, "Client-Id:" + _client_id, "Content-Type: application/json"], HTTPClient.METHOD_GET)
|
|
|
|
var reply : Array = await(request.request_completed)
|
|
var response : Dictionary = JSON.parse_string(reply[3].get_string_from_utf8())
|
|
var mappings : Dictionary = {}
|
|
for entry in response["data"]:
|
|
if (!mappings.has(entry["set_id"])):
|
|
mappings[entry["set_id"]] = {"versions": {}}
|
|
for version in entry["versions"]:
|
|
mappings[entry["set_id"]]["versions"][version["id"]] = version
|
|
request.queue_free()
|
|
if (reply[1] == HTTPClient.RESPONSE_OK):
|
|
caches[RequestType.BADGE_MAPPING][channel_id] = mappings
|
|
if _use_cache:
|
|
DirAccess.make_dir_recursive_absolute(filename.get_base_dir())
|
|
var file : FileAccess = FileAccess.open(filename, FileAccess.WRITE)
|
|
file.store_string(JSON.stringify(mappings))
|
|
else:
|
|
print("Could not retrieve badge mapping for channel_id " + channel_id + ".")
|
|
return {}
|
|
return caches[RequestType.BADGE_MAPPING][channel_id]
|
|
|
|
func get_settings():
|
|
_client_id = VSTSettings.get_setting(VSTSettings.settings.client_id)
|
|
_twitch_chat_url = VSTSettings.get_setting(VSTSettings.settings.twitch_chat_url)
|
|
_twitch_chat_port = VSTSettings.get_setting(VSTSettings.settings.twitch_port)
|
|
_use_cache = VSTSettings.get_setting(VSTSettings.settings.disk_cache)
|
|
_cache_path = VSTSettings.get_setting(VSTSettings.settings.disk_cache_path)
|
|
_chat_timeout_ms = VSTSettings.get_setting(VSTSettings.settings.twitch_timeout_ms)
|
|
|
|
# stops chat socket from tts server
|
|
func disconnect_api():
|
|
if _chatClient:
|
|
_chatClient.close()
|
|
|
|
_hasConnected = false
|