2025-10-29 22:23:34 +11:00
|
|
|
class_name DiscordBot
|
|
|
|
|
extends Node
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
Main script for discord.gd plugin
|
|
|
|
|
Copyright 2021-present, Delano Lourenco
|
|
|
|
|
For Copyright and License: See LICENSE.md
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Variables
|
|
|
|
|
|
|
|
|
|
var TOKEN: String
|
2025-11-03 01:38:28 +11:00
|
|
|
var VERBOSE: bool = true
|
2025-10-29 22:23:34 +11:00
|
|
|
var INTENTS: int = 513
|
|
|
|
|
|
|
|
|
|
# Caches
|
|
|
|
|
var user: User
|
|
|
|
|
var application: Dictionary
|
|
|
|
|
var guilds = {}
|
|
|
|
|
var channels = {}
|
|
|
|
|
var users = {}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Signals
|
|
|
|
|
|
|
|
|
|
signal bot_ready(bot) # bot: DiscordBot
|
|
|
|
|
signal guild_create(bot, guild) # bot: DiscordBot, guild: Dictionary
|
|
|
|
|
signal guild_update(bot, guild) # bot: DiscordBot, guild: Dictionary
|
|
|
|
|
signal guild_delete(bot, guild) # bot: DiscordBot, guild: Dictionary
|
|
|
|
|
signal message_create(bot, message, channel) # bot: DiscordBot, message: Message, channel: Dictionary
|
|
|
|
|
signal message_delete(bot, message) # bot: DiscordBot, message: Dictionary
|
|
|
|
|
signal interaction_create(bot, interaction) # bot: DiscordBot, interaction: DiscordInteraction
|
|
|
|
|
signal message_reaction_add(bot, data) # bot: DiscordBot, data: Dictionary
|
|
|
|
|
signal message_reaction_remove(bot, data) # bot: DiscordBot, data: Dictionary
|
|
|
|
|
signal message_reaction_remove_all(bot, data) # bot: DiscordBot, data: Dictionary
|
|
|
|
|
signal message_reaction_remove_emoji(bot, data) # bot: DiscordBot, data: Dictionary
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Private Variables
|
|
|
|
|
var _gateway_base = 'wss://gateway.discord.gg/?v=9&encoding=json'
|
|
|
|
|
var _https_domain = 'https://discord.com'
|
|
|
|
|
var _api_slug = '/api/v9'
|
|
|
|
|
var _https_base = _https_domain + _api_slug
|
|
|
|
|
var _cdn_base = 'https://cdn.discordapp.com'
|
|
|
|
|
var _headers: Array
|
|
|
|
|
var _client: WebSocketPeer
|
|
|
|
|
var _sess_id: String
|
|
|
|
|
var _last_seq: float
|
|
|
|
|
var _invalid_session_is_resumable: bool
|
|
|
|
|
var _heartbeat_interval: int
|
|
|
|
|
var _heartbeat_ack_received = true
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Count of the number of guilds initially loaded
|
|
|
|
|
var guilds_loaded = 0
|
|
|
|
|
|
|
|
|
|
const CHANNEL_TYPES = {
|
|
|
|
|
'0': 'GUILD_TEXT',
|
|
|
|
|
'1': 'DM',
|
|
|
|
|
'2': 'GUILD_VOICE',
|
|
|
|
|
'3': 'GROUP_DM',
|
|
|
|
|
'4': 'GUILD_CATEGORY',
|
|
|
|
|
'5': 'GUILD_NEWS',
|
|
|
|
|
'10': 'GUILD_NEWS_THREAD',
|
|
|
|
|
'11': 'GUILD_PUBLIC_THREAD',
|
|
|
|
|
'12': 'GUILD_PRIVATE_THREAD',
|
|
|
|
|
'13': 'GUILD_STAGE_VOICE',
|
|
|
|
|
'14': 'GUILD_DIRECTORY',
|
|
|
|
|
'15': 'GUILD_FORUM',
|
|
|
|
|
'16': 'GUILD_MEDIA',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var GUILD_ICON_SIZES = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
|
|
|
|
|
|
|
|
|
|
var ACTIVITY_TYPES = {'GAME': 0, 'STREAMING': 1, 'LISTENING': 2, 'WATCHING': 3, 'COMPETING': 5}
|
|
|
|
|
|
|
|
|
|
var PRESENCE_STATUS_TYPES = ['ONLINE', 'DND', 'IDLE', 'INVISIBLE', 'OFFLINE']
|
|
|
|
|
|
|
|
|
|
# Public Functions
|
|
|
|
|
func login() -> void:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print("Logging in...")
|
|
|
|
|
assert(TOKEN.length() > 10, 'ERROR: Unable to login. TOKEN attribute not set.')
|
|
|
|
|
_headers = [
|
|
|
|
|
'Authorization: Bot %s' % TOKEN,
|
|
|
|
|
'User-Agent: discord.gd (https://github.com/3ddelano/discord.gd)'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
var err = _client.connect_to_url(_gateway_base)
|
|
|
|
|
|
|
|
|
|
if err == ERR_INVALID_PARAMETER:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Login Error: Invalid ws url')
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if err == ERR_ALREADY_IN_USE:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Login Error: Already logged in or logging in')
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if err != OK:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Login Error: %s (%s)' % [error_string(err), err])
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region messages
|
|
|
|
|
|
|
|
|
|
func send(messageorchannelid, content, options: Dictionary = {}) -> Message:
|
|
|
|
|
# channel
|
|
|
|
|
var res = await _send_message_request(messageorchannelid, content, options)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func reply(message: Message, content, options: Dictionary = {}) -> Message:
|
|
|
|
|
options.message_reference = {'message_id': message.id}
|
|
|
|
|
var res = await _send_message_request(message, content, options)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func edit(message: Message, content, options: Dictionary = {}) -> Message:
|
|
|
|
|
var res = await _send_message_request(message, content, options, HTTPClient.METHOD_PATCH)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func delete(message: Message):
|
|
|
|
|
var res = await _send_message_request(message, '', {}, HTTPClient.METHOD_DELETE)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region threads
|
|
|
|
|
|
|
|
|
|
func start_thread(message: Message, thread_name: String, duration: int = 60 * 24) -> Dictionary:
|
|
|
|
|
var payload = {'name': thread_name, 'auto_archive_duration': duration}
|
|
|
|
|
var res = await _send_request(
|
|
|
|
|
'/channels/%s/messages/%s/threads' % [message.channel_id, message.id], payload
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region channels
|
|
|
|
|
|
|
|
|
|
# See https://discord.com/developers/docs/resources/guild#create-guild-channel
|
|
|
|
|
# All parameters are optional and nullable excluding data.name
|
|
|
|
|
func create_channel(guild_id: String, data: Dictionary) -> Dictionary:
|
|
|
|
|
var res = await _send_request('/guilds/%s/channels' % guild_id, data)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# See https://discord.com/developers/docs/resources/channel#get-channel
|
|
|
|
|
func get_channel(channel_id: String) -> Dictionary:
|
|
|
|
|
var res = await _send_get('/channels/%s' % channel_id)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# See https://discord.com/developers/docs/resources/channel#modify-channel
|
|
|
|
|
func update_channel(channel_id: String, data: Dictionary) -> Dictionary:
|
|
|
|
|
var res = await _send_request('/channels/%s' % channel_id, data, HTTPClient.METHOD_PATCH)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# See https://discord.com/developers/docs/resources/channel#deleteclose-channel
|
|
|
|
|
func delete_channel(channel_id: String):
|
|
|
|
|
var res = await _send_request('/channels/%s' % channel_id, {}, HTTPClient.METHOD_DELETE)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# See https://discord.com/developers/docs/resources/message#get-channel-messages
|
|
|
|
|
# The before, after, and around parameters are mutually exclusive, only one may be passed at a time.
|
|
|
|
|
func get_channel_messages(channel_id: String, limit := 50, filter_type := "", filter_value := ""):
|
|
|
|
|
if filter_type != "":
|
|
|
|
|
assert(filter_type in ["before", "after", "around"], "Invalid filter type: %s, Expected: before, after, around" % filter_type)
|
|
|
|
|
assert(filter_value != "", "Filter value must not be empty")
|
|
|
|
|
return await _send_get('/channels/%s/messages?limit=%s&%s=%s' % [channel_id, limit, filter_type, filter_value])
|
|
|
|
|
return await _send_get('/channels/%s/messages?limit=%s' % [channel_id, limit])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# See https://discord.com/developers/docs/resources/message#get-channel-message
|
|
|
|
|
func get_channel_message(channel_id: String, message_id: String) -> Message:
|
|
|
|
|
var res = await _send_get('/channels/%s/messages/%s' % [channel_id, message_id])
|
|
|
|
|
await _parse_message(res)
|
|
|
|
|
return Message.new(res)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func create_dm_channel(user_id: String) -> Dictionary:
|
|
|
|
|
var res = await _send_request('/users/@me/channels', {'recipient_id': user_id})
|
|
|
|
|
if typeof(res) == TYPE_DICTIONARY:
|
|
|
|
|
_clean_channel(res)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func permissions_in(channel_id: String):
|
|
|
|
|
# Permissions for the bot in a channel
|
|
|
|
|
return permissions_for(user.id, channel_id)
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region guilds
|
|
|
|
|
|
|
|
|
|
func get_guild_icon(guild_id: String, size: int = 256) -> PackedByteArray:
|
|
|
|
|
assert(Helpers.is_valid_str(guild_id), 'Invalid Type: guild_id must be a valid String')
|
|
|
|
|
|
|
|
|
|
var guild = guilds.get(str(guild_id))
|
|
|
|
|
|
|
|
|
|
if not guild:
|
|
|
|
|
push_error('Guild not found.')
|
|
|
|
|
return PackedByteArray()
|
|
|
|
|
|
|
|
|
|
if not guild.icon:
|
|
|
|
|
push_error('Guild has no icon set.')
|
|
|
|
|
return PackedByteArray()
|
|
|
|
|
|
|
|
|
|
if size != 256:
|
|
|
|
|
assert(size in GUILD_ICON_SIZES, 'Invalid size for guild icon provided')
|
|
|
|
|
|
|
|
|
|
var png_bytes = await _send_get_cdn('/icons/%s/%s.png?size=%s' % [guild.id, guild.icon, size])
|
|
|
|
|
return png_bytes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_guild_emojis(guild_id: String) -> Array:
|
|
|
|
|
var res = await _send_get('/guilds/%s/emojis' % guild_id)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_guild_member(guild_id: String, member_id: String) -> Dictionary:
|
|
|
|
|
var member = await _send_get('/guilds/%s/members/%s' % [guild_id, member_id])
|
|
|
|
|
return member
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region guild member
|
|
|
|
|
|
|
|
|
|
func remove_member_role(guild_id: String, user_id: String, role_id: String):
|
|
|
|
|
var res = await _send_get('/guilds/%s/members/%s/roles/%s' % [guild_id, user_id, role_id], HTTPClient.METHOD_DELETE)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func add_member_role(guild_id: String, user_id: String, role_id: String):
|
|
|
|
|
var res = await _send_request('/guilds/%s/members/%s/roles/%s' % [guild_id, user_id, role_id], {}, HTTPClient.METHOD_PUT)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ban_member(guild_id: String, user_id: String, opts = {delete_message_seconds = 0}):
|
|
|
|
|
var res = await _send_request('/guilds/%s/bans/%s' % [guild_id, user_id], opts, HTTPClient.METHOD_PUT)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unban_member(guild_id: String, user_id: String):
|
|
|
|
|
var res = await _send_request('/guilds/%s/bans/%s' % [guild_id, user_id], {}, HTTPClient.METHOD_DELETE)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func permissions_for(user_id: String, channel_id: String):
|
|
|
|
|
# Permissions for a user in a channel
|
|
|
|
|
if not channels.has(channel_id):
|
|
|
|
|
push_error('Channel with the id' + channel_id + ' not found.')
|
|
|
|
|
return Permissions.new(Permissions.new().ALL)
|
|
|
|
|
|
|
|
|
|
var channel = channels[channel_id]
|
|
|
|
|
var guild = guilds[channel.guild_id]
|
|
|
|
|
|
|
|
|
|
# Check for guild owner
|
|
|
|
|
if user_id == guild.owner_id:
|
|
|
|
|
return Permissions.new(Permissions.new().ALL)
|
|
|
|
|
|
|
|
|
|
# @everyone base role
|
|
|
|
|
var permissions = Permissions.new(guild.roles[guild.id].permissions)
|
|
|
|
|
if not guild.members.has(user_id):
|
|
|
|
|
push_warning('Member not found in cached members. Make sure the GUILD_MEMBERS intent is setup.')
|
|
|
|
|
return permissions
|
|
|
|
|
|
|
|
|
|
var role_ids = guild.members[user_id].roles
|
|
|
|
|
|
|
|
|
|
# Apply member global roles
|
|
|
|
|
for role_id in role_ids:
|
|
|
|
|
permissions.add(guild.roles[role_id].permissions)
|
|
|
|
|
|
|
|
|
|
if permissions.has('ADMINISTRATOR'):
|
|
|
|
|
return Permissions.new(Permissions.new().ALL)
|
|
|
|
|
|
|
|
|
|
var overwrites = channel.permission_overwrites
|
|
|
|
|
|
|
|
|
|
# Apply @everyone overwrite
|
|
|
|
|
for overwrite in overwrites:
|
|
|
|
|
if overwrite.id == guild.id:
|
|
|
|
|
permissions.remove(overwrite.deny)
|
|
|
|
|
permissions.add(overwrite.allow)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Apply member roles overwrite
|
|
|
|
|
for overwrite in overwrites:
|
|
|
|
|
if overwrite.id in role_ids:
|
|
|
|
|
permissions.remove(overwrite.deny)
|
|
|
|
|
for overwrite in overwrites:
|
|
|
|
|
if overwrite.id in role_ids:
|
|
|
|
|
permissions.add(overwrite.allow)
|
|
|
|
|
|
|
|
|
|
# Apply user overwrite
|
|
|
|
|
for overwrite in overwrites:
|
|
|
|
|
if overwrite.id == user_id:
|
|
|
|
|
permissions.remove(overwrite.deny)
|
|
|
|
|
permissions.add(overwrite.allow)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
return permissions
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region roles
|
|
|
|
|
|
|
|
|
|
func create_role(guild_id: String, p_opts: Dictionary):
|
|
|
|
|
var opts = {
|
|
|
|
|
name = "new role",
|
|
|
|
|
permissions = Permissions.DEFAULT, # Permissions object
|
|
|
|
|
color = 0,
|
|
|
|
|
hoist = false,
|
|
|
|
|
icon = null, # String
|
|
|
|
|
unicode_emoji = null, # String
|
|
|
|
|
mentionable = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key in p_opts:
|
|
|
|
|
opts[key] = p_opts[key]
|
|
|
|
|
|
|
|
|
|
if opts.permissions is Permissions:
|
|
|
|
|
opts.permissions = opts.permissions.value_of()
|
|
|
|
|
var res = await _send_request('/guilds/%s/roles' % [guild_id], opts, HTTPClient.METHOD_POST)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func update_role(guild_id: String, role_id: String, opts: Dictionary):
|
|
|
|
|
if opts.has("permissions") and opts.permissions is Permissions:
|
|
|
|
|
opts.permissions = opts.permissions.value_of()
|
|
|
|
|
|
|
|
|
|
var res = await _send_request('/guilds/%s/roles/%s' % [guild_id, role_id], opts, HTTPClient.METHOD_PATCH)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func delete_role(guild_id: String, role_id: String):
|
|
|
|
|
var res = await _send_request('/guilds/%s/roles/%s' % [guild_id, role_id], {}, HTTPClient.METHOD_DELETE)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region reactions
|
|
|
|
|
|
|
|
|
|
# ONLY custom emojis will work, pass in only the Id of the emoji to the custom_emoji
|
|
|
|
|
func create_reaction(messageordict, custom_emoji: String) -> int:
|
|
|
|
|
assert(Helpers.is_valid_str(custom_emoji), 'Invalid Type: custom_emoji must be a String')
|
|
|
|
|
custom_emoji = 'a:' + custom_emoji
|
|
|
|
|
assert(messageordict is Message or typeof(messageordict) == TYPE_DICTIONARY, 'Invalid type: Expected a Message or Dictionary')
|
|
|
|
|
|
|
|
|
|
if typeof(messageordict) == TYPE_DICTIONARY and messageordict.has('message_id'):
|
|
|
|
|
messageordict.id = messageordict.message_id
|
|
|
|
|
|
|
|
|
|
var status_code = await _send_get('/channels/%s/messages/%s/reactions/%s/@me' % [messageordict.channel_id, messageordict.id, custom_emoji], HTTPClient.METHOD_PUT, ['Content-Length:0'])
|
|
|
|
|
return status_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func delete_reaction(messageordict, custom_emoji: String, userid: String = '@me') -> int:
|
|
|
|
|
assert(Helpers.is_valid_str(custom_emoji), 'Invalid Type: custom_emoji must be a String')
|
|
|
|
|
custom_emoji = 'a:' + custom_emoji
|
|
|
|
|
assert(messageordict is Message or typeof(messageordict) == TYPE_DICTIONARY, 'Invalid type: Expected a Message or Dictionary')
|
|
|
|
|
|
|
|
|
|
if typeof(messageordict) == TYPE_DICTIONARY and messageordict.has('message_id'):
|
|
|
|
|
messageordict.id = messageordict.message_id
|
|
|
|
|
|
|
|
|
|
var status_code = await _send_get('/channels/%s/messages/%s/reactions/%s/%s' % [messageordict.channel_id, messageordict.id, custom_emoji, userid], HTTPClient.METHOD_DELETE, ['Content-Length:0'])
|
|
|
|
|
|
|
|
|
|
return status_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func delete_reactions(messageordict, custom_emoji = '') -> int:
|
|
|
|
|
assert(messageordict is Message or typeof(messageordict) == TYPE_DICTIONARY, 'Invalid type: Expected a Message or Dictionary')
|
|
|
|
|
if typeof(messageordict) == TYPE_DICTIONARY and messageordict.has('message_id'):
|
|
|
|
|
messageordict.id = messageordict.message_id
|
|
|
|
|
|
|
|
|
|
var status_code
|
|
|
|
|
if custom_emoji != '':
|
|
|
|
|
custom_emoji = 'a:' + custom_emoji
|
|
|
|
|
status_code = await _send_get('/channels/%s/messages/%s/reactions/%s' % [messageordict.channel_id, messageordict.id, custom_emoji], HTTPClient.METHOD_DELETE, ['Content-Length:0'])
|
|
|
|
|
else:
|
|
|
|
|
status_code = await _send_get('/channels/%s/messages/%s/reactions' % [messageordict.channel_id, messageordict.id], HTTPClient.METHOD_DELETE, ['Content-Length:0'])
|
|
|
|
|
|
|
|
|
|
return status_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_reactions(messageordict, custom_emoji: String):
|
|
|
|
|
assert(Helpers.is_valid_str(custom_emoji), 'Invalid Type: custom_emoji must be a String')
|
|
|
|
|
custom_emoji = 'a:' + custom_emoji
|
|
|
|
|
assert(messageordict is Message or typeof(messageordict) == TYPE_DICTIONARY, 'Invalid type: Expected a Message or Dictionary')
|
|
|
|
|
if typeof(messageordict) == TYPE_DICTIONARY and messageordict.has('message_id'):
|
|
|
|
|
messageordict.id = messageordict.message_id
|
|
|
|
|
|
|
|
|
|
var ret = await _send_get('/channels/%s/messages/%s/reactions/%s' % [messageordict.channel_id, messageordict.id, custom_emoji])
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region commands
|
|
|
|
|
|
|
|
|
|
func register_command(command: ApplicationCommand, guild_id: String = '') -> ApplicationCommand:
|
|
|
|
|
var slug = '/applications/%s' % application.id
|
|
|
|
|
|
|
|
|
|
if Helpers.is_valid_str(guild_id):
|
|
|
|
|
# Registering a guild command
|
|
|
|
|
slug += '/guilds/%s' % guild_id
|
|
|
|
|
|
|
|
|
|
slug += '/commands'
|
|
|
|
|
var res = await _send_request(slug, command._to_dict(true))
|
|
|
|
|
return ApplicationCommand.new(res)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func register_commands(commands: Array, guild_id: String = '') -> Array:
|
|
|
|
|
for i in range(len(commands)):
|
|
|
|
|
if commands[i] is ApplicationCommand:
|
|
|
|
|
commands[i] = commands[i]._to_dict(true)
|
|
|
|
|
|
|
|
|
|
var slug = '/applications/%s' % application.id
|
|
|
|
|
|
|
|
|
|
if Helpers.is_valid_str(guild_id):
|
|
|
|
|
# Registering guild commands
|
|
|
|
|
slug += '/guilds/%s' % guild_id
|
|
|
|
|
|
|
|
|
|
slug += '/commands'
|
|
|
|
|
var res = await _send_request(slug, commands, HTTPClient.METHOD_PUT)
|
|
|
|
|
if typeof(res) == TYPE_ARRAY:
|
|
|
|
|
for i in range(len(res)):
|
|
|
|
|
res[i] = ApplicationCommand.new(res[i])
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func delete_command(command_id: String, guild_id: String = '') -> int:
|
|
|
|
|
var slug = '/applications/%s' % application.id
|
|
|
|
|
|
|
|
|
|
if Helpers.is_valid_str(guild_id):
|
|
|
|
|
# Deleting a guild command
|
|
|
|
|
slug += '/guilds/%s' % guild_id
|
|
|
|
|
|
|
|
|
|
slug += '/commands/%s' % command_id
|
|
|
|
|
var res = await _send_get(slug, HTTPClient.METHOD_DELETE)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func delete_commands(guild_id: String = '') -> int:
|
|
|
|
|
var slug = '/applications/%s' % application.id
|
|
|
|
|
|
|
|
|
|
if Helpers.is_valid_str(guild_id):
|
|
|
|
|
# Deleting guild commands
|
|
|
|
|
slug += '/guilds/%s' % guild_id
|
|
|
|
|
|
|
|
|
|
slug += '/commands'
|
|
|
|
|
var res = await _send_request(slug, [], HTTPClient.METHOD_PUT)
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_command(command_id: String, guild_id: String = '') -> ApplicationCommand:
|
|
|
|
|
var slug = '/applications/%s' % application.id
|
|
|
|
|
|
|
|
|
|
if Helpers.is_valid_str(guild_id):
|
|
|
|
|
# Getting a guild command
|
|
|
|
|
slug += '/guilds/%s' % guild_id
|
|
|
|
|
|
|
|
|
|
slug += '/commands/%s' % command_id
|
|
|
|
|
|
|
|
|
|
var cmd = await _send_get(slug)
|
|
|
|
|
cmd = ApplicationCommand.new(cmd)
|
|
|
|
|
return cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_commands(guild_id: String = '') -> Array:
|
|
|
|
|
var slug = '/applications/%s' % application.id
|
|
|
|
|
|
|
|
|
|
if Helpers.is_valid_str(guild_id):
|
|
|
|
|
# Getting guild commands
|
|
|
|
|
slug += '/guilds/%s' % guild_id
|
|
|
|
|
|
|
|
|
|
slug += '/commands'
|
|
|
|
|
|
|
|
|
|
var cmds = await _send_get(slug)
|
|
|
|
|
for i in range(len(cmds)):
|
|
|
|
|
cmds[i] = ApplicationCommand.new(cmds[i])
|
|
|
|
|
return cmds
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func set_presence(p_options: Dictionary) -> void:
|
|
|
|
|
"""
|
|
|
|
|
p_options {
|
|
|
|
|
status: String, text of the presence,
|
|
|
|
|
afk: bool, whether or not the client is afk,
|
|
|
|
|
|
|
|
|
|
activity: {
|
|
|
|
|
type: String, type of the presence,
|
|
|
|
|
name: String, name of the presence,
|
|
|
|
|
url: String, url of the presence,
|
|
|
|
|
created_at: int, unix timestamp (in milliseconds) of when activity was added to user's session
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
var new_presence = {'status': 'online', 'afk': false, 'activity': {}}
|
|
|
|
|
|
|
|
|
|
assert(p_options, 'Missing options for set_presence')
|
|
|
|
|
assert(
|
|
|
|
|
typeof(p_options) == TYPE_DICTIONARY,
|
|
|
|
|
'Invalid Type: options in set_presence must be a Dictionary'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if p_options.has('status') and Helpers.is_valid_str(p_options.status):
|
|
|
|
|
assert(
|
|
|
|
|
str(p_options.status).to_upper() in PRESENCE_STATUS_TYPES,
|
|
|
|
|
'Invalid Type: status must be one of PRESENCE_STATUS_TYPES'
|
|
|
|
|
)
|
|
|
|
|
new_presence.status = p_options.status.to_lower()
|
|
|
|
|
if p_options.has('afk') and typeof(p_options.afk) == TYPE_BOOL:
|
|
|
|
|
new_presence.afk = p_options.afk
|
|
|
|
|
|
|
|
|
|
# Check if an activity was passed
|
|
|
|
|
if p_options.has('activity') and typeof(p_options.activity) == TYPE_DICTIONARY:
|
|
|
|
|
if p_options.activity.has('name') and Helpers.is_valid_str(p_options.activity.name):
|
|
|
|
|
new_presence.activity.name = p_options.activity.name
|
|
|
|
|
|
|
|
|
|
if p_options.activity.has('url') and Helpers.is_valid_str(p_options.activity.url):
|
|
|
|
|
new_presence.activity.url = p_options.activity.url
|
|
|
|
|
|
|
|
|
|
if p_options.activity.has('created_at') and Helpers.is_num(p_options.activity.created_at):
|
|
|
|
|
new_presence.activity.created_at = p_options.activity.created_at
|
|
|
|
|
else:
|
|
|
|
|
new_presence.activity.created_at = Time.get_unix_time_from_system() * 1000
|
|
|
|
|
|
|
|
|
|
if p_options.activity.has('type') and Helpers.is_valid_str(p_options.activity.type):
|
|
|
|
|
assert(
|
|
|
|
|
str(p_options.activity.type).to_upper() in ACTIVITY_TYPES,
|
|
|
|
|
'Invalid Type: type must be one of ACTIVITY_TYPES'
|
|
|
|
|
)
|
|
|
|
|
new_presence.activity.type = ACTIVITY_TYPES[str(p_options.activity.type).to_upper()]
|
|
|
|
|
|
|
|
|
|
_update_presence(new_presence)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Inbuilt Functions
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
randomize()
|
|
|
|
|
|
|
|
|
|
# Generate needed nodes
|
|
|
|
|
_generate_timer_nodes()
|
|
|
|
|
|
|
|
|
|
# Setup web socket client
|
|
|
|
|
_client = WebSocketPeer.new()
|
|
|
|
|
|
|
|
|
|
$HeartbeatTimer.timeout.connect(_send_heartbeat)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(_delta) -> void:
|
|
|
|
|
if not _client:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_client.poll()
|
|
|
|
|
|
|
|
|
|
var state = _client.get_ready_state()
|
|
|
|
|
|
|
|
|
|
if state == WebSocketPeer.STATE_OPEN:
|
|
|
|
|
while _client.get_available_packet_count():
|
|
|
|
|
var packet = _client.get_packet()
|
|
|
|
|
_data_received(packet.get_string_from_utf8())
|
|
|
|
|
elif state == WebSocketPeer.STATE_CLOSED:
|
|
|
|
|
var code = _client.get_close_code()
|
|
|
|
|
var reason = _client.get_close_reason()
|
|
|
|
|
_connection_closed(code, reason)
|
|
|
|
|
set_process(false) # Stop processing.
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region Private Functions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _generate_timer_nodes() -> void:
|
|
|
|
|
var heart_beat_timer = Timer.new()
|
|
|
|
|
heart_beat_timer.name = 'HeartbeatTimer'
|
|
|
|
|
add_child(heart_beat_timer)
|
|
|
|
|
|
|
|
|
|
var invalid_session_timer = Timer.new()
|
|
|
|
|
invalid_session_timer.name = 'InvalidSessionTimer'
|
|
|
|
|
add_child(invalid_session_timer)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _connection_closed(code: int, reason: String) -> void:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('WSS connection closed with code=%s, reason=%s' % [code, reason])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _data_received(msg: String) -> void:
|
|
|
|
|
#if VERBOSE:
|
|
|
|
|
#print("Got packet: ", msg)
|
|
|
|
|
var data := msg
|
|
|
|
|
var dict = _jsonstring_to_dict(data)
|
|
|
|
|
var op = str(int(dict.op)) # OP Code Received
|
|
|
|
|
var d = dict.d # Data Received
|
|
|
|
|
|
|
|
|
|
match op:
|
|
|
|
|
'10':
|
|
|
|
|
# Got hello
|
|
|
|
|
_setup_heartbeat_timer(d.heartbeat_interval)
|
|
|
|
|
|
|
|
|
|
var response_d = {'op': -1}
|
|
|
|
|
if _sess_id:
|
|
|
|
|
# Resume session
|
|
|
|
|
response_d.op = 6
|
|
|
|
|
response_d['d'] = {'token': TOKEN, 'session_id': _sess_id, 'seq': _last_seq}
|
|
|
|
|
else:
|
|
|
|
|
# Make new session
|
|
|
|
|
response_d.op = 2
|
|
|
|
|
response_d['d'] = {
|
|
|
|
|
'token': TOKEN,
|
|
|
|
|
'intents': INTENTS,
|
|
|
|
|
'properties':
|
|
|
|
|
{'$os': 'linux', '$browser': 'discord.gd', '$device': 'discord.gd'}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_send_dict_wss(response_d)
|
|
|
|
|
'11':
|
|
|
|
|
# Heartbeat Acknowledged
|
|
|
|
|
_heartbeat_ack_received = true
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Heartbeat ack')
|
|
|
|
|
'9':
|
|
|
|
|
# Opcode 9 Invalid Session
|
|
|
|
|
_invalid_session_is_resumable = d
|
|
|
|
|
var timer = $InvalidSessionTimer
|
|
|
|
|
timer.one_shot = true
|
|
|
|
|
timer.wait_time = randi_range(1, 5)
|
|
|
|
|
timer.start()
|
|
|
|
|
'0':
|
|
|
|
|
# Event Dispatched
|
|
|
|
|
_handle_events(dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_heartbeat() -> void: # Send heartbeat OP code 1
|
|
|
|
|
if not _heartbeat_ack_received:
|
|
|
|
|
_client.close(1002)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var response_payload = {'op': 1, 'd': _last_seq}
|
2025-11-03 01:38:28 +11:00
|
|
|
if _client.get_ready_state() == WebSocketPeer.State.STATE_OPEN:
|
|
|
|
|
_send_dict_wss(response_payload)
|
|
|
|
|
else:
|
|
|
|
|
print("Websocket not open, skipping send")
|
|
|
|
|
login()
|
2025-10-29 22:23:34 +11:00
|
|
|
_heartbeat_ack_received = false
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Heartbeat sent!')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _handle_events(dict: Dictionary) -> void:
|
|
|
|
|
_last_seq = dict.s
|
|
|
|
|
var event_name = dict.t
|
|
|
|
|
|
|
|
|
|
match event_name:
|
|
|
|
|
'READY':
|
|
|
|
|
_sess_id = dict.d.session_id
|
|
|
|
|
var d = dict.d
|
|
|
|
|
|
|
|
|
|
var _application = d.application
|
|
|
|
|
var _guilds = d.guilds
|
|
|
|
|
_clean_guilds(_guilds)
|
|
|
|
|
|
|
|
|
|
var _user: User = User.new(self, d.user)
|
|
|
|
|
user = _user
|
|
|
|
|
application = _application
|
|
|
|
|
|
|
|
|
|
for guild in _guilds:
|
|
|
|
|
guilds[guild.id] = guild
|
|
|
|
|
|
|
|
|
|
'GUILD_CREATE':
|
|
|
|
|
var guild = dict.d
|
|
|
|
|
_clean_guilds([guild])
|
|
|
|
|
# Update number of cached guilds
|
|
|
|
|
if guild.has('lazy') and guild.lazy:
|
|
|
|
|
guilds_loaded += 1
|
|
|
|
|
if guilds_loaded == guilds.size():
|
|
|
|
|
bot_ready.emit(self)
|
|
|
|
|
|
|
|
|
|
if not guilds.has(guild.id):
|
|
|
|
|
# Joined a new guild
|
|
|
|
|
guild_create.emit(self, guild)
|
|
|
|
|
|
|
|
|
|
# Update cache
|
|
|
|
|
guilds[guild.id] = guild
|
|
|
|
|
|
|
|
|
|
'GUILD_UPDATE':
|
|
|
|
|
var guild = dict.d
|
|
|
|
|
_clean_guilds([guild])
|
|
|
|
|
guilds[guild.id] = guild
|
|
|
|
|
guild_update.emit(self, guild)
|
|
|
|
|
|
|
|
|
|
'GUILD_DELETE':
|
|
|
|
|
var guild = dict.d
|
|
|
|
|
guilds.erase(guild.id)
|
|
|
|
|
guild_delete.emit(self, guild.id)
|
|
|
|
|
|
|
|
|
|
# 'GUILD_MEMBER_ADD':
|
|
|
|
|
# print('-----------guild member add')
|
|
|
|
|
# var member = dict.d
|
|
|
|
|
# print(member)
|
|
|
|
|
|
|
|
|
|
# 'GUILD_MEMBER_UPDATE':
|
|
|
|
|
# print('--------guild_member update')
|
|
|
|
|
# var data = dict.d
|
|
|
|
|
|
|
|
|
|
# var guild = guilds[data.guild_id]
|
|
|
|
|
# data.erase('guild_id')
|
|
|
|
|
|
|
|
|
|
# # Update users cache
|
|
|
|
|
# var user = data.user
|
|
|
|
|
# var user_id = user.id
|
|
|
|
|
# data.erase('user')
|
|
|
|
|
# users[user_id] = user
|
|
|
|
|
|
|
|
|
|
# if data.has('pending'):
|
|
|
|
|
# var pending = data.pending
|
|
|
|
|
# data.erase('pending')
|
|
|
|
|
# data.is_pending = pending
|
|
|
|
|
# guild.members[user_id] = data
|
|
|
|
|
|
|
|
|
|
# 'GUILD_MEMBER_DELETE':
|
|
|
|
|
# print('-----------guild member delete')
|
|
|
|
|
# var member = dict.d
|
|
|
|
|
# print(member)
|
|
|
|
|
|
|
|
|
|
# 'GUILD_MEMBERS_CHUNK':
|
|
|
|
|
# print('-----------guild member chunk')
|
|
|
|
|
# var member = dict.d
|
|
|
|
|
# print(member)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'RESUMED':
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Session Resumed')
|
|
|
|
|
|
|
|
|
|
'MESSAGE_CREATE':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
|
|
|
|
|
# Dont respond to webhooks
|
|
|
|
|
if d.has('webhook_id') and d.webhook_id:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if d.has('sticker_items') and d.sticker_items and typeof(d.sticker_items) == TYPE_ARRAY:
|
|
|
|
|
if d.sticker_items.size() != 0:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var coroutine = await _parse_message(d)
|
|
|
|
|
if coroutine == null:
|
|
|
|
|
# message might be a thread
|
|
|
|
|
# TODO: Handle sending messages in threads
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
d = Message.new(d)
|
|
|
|
|
|
|
|
|
|
var channel = channels.get(str(d.channel_id))
|
|
|
|
|
message_create.emit(self, d, channel)
|
|
|
|
|
|
|
|
|
|
'MESSAGE_DELETE':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
message_delete.emit(self, d)
|
|
|
|
|
|
|
|
|
|
'MESSAGE_REACTION_ADD':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
|
|
|
|
|
message_reaction_add.emit(self, d)
|
|
|
|
|
|
|
|
|
|
'MESSAGE_REACTION_REMOVE':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
message_reaction_remove.emit(self, d)
|
|
|
|
|
|
|
|
|
|
'MESSAGE_REACTION_REMOVE_ALL':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
message_reaction_remove_all.emit(self, d)
|
|
|
|
|
|
|
|
|
|
'MESSAGE_REACTION_REMOVE_EMOJI':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
message_reaction_remove_emoji.emit(self, d)
|
|
|
|
|
|
|
|
|
|
'INTERACTION_CREATE':
|
|
|
|
|
var d = dict.d
|
|
|
|
|
|
|
|
|
|
var id = d.id
|
|
|
|
|
var data = d.data
|
|
|
|
|
var token = d.token
|
|
|
|
|
|
|
|
|
|
var interaction = await DiscordInteraction.new(self, d)
|
|
|
|
|
interaction_create.emit(self, interaction)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_raw_request(slug: String, payload: Dictionary, method = HTTPClient.METHOD_POST):
|
|
|
|
|
var headers = _headers.duplicate(true)
|
|
|
|
|
var multipart_header = 'Content-Type: multipart/form-data; boundary="boundary"'
|
|
|
|
|
if headers.find(multipart_header) == -1:
|
|
|
|
|
headers.append(multipart_header)
|
|
|
|
|
|
|
|
|
|
var http_client = HTTPClient.new()
|
|
|
|
|
|
|
|
|
|
var body = PackedByteArray()
|
|
|
|
|
|
|
|
|
|
# Add the payload_json to the form
|
|
|
|
|
body.append_array('--boundary\r\n'.to_utf8_buffer())
|
|
|
|
|
body.append_array('Content-Disposition: form-data; name="payload_json"\r\n'.to_utf8_buffer())
|
|
|
|
|
body.append_array('Content-Type: application/json\r\n\r\n'.to_utf8_buffer())
|
|
|
|
|
|
|
|
|
|
if payload.has('payload_json'):
|
|
|
|
|
body.append_array(JSON.stringify(payload.payload_json).to_utf8_buffer())
|
|
|
|
|
elif payload.has('payload'):
|
|
|
|
|
body.append_array(JSON.stringify(payload.payload).to_utf8_buffer())
|
|
|
|
|
|
|
|
|
|
var count = 0
|
|
|
|
|
for file in payload.files:
|
|
|
|
|
# Extract the name, media_type and data of each file
|
|
|
|
|
var file_name = file.name
|
|
|
|
|
var media_type = file.media_type
|
|
|
|
|
var data = file.data
|
|
|
|
|
# Add the file to the form
|
|
|
|
|
body.append_array('\r\n--boundary\r\n'.to_utf8_buffer())
|
|
|
|
|
body.append_array(
|
|
|
|
|
('Content-Disposition: form-data; name="file' + str(count) + '"; filename="' + file_name + '"').to_utf8_buffer()
|
|
|
|
|
)
|
|
|
|
|
body.append_array(('\r\nContent-Type: ' + media_type + '\r\n\r\n').to_utf8_buffer())
|
|
|
|
|
body.append_array(data)
|
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
# End the form-data
|
|
|
|
|
body.append_array('\r\n--boundary--'.to_utf8_buffer())
|
|
|
|
|
var err = http_client.connect_to_host(_https_domain)
|
|
|
|
|
assert(err == OK, 'Error connecting to Discord HTTPS server')
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
http_client.get_status() == HTTPClient.STATUS_CONNECTING
|
|
|
|
|
or http_client.get_status() == HTTPClient.STATUS_RESOLVING
|
|
|
|
|
):
|
|
|
|
|
http_client.poll()
|
|
|
|
|
await get_tree().process_frame
|
|
|
|
|
|
|
|
|
|
assert(
|
|
|
|
|
http_client.get_status() == HTTPClient.STATUS_CONNECTED,
|
|
|
|
|
'Could not connect to Discord HTTPS server'
|
|
|
|
|
)
|
|
|
|
|
err = http_client.request_raw(method, _api_slug + slug, headers, body)
|
|
|
|
|
|
|
|
|
|
while http_client.get_status() == HTTPClient.STATUS_REQUESTING:
|
|
|
|
|
http_client.poll()
|
|
|
|
|
await get_tree().process_frame
|
|
|
|
|
|
|
|
|
|
# Request is made, now extract the reponse body
|
|
|
|
|
assert(
|
|
|
|
|
(
|
|
|
|
|
http_client.get_status() == HTTPClient.STATUS_BODY
|
|
|
|
|
or http_client.get_status() == HTTPClient.STATUS_CONNECTED
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if http_client.has_response():
|
|
|
|
|
headers = http_client.get_response_headers_as_dictionary()
|
|
|
|
|
|
|
|
|
|
var rb = PackedByteArray()
|
|
|
|
|
while http_client.get_status() == HTTPClient.STATUS_BODY:
|
|
|
|
|
# While there is body left to be read
|
|
|
|
|
http_client.poll()
|
|
|
|
|
var chunk = http_client.read_response_body_chunk()
|
|
|
|
|
if chunk.size() == 0:
|
|
|
|
|
# Got nothing, wait for buffers to fill a bit.
|
|
|
|
|
OS.delay_usec(1000)
|
|
|
|
|
else:
|
|
|
|
|
rb = rb + chunk # Append to read buffer.
|
|
|
|
|
|
|
|
|
|
var response = _jsonstring_to_dict(rb.get_string_from_utf8())
|
|
|
|
|
if response == null:
|
|
|
|
|
if http_client.get_response_code() == 204:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
if response.has('code'):
|
|
|
|
|
print('Response: status code ', str(http_client.get_response_code()))
|
|
|
|
|
print(JSON.stringify(response, '\t'))
|
|
|
|
|
|
|
|
|
|
assert(not response.has('code'), 'Error sending request. See output window')
|
|
|
|
|
|
|
|
|
|
if response.has('retry_after'):
|
|
|
|
|
# We got ratelimited
|
|
|
|
|
await get_tree().create_timer(int(response.retry_after)).timeout
|
|
|
|
|
response = await _send_raw_request(slug, payload, method)
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
else:
|
|
|
|
|
assert(false, 'Unable to upload file. Got empty response from server')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_request(slug: String, payload, method = HTTPClient.METHOD_POST):
|
|
|
|
|
var headers = _headers.duplicate(true)
|
|
|
|
|
|
|
|
|
|
var json_header = 'Content-Type: application/json'
|
|
|
|
|
if headers.find(json_header) == -1:
|
|
|
|
|
headers.append(json_header)
|
|
|
|
|
|
|
|
|
|
var http_request = HTTPRequest.new()
|
|
|
|
|
add_child(http_request)
|
|
|
|
|
http_request.request.call_deferred(
|
|
|
|
|
_https_base + slug, headers, method, JSON.stringify(payload)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var data = await http_request.request_completed
|
|
|
|
|
http_request.queue_free()
|
|
|
|
|
|
|
|
|
|
# Check for errors
|
|
|
|
|
assert(data[0] == HTTPRequest.RESULT_SUCCESS, 'Error sending request: HTTP Failed')
|
|
|
|
|
var response = _jsonstring_to_dict(data[3].get_string_from_utf8())
|
|
|
|
|
if response == null:
|
|
|
|
|
if data[1] == 204:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
if response and response.has('code'):
|
|
|
|
|
# Got an error
|
|
|
|
|
print('Response: status code ', str(data[1]))
|
|
|
|
|
print('Error: ' + JSON.stringify(response, '\t'))
|
|
|
|
|
|
|
|
|
|
if method != HTTPClient.METHOD_DELETE:
|
|
|
|
|
if response.has('code'):
|
|
|
|
|
push_error('Error sending request. See output window')
|
|
|
|
|
|
|
|
|
|
if response.has('retry_after'):
|
|
|
|
|
# We got ratelimited
|
|
|
|
|
await get_tree().create_timer(int(response.retry_after)).timeout
|
|
|
|
|
response = await _send_request(slug, payload, method)
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _get_dm_channel(channel_id: String) -> Dictionary:
|
|
|
|
|
assert(Helpers.is_valid_str(channel_id), 'Invalid Type: channel_id must be a valid String')
|
|
|
|
|
var data = await _send_get('/channels/%s' % channel_id)
|
|
|
|
|
if typeof(data) == TYPE_DICTIONARY:
|
|
|
|
|
_clean_channel(data)
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_get(slug, method = HTTPClient.METHOD_GET, additional_headers = []):
|
|
|
|
|
var http_request = HTTPRequest.new()
|
|
|
|
|
add_child(http_request)
|
|
|
|
|
|
|
|
|
|
var headers = _headers + additional_headers
|
|
|
|
|
http_request.request.call_deferred(_https_base + slug, headers, method)
|
|
|
|
|
|
|
|
|
|
var data = await http_request.request_completed
|
|
|
|
|
http_request.queue_free()
|
|
|
|
|
|
|
|
|
|
assert(data[0] == HTTPRequest.RESULT_SUCCESS)
|
|
|
|
|
if method == HTTPClient.METHOD_GET:
|
|
|
|
|
var response = _jsonstring_to_dict(data[3].get_string_from_utf8())
|
|
|
|
|
if response != null and response.has('code'):
|
|
|
|
|
# Got an error
|
|
|
|
|
print('GET: status code ', str(data[1]))
|
|
|
|
|
print('Error sending GET request: ' + JSON.stringify(response, '\t'))
|
|
|
|
|
push_error('Error sending GET request. See output window')
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
else: # Maybe a PUT/DELETE for reaction
|
|
|
|
|
return data[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_get_cdn(slug) -> PackedByteArray:
|
|
|
|
|
var http_request = HTTPRequest.new()
|
|
|
|
|
add_child(http_request)
|
|
|
|
|
|
|
|
|
|
if slug.find('/') == 0:
|
|
|
|
|
http_request.request(_cdn_base + slug, _headers)
|
|
|
|
|
else:
|
|
|
|
|
http_request.request(slug, _headers)
|
|
|
|
|
|
|
|
|
|
var data = await http_request.request_completed
|
|
|
|
|
http_request.queue_free()
|
|
|
|
|
|
|
|
|
|
# Check for errors
|
|
|
|
|
assert(data[0] == HTTPRequest.RESULT_SUCCESS, 'Error sending GET cdn request: HTTP Failed')
|
|
|
|
|
|
|
|
|
|
if data[1] != 200:
|
|
|
|
|
print('HTTPS GET cdn Error: Status Code: %s' % data[1])
|
|
|
|
|
assert(data[1] == 200, 'HTTPS GET cdn Error: Status code ' + str(data[1]))
|
|
|
|
|
|
|
|
|
|
return data[3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_message_request(
|
|
|
|
|
messageorchannelid, content, options := {}, method := HTTPClient.METHOD_POST
|
|
|
|
|
):
|
|
|
|
|
var payload = {
|
|
|
|
|
'content': null,
|
|
|
|
|
'tts': false,
|
|
|
|
|
'embeds': null,
|
|
|
|
|
'components': null,
|
|
|
|
|
'allowed_mentions': null,
|
|
|
|
|
'message_reference': null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var slug
|
|
|
|
|
if messageorchannelid is Message:
|
|
|
|
|
slug ='/channels/%s/messages' % str(messageorchannelid.channel_id)
|
|
|
|
|
else:
|
|
|
|
|
assert(messageorchannelid.length() > 16, 'channel_id is not valid')
|
|
|
|
|
slug = '/channels/%s/messages' % str(messageorchannelid)
|
|
|
|
|
|
|
|
|
|
# Handle edit message or delete message
|
|
|
|
|
if method == HTTPClient.METHOD_PATCH or method == HTTPClient.METHOD_DELETE:
|
|
|
|
|
slug += '/' + str(messageorchannelid.id)
|
|
|
|
|
|
|
|
|
|
if method == HTTPClient.METHOD_PATCH:
|
|
|
|
|
if typeof(messageorchannelid) == TYPE_OBJECT and typeof(messageorchannelid.attachments) == TYPE_ARRAY:
|
|
|
|
|
if messageorchannelid.attachments.size() == 0:
|
|
|
|
|
payload.attachments = null
|
|
|
|
|
else:
|
|
|
|
|
# Add the attachments to keep to the payload
|
|
|
|
|
payload.attachments = messageorchannelid.attachments
|
|
|
|
|
|
|
|
|
|
# Check if the content is only a string
|
|
|
|
|
if typeof(content) == TYPE_STRING and content.length() > 0:
|
|
|
|
|
assert(content.length() <= 2048, 'Message content must be less than 2048 characters')
|
|
|
|
|
payload.content = content
|
|
|
|
|
|
|
|
|
|
elif typeof(content) == TYPE_DICTIONARY: # Check if the content is the options dictionary
|
|
|
|
|
options = content
|
|
|
|
|
content = null
|
|
|
|
|
|
|
|
|
|
# Parse the options
|
|
|
|
|
if typeof(options) == TYPE_DICTIONARY:
|
|
|
|
|
"""parse the message options - refer https://discord.com/developers/docs/resources/channel#create-message-jsonform-params
|
|
|
|
|
options {
|
|
|
|
|
tts: bool,
|
|
|
|
|
embeds: Array,
|
|
|
|
|
components: Array,
|
|
|
|
|
files: Array,
|
|
|
|
|
allowed_mentions: object,
|
|
|
|
|
message_reference: object,
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if options.has('content') and Helpers.is_str(options.content):
|
|
|
|
|
assert(
|
|
|
|
|
options.content.length() <= 2048,
|
|
|
|
|
'Message content must be less than 2048 characters'
|
|
|
|
|
)
|
|
|
|
|
payload.content = options.content
|
|
|
|
|
|
|
|
|
|
if options.has('tts') and options.tts:
|
|
|
|
|
payload.tts = true
|
|
|
|
|
|
|
|
|
|
if options.has('embeds') and options.embeds.size() > 0:
|
|
|
|
|
for embed in options.embeds:
|
|
|
|
|
if embed is Embed:
|
|
|
|
|
if payload.embeds == null:
|
|
|
|
|
payload.embeds = []
|
|
|
|
|
payload.embeds.append(embed._to_dict())
|
|
|
|
|
|
|
|
|
|
if options.has('components') and options.components.size() > 0:
|
|
|
|
|
assert(
|
|
|
|
|
options.components.size() <= 5,
|
|
|
|
|
'Message can have a max of 5 MessageActionRow components.'
|
|
|
|
|
)
|
|
|
|
|
for component in options.components:
|
|
|
|
|
assert(
|
|
|
|
|
component is MessageActionRow, 'Parent component must be a MessageActionRow.'
|
|
|
|
|
)
|
|
|
|
|
if payload.components == null:
|
|
|
|
|
payload.components = []
|
|
|
|
|
payload.components.append(component._to_dict())
|
|
|
|
|
|
|
|
|
|
if options.has('allowed_mentions') and options.allowed_mentions:
|
|
|
|
|
if typeof(options.allowed_mentions) == TYPE_DICTIONARY:
|
|
|
|
|
"""
|
|
|
|
|
allowedMentions {
|
|
|
|
|
parse: array of mention types ['roles', 'users', 'everyone']
|
|
|
|
|
roles: array of role_ids
|
|
|
|
|
users: array of user_ids
|
|
|
|
|
replied_user: bool, whether to mention author of msg
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
payload.allowed_mentions = options.allowed_mentions
|
|
|
|
|
|
|
|
|
|
if options.has('message_reference') and options.message_reference:
|
|
|
|
|
"""
|
|
|
|
|
message_reference {
|
|
|
|
|
message_id: id of originating msg,
|
|
|
|
|
channel_id? *: optional
|
|
|
|
|
guild_id?: optional
|
|
|
|
|
fail_if_not_exists?: bool, whether to error
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
payload.message_reference = options.message_reference
|
|
|
|
|
|
|
|
|
|
if options.has('files') and options.files:
|
|
|
|
|
assert(
|
|
|
|
|
typeof(options.files) == TYPE_ARRAY,
|
|
|
|
|
'Invalid Type: files in message options must be an array'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if options.files.size() > 0:
|
|
|
|
|
# Loop through each file
|
|
|
|
|
for file in options.files:
|
|
|
|
|
assert(
|
|
|
|
|
file.has('name') and Helpers.is_valid_str(file.name),
|
|
|
|
|
'Missing name for file in files'
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
file.has('media_type') and Helpers.is_valid_str(file.media_type),
|
|
|
|
|
'Missing media_type for file in files'
|
|
|
|
|
)
|
|
|
|
|
assert(file.has('data') and file.data, 'Missing data for file in files')
|
|
|
|
|
assert(
|
|
|
|
|
file.data is PackedByteArray,
|
|
|
|
|
'Invalid Type: data of file in files must be PackedByteArray'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var json_payload = payload.duplicate(true)
|
|
|
|
|
var new_payload = {'files': options.files, 'payload_json': json_payload}
|
|
|
|
|
payload = new_payload
|
|
|
|
|
|
|
|
|
|
var res
|
|
|
|
|
if payload.has('files') and payload.files and typeof(payload.files) == TYPE_ARRAY:
|
|
|
|
|
# Send raw post request using multipart/form-data
|
|
|
|
|
var coroutine = await _send_raw_request(slug, payload, method)
|
|
|
|
|
if typeof(coroutine) == TYPE_OBJECT:
|
|
|
|
|
res = await coroutine
|
|
|
|
|
else:
|
|
|
|
|
res = coroutine
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
res = await _send_request(slug, payload, method)
|
|
|
|
|
|
|
|
|
|
if method == HTTPClient.METHOD_DELETE:
|
|
|
|
|
return res
|
|
|
|
|
else:
|
|
|
|
|
await _parse_message(res)
|
|
|
|
|
|
|
|
|
|
if res.has("code") and res.has("errors"):
|
|
|
|
|
# its an error
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
var msg = Message.new(res)
|
|
|
|
|
return msg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _update_presence(new_presence: Dictionary) -> void:
|
|
|
|
|
var status = new_presence.status
|
|
|
|
|
var activity = new_presence.activity
|
|
|
|
|
|
|
|
|
|
var response_d = {
|
|
|
|
|
'op': 3, # Presence update
|
|
|
|
|
}
|
|
|
|
|
response_d['d'] = {
|
|
|
|
|
'since': new_presence if new_presence.has('since') else null,
|
|
|
|
|
'status': new_presence.status,
|
|
|
|
|
'afk': new_presence.afk,
|
|
|
|
|
'activities': [new_presence.activity]
|
|
|
|
|
}
|
|
|
|
|
_send_dict_wss(response_d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Helper functions
|
|
|
|
|
func _jsonstring_to_dict(data: String):
|
|
|
|
|
var json = JSON.new()
|
|
|
|
|
var result = json.parse(data)
|
|
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
if result != OK:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print("Failed to parse json: error at line %s with msg %s for data %s" % [json.get_error_line(), json.get_error_message(), data])
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
return json.data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _setup_heartbeat_timer(interval: int) -> void:
|
|
|
|
|
# Setup heartbeat timer and start it
|
|
|
|
|
_heartbeat_interval = int(interval) / 1000
|
|
|
|
|
var timer = $HeartbeatTimer
|
|
|
|
|
timer.wait_time = _heartbeat_interval
|
|
|
|
|
timer.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _send_dict_wss(d: Dictionary) -> void:
|
|
|
|
|
var payload = JSON.stringify(d)
|
|
|
|
|
var err = _client.put_packet(payload.to_utf8_buffer())
|
|
|
|
|
if OK != err:
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print("Failed to send packet: error=%s (%s)" % [error_string(err), err])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _clean_guilds(guilds: Array) -> void:
|
|
|
|
|
for guild in guilds:
|
|
|
|
|
# Converts the unavailable property to available
|
|
|
|
|
if guild.has('unavailable'):
|
|
|
|
|
guild.available = not guild.unavailable
|
|
|
|
|
else:
|
|
|
|
|
guild.available = true
|
|
|
|
|
guild.erase('unavailable')
|
|
|
|
|
|
|
|
|
|
if guild.has('channels'):
|
|
|
|
|
for channel in guild.channels:
|
|
|
|
|
_clean_channel(channel)
|
|
|
|
|
channel.guild_id = guild.id
|
|
|
|
|
channels[channel.id] = channel
|
|
|
|
|
|
|
|
|
|
if guild.has('members') and typeof(guild.members) == TYPE_ARRAY:
|
|
|
|
|
# Parse the guild members
|
|
|
|
|
var members = {}
|
|
|
|
|
for member in guild.members:
|
|
|
|
|
var member_id = member.user.id
|
|
|
|
|
users[member_id] = member.user
|
|
|
|
|
member.erase('user')
|
|
|
|
|
members[member_id] = member
|
|
|
|
|
guild.members = members
|
|
|
|
|
|
|
|
|
|
if guild.has('roles') and typeof(guild.roles) == TYPE_ARRAY:
|
|
|
|
|
# Parse the guild roles
|
|
|
|
|
var roles = {}
|
|
|
|
|
for role in guild.roles:
|
|
|
|
|
var role_id = role.id
|
|
|
|
|
role.erase('id')
|
|
|
|
|
roles[role_id] = role
|
|
|
|
|
guild.roles = roles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _clean_channel(channel: Dictionary) -> void:
|
|
|
|
|
_float_to_int(channel, "type")
|
|
|
|
|
_float_to_int(channel, "flags")
|
|
|
|
|
if channel.has('type') and typeof(channel.type) == TYPE_INT:
|
|
|
|
|
channel.type = CHANNEL_TYPES.get(str(channel.type))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _parse_message(message):
|
|
|
|
|
if typeof(message) == TYPE_OBJECT and message is Message:
|
|
|
|
|
return message
|
|
|
|
|
|
|
|
|
|
if typeof(message) != TYPE_DICTIONARY:
|
|
|
|
|
printerr("_parse_message error: Type of message must be dictionary")
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
_float_to_int(message, "type")
|
|
|
|
|
_float_to_int(message, "flags")
|
|
|
|
|
|
|
|
|
|
if message.has('channel_id') and message.channel_id:
|
|
|
|
|
# Check if channel is cached
|
|
|
|
|
var channel = channels.get(str(message.channel_id))
|
|
|
|
|
|
|
|
|
|
if not channel:
|
|
|
|
|
# Try to check if it is a DM channel
|
|
|
|
|
if VERBOSE:
|
|
|
|
|
print('Fetching DM channel: %s from api' % message.channel_id)
|
|
|
|
|
|
|
|
|
|
channel = await _get_dm_channel(message.channel_id)
|
|
|
|
|
_clean_channel(channel)
|
|
|
|
|
|
|
|
|
|
if channel and channel.has('type') and channel.type == 'DM':
|
|
|
|
|
channels[str(message.channel_id)] = channel
|
|
|
|
|
else:
|
|
|
|
|
# not a valid channel, it might be a thread
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
|
|
if message.has('author') and typeof(message.author) == TYPE_DICTIONARY:
|
|
|
|
|
# get the cached author of the message
|
|
|
|
|
message.author = User.new(self, message.author)
|
|
|
|
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _float_to_int(dict, key):
|
|
|
|
|
if dict.has(key) and typeof(dict[key]) == TYPE_FLOAT:
|
|
|
|
|
dict[key] = int(dict[key])
|
|
|
|
|
|
|
|
|
|
#endregion
|