barebones prototype

This commit is contained in:
Tabby 2025-10-29 22:23:34 +11:00
parent 83f340ea01
commit b53d33584c
60 changed files with 3743 additions and 1 deletions

View file

@ -0,0 +1,211 @@
class_name ApplicationCommand
"""
Represents a Discord application command.
"""
enum COMMAND_TYPES {
__,
CHAT_INPUT,
USER,
MESSAGE
}
const _COMMAND_TYPES = {
1: 'CHAT_INPUT',
2: 'USER',
3: 'MESSAGE'
}
enum OPTION_TYPES {
__,
SUB_COMMAND,
SUB_COMMAND_GROUP,
STRING,
INTEGER,
BOOLEAN,
USER,
CHANNEL,
ROLE,
MENTIONABLE,
NUMBER
}
const _OPTION_TYPES = {
1: 'SUB_COMMAND',
2: 'SUB_COMMAND_GROUP',
3: 'STRING',
4: 'INTEGER',
5: 'BOOLEAN',
6: 'COMMAND',
7: 'CHANNEL',
8: 'ROLE',
9: 'MENTIONABLE',
10: 'NUMBER',
}
const _CHANNEL_TYPES = {
'GUILD_TEXT': 0,
'DM': 1,
'GUILD_VOICE': 2,
'GROUP_DM': 3,
'GUILD_CATEGORY': 4,
'GUILD_NEWS': 5,
'GUILD_STORE': 6,
'GUILD_NEWS_THREAD': 10,
'GUILD_PUBLIC_THREAD': 11,
'GUILD_PRIVATE_THREAD': 12,
'GUILD_STAGE_VOICE': 13
}
var id: String: get = get_id
var type: int = 1
var application_id: String: get = get_application_id
var guild_id: String: get = get_guild_id
var name: String: set = set_name, get = get_name
var description: String: set = set_description, get = get_description
var options: Array: set = set_options, get = get_options
var default_permission: bool = true
var version: String
func get_id() -> String:
return id
func set_type(p_type: String):
type = COMMAND_TYPES[p_type]
return self
func get_type():
return _OPTION_TYPES[type]
func get_application_id() -> String:
return application_id
func set_name(new_name: String):
name = new_name
return self
func get_name() -> String:
return name
func set_description(new_description: String):
description = new_description
return self
func get_description() -> String:
return description
func get_guild_id() -> String:
return guild_id
func set_options(new_options: Array):
options = new_options
return self
func get_options() -> Array:
return options
func add_option(option_data: Dictionary) -> ApplicationCommand:
# Generic method to add an option to the command
assert(option_data.has('type'), 'ApplicationCommand option must have a type')
assert(option_data.has('name') and Helpers.is_valid_str(option_data.name), 'ApplicationCommand option must have a name')
assert(option_data.has('description') and Helpers.is_valid_str(option_data.description), 'ApplicationCommand option must have a description')
options.append(option_data)
return self
static func sub_command_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.SUB_COMMAND, name, description, data)
static func sub_command_group_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.SUB_COMMAND_GROUP, name, description, data)
static func string_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.STRING, name, description, data)
static func integer_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.INTEGER, name, description, data)
static func boolean_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.BOOLEAN, name, description, data)
static func user_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.USER, name, description, data)
static func channel_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.CHANNEL, name, description, data)
static func role_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.ROLE, name, description, data)
static func mentionable_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.MENTIONABLE, name, description, data)
static func number_option(name: String, description: String, data: Dictionary = {}) -> Dictionary:
return _make_option(OPTION_TYPES.NUMBER, name, description, data)
static func choice(name: String, value) -> Dictionary:
return {
'name': name,
'value': value
}
static func _make_option(type: int, name: String, description: String, data: Dictionary = {}) -> Dictionary:
if data.has('channel_types'):
for i in range(len(data.channel_types)):
if _CHANNEL_TYPES.has(data.channel_types[i]):
data.channel_types[i] = _CHANNEL_TYPES[data.channel_types[i]]
return {
'type': type,
'name': name,
'description': description,
# Optional data
'required': data.required if data.has('required') else null,
'choices': data.choices if data.has('choices') else null,
'options': data.options if data.has('options') else null,
'channel_types': data.channel_types if data.has('channel_types') else null,
'min_value': data.min_value if data.has('min_value') else null,
'max_value': data.max_value if data.has('max_value') else null,
'autocomplete': data.autocomplete if data.has('autocomplete') else false
}
func _init(data: Dictionary = {}):
id = data.id if data.has('id') else ''
type = data.type if data.has('type') else 1
application_id = data.application_id if data.has('application_id') else ''
guild_id = data.guild_id if data.has('guild_id') else ''
name = data.name if data.has('name') else ''
description = data.description if data.has('description') else ''
options = data.options if data.has('options') else []
default_permission = data.default_permission if data.has('default_permission') else true
version = data.version if data.has('version') else ''
func _to_string(pretty: bool = false) -> String:
return JSON.stringify(_to_dict(), '\t') if pretty else JSON.stringify(_to_dict())
func print():
print(_to_string(true))
func _to_dict(is_register = false) -> Dictionary:
if is_register:
return {
'name': name,
'type': type,
'description': description,
'default_permission': default_permission,
'options': options
}
return {
'id': id,
'type': type,
'application_id': application_id,
'guild_id': '',
'name': name,
'description': description,
'options': options,
'default_permission': default_permission,
'version': version
}

View file

@ -0,0 +1 @@
uid://cpk5b2jequ6my

View file

@ -0,0 +1,126 @@
class_name BitField
"""
Helper class for bit operations.
"""
var default_bit = 0
var FLAGS = {}
var bitfield: int
func any(bit):
return (bitfield & resolve(bit)) != default_bit
func equals(bit):
return bitfield == resolve(bit)
func has(bit):
bit = resolve(bit)
return (bitfield & bit) == bit
func missing(bits):
pass
func add(bits):
if not typeof(bits) == TYPE_ARRAY:
bits = [bits]
var total = default_bit
for bit in bits:
total |= resolve(bit)
bitfield |= total
return self
func remove(bits):
if typeof(bits) == TYPE_OBJECT and bits.is_class(self.get_class()):
bits = bits.bitfield
if not typeof(bits) == TYPE_ARRAY:
bits = [bits]
var total = default_bit
for bit in bits:
total |= resolve(bit)
bitfield &= ~total
return self
func serialize():
var serialized = {}
var flags = FLAGS.keys()
var bits = FLAGS.values()
var i = 0
for flag in flags:
var bit = bits[i]
serialized[flag] = has(bit)
i += 1
return serialized
func to_array():
var ret = []
var flags = FLAGS.keys()
var bits = FLAGS.values()
var i = 0
for flag in flags:
var bit = bits[i]
if has(bit):
ret.append(flag)
i += 1
return ret
func resolve(bit):
if typeof(default_bit) == TYPE_INT or typeof(default_bit) == TYPE_FLOAT:
default_bit = int(default_bit)
if typeof(bit) == TYPE_INT or typeof(bit) == TYPE_FLOAT:
bit = int(bit)
if typeof(default_bit) == typeof(bit):
if bit >= default_bit:
return bit
if typeof(bit) == TYPE_OBJECT and bit.is_class(self.get_class()):
return bit.bitfield
if (typeof(bit) == TYPE_ARRAY):
var ret = default_bit
for b in bit:
ret = ret | resolve(b)
return ret
if (Helpers.is_valid_str(bit)):
if (FLAGS.has(bit)):
return FLAGS[bit]
if (not is_nan(float(bit))):
return int(bit)
assert(false, 'Bitfield is invalid.')
func _init(bits = default_bit):
if bits == null:
bits = default_bit
bitfield = resolve(bits)
func _to_dict():
if typeof(bitfield) == TYPE_INT:
return bitfield
else:
return str(bitfield)
func value_of():
return bitfield
func _to_string():
return str(bitfield)

View file

@ -0,0 +1 @@
uid://csgxh04sf3r75

View file

@ -0,0 +1,343 @@
class_name DiscordInteraction
"""
Represents a Discord interaction.
"""
var bot
var replied = false
var deferred = false
var ephemeral = false
# Compulsory
var id: String
var application_id: String
var type: String
var token: String
# Optional
var message: Message
var channel_id: String
var guild_id: String
var member: Dictionary
var data: Dictionary
var RESPONSE_TYPES = {
'CHANNEL_MESSAGE_WITH_SOURCE': 4,
'DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE': 5,
'DEFERRED_UPDATE_MESSAGE': 6,
'UPDATE_MESSAGE': 7,
'APPLICATION_COMMAND_AUTOCOMPLETE_RESULT': 8
}
var TYPES = {
1: 'PING',
2: 'APPLICATION_COMMAND',
3: 'MESSAGE_COMPONENT',
4: 'APPLICATION_COMMAND_AUTOCOMPLETE'
}
func is_command() -> bool:
return type == 'APPLICATION_COMMAND'
func is_autocomplete() -> bool:
return type == 'APPLICATION_COMMAND_AUTOCOMPLETE'
func is_message_component() -> bool:
return type == 'MESSAGE_COMPONENT'
func is_button() -> bool:
return is_message_component() and data.component_type == 2
func is_select_menu() -> bool:
return is_message_component() and data.component_type == 3
func in_guild() -> bool:
return guild_id != '' and member != {}
func respond_autocomplete(choices: Array):
var payload = {
'type': RESPONSE_TYPES['APPLICATION_COMMAND_AUTOCOMPLETE_RESULT'],
'data': {
'choices': choices
}
}
var res = await bot._send_request('/interactions/%s/%s/callback' % [id, token], payload)
return res
func fetch_reply(message_id: String = '@original'):
#assert(not ephemeral, 'Unable to fetch ephemeral Interaction reply.')
if ephemeral:
push_error('Unable to fetch ephemeral reply.')
return
var msg = await bot._send_get('/webhooks/%s/%s/messages/%s' % [application_id, token, message_id])
await bot._parse_message(msg)
return Message.new(msg)
func reply(options: Dictionary):
if replied or deferred:
push_error('Already replied to Interaction.')
return
options.type = RESPONSE_TYPES['CHANNEL_MESSAGE_WITH_SOURCE']
var res = await _send_request('/interactions/%s/%s/callback' % [id, token], options)
replied = true
return res
func edit_reply(options: Dictionary):
if (not replied) and (not deferred):
push_error('Unable to edit Interaction. Not replied.')
return
var res = await _edit_message('@original', options)
replied = true
return res
func delete_reply():
if ephemeral:
push_error('Unable to delete ephemeral Interaction reply.')
return
return await _delete_message()
func defer_reply(options: Dictionary = {}):
if replied or deferred:
push_error('Already replied to Interaction.')
return
options.type = RESPONSE_TYPES['DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE']
var res = await _send_request('/interactions/%s/%s/callback' % [id, token], options)
deferred = true
return res
func update(options: Dictionary):
if replied or deferred:
push_error('Already replied to Interaction.')
return
options.type = RESPONSE_TYPES['UPDATE_MESSAGE']
var msg = await _send_request('/interactions/%s/%s/callback' % [id, token], options)
replied = true
return msg
func defer_update(options: Dictionary = {}):
if replied or deferred:
push_error('Already replied to Interaction.')
return
options.type = RESPONSE_TYPES['DEFERRED_UPDATE_MESSAGE']
var res = await _send_request('/interactions/%s/%s/callback' % [id, token], options)
deferred = true
return res
func follow_up(options: Dictionary):
options.type = RESPONSE_TYPES['CHANNEL_MESSAGE_WITH_SOURCE']
var res = await _send_request(
'/webhooks/%s/%s' % [application_id, token], options, HTTPClient.METHOD_POST, true
)
return res
func edit_follow_up(msg: Message, options: Dictionary):
var res = await _edit_message(msg.id, options)
return res
func delete_follow_up(msg: Message):
var res = await _delete_message(msg.id)
return res
func has(attribute):
return true if self[attribute] else false
func _delete_message(message_id: String = '@original'):
var res = await bot._send_get(
'/webhooks/%s/%s/messages/%s' % [application_id, token, message_id],
HTTPClient.METHOD_DELETE
)
return res
func _edit_message(message_id: String, options: Dictionary):
options.type = RESPONSE_TYPES['CHANNEL_MESSAGE_WITH_SOURCE']
var msg = await _send_request(
'/webhooks/%s/%s/messages/%s' % [application_id, token, message_id],
options,
HTTPClient.METHOD_PATCH
)
return msg
func _send_request(
slug: String, options: Dictionary, method = HTTPClient.METHOD_POST, is_follow_up = false
):
var files = []
if options.has('files'):
files = options.files
options.erase('files')
var _type = options.type
options.erase('type')
options.attachments = message.attachments if message != null else []
if options.has('ephemeral') and typeof(options.ephemeral) == TYPE_BOOL:
ephemeral = options.ephemeral
options.erase('ephemeral')
var _fetch_reply = false
if options.has('fetch_reply'):
_fetch_reply = options.fetch_reply
options.erase('fetch_reply')
var _embeds = []
if options.has('embeds') and options.embeds.size() > 0:
for embed in options.embeds:
if typeof(embed) == TYPE_DICTIONARY:
_embeds.append(embed)
else:
_embeds.append(embed._to_dict())
var _components = []
if options.has('components') and options.components.size() > 0:
for component in options.components:
if typeof(component) == TYPE_DICTIONARY:
_components.append(component)
else:
_components.append(component._to_dict())
var payload = {
'type': _type,
'data':
{
'tts': options.tts if options.has('tts') else false,
'content': options.content if options.has('content') else null,
'embeds': _embeds,
'allowed_mentions': options.allowed_mentions if options.has('allowed_mentions') else {},
'attachments': options.attachments if options.has('attachments') else [],
'flags': MessageFlags.new('EPHEMERAL') if ephemeral else null,
'components': _components
}
}
if _type == RESPONSE_TYPES['UPDATE_MESSAGE']:
# Append the message parts from the original message if the options doesnt contain that part
if not options.has('tts'):
payload.data.tts = message.tts
if not options.has('content'):
payload.data.content = message.content
if not options.has('embeds'):
payload.data.embeds = message.embeds
if not options.has('components'):
payload.data.components = message.components
if method == HTTPClient.METHOD_PATCH or is_follow_up:
payload = payload.data
var res
var coroutine = await bot._send_raw_request(slug, {'payload': payload, 'files': files}, method)
if is_follow_up:
coroutine = await bot._parse_message(res)
return Message.new(res)
if _fetch_reply:
return await fetch_reply('@original')
else:
return true
func _init(_bot, interaction: Dictionary):
bot = _bot
assert(Helpers.is_valid_str(interaction.id), 'Interaction must have an id')
assert(
Helpers.is_valid_str(interaction.application_id), 'Interaction must have an application id'
)
assert(Helpers.is_valid_str(interaction.token), 'Interaction must have a token')
assert(interaction.has('type'), 'Interaction must have a type')
assert(Helpers.is_num(interaction.version), 'Interaction must have a version')
id = interaction.id
application_id = interaction.application_id
token = interaction.token
type = TYPES[int(interaction.type)]
if interaction.has('member'):
member = interaction.member
# Try to parse the member permissions
if member.has('permissions'):
member.permissions = Permissions.new(member.permissions)
# Try to parse the member user
if member.has('user'):
member.user = User.new(bot, member.user)
if interaction.has('guild_id'):
guild_id = interaction.guild_id
if interaction.has('channel_id'):
channel_id = interaction.channel_id
if interaction.has('data'):
data = interaction.data
if type == 'APPLICATION_COMMAND':
data.type = ApplicationCommand._COMMAND_TYPES[int(data.type)]
data = _parse_data_options(interaction.data)
if interaction.has('message'):
await bot._parse_message(interaction.message)
message = Message.new(interaction.message)
func _parse_data_options(data, option = false):
if option and data.has('type'):
data.type = ApplicationCommand._OPTION_TYPES[int(data.type)]
if data.has('options'):
for i in range(len(data.options)):
data.options[i] = _parse_data_options(data.options[i], true)
return data
func _to_string(pretty: bool = false) -> String:
return JSON.stringify(_to_dict(), '\t') if pretty else JSON.stringify(_to_dict())
func print():
print(_to_string(true))
func _to_dict() -> Dictionary:
return {
'version': 1,
'type': type,
'token': token,
'message': message._to_string() if message is Message else {},
'member': member,
'id': id,
'guild_id': guild_id,
'data': data,
'channel_id': channel_id,
'application_id': application_id,
}

View file

@ -0,0 +1 @@
uid://dklo5j3tsw3qa

View file

@ -0,0 +1,237 @@
class_name Embed
"""
Stores data about a Discord Embed
and has functions to add, modify and edit
the various properties of an Embed.
"""
var title: String: set = set_title, get = get_title
var type: String = 'rich': set = set_type, get = get_type
var description: String: set = set_description, get = get_description
var url: String: set = set_url, get = get_url
var timestamp: String: set = set_timestamp, get = get_timestamp
var color: set = set_color, get = get_color
var footer = null
var image = null
var thumbnail = null
var video = null
var provider = null
var author = null
var fields: Array
func get_title():
return title if Helpers.is_valid_str(title) else null
func get_type():
return type if Helpers.is_valid_str(type) else null
func get_description():
return description if Helpers.is_valid_str(description) else null
func get_url():
return url if Helpers.is_valid_str(url) else null
func get_timestamp():
return timestamp if Helpers.is_valid_str(timestamp) else null
func get_color():
return color
func set_title(_title):
assert(Helpers.is_valid_str(_title), 'Invalid Type: title of Embed must be a String')
assert(_title.length() <= 256, 'title of Embed must be <= 256 characters')
title = _title
return self
func set_type(_type):
assert(Helpers.is_valid_str(_type), 'Invalid Type: type of Embed must be a String')
type = _type
return self
func set_description(_description):
assert(
Helpers.is_valid_str(_description), 'Invalid Type: description of Embed must be a String'
)
assert(_description.length() <= 4096, 'Embed description must be <= 4096 characters')
description = _description
return self
func set_url(_url):
assert(Helpers.is_valid_str(_url), 'Invalid Type: url of Embed must be a String')
url = _url
return self
func set_timestamp(_timestamp = ''):
timestamp = Helpers.make_iso_string()
return self
func set_color(_color):
# RBG color
if typeof(_color) == TYPE_ARRAY:
color = (int(_color[0]) * 256 * 256) + (int(_color[1]) * 256) + int(_color[2])
# Hex color
elif typeof(_color) == TYPE_STRING and _color.begins_with('#'):
color = _color.replace('#', '0x').hex_to_int()
# Decimal color
elif _color.is_valid_integer:
color = int(_color)
return self
func set_footer(text: String, icon_url: String = '', proxy_icon_url: String = ''):
assert(Helpers.is_valid_str(text), 'Invalid Type: footer text of Embed must be a valid String')
assert(text.length() <= 2048, 'Embed footer text must be <= 2048 characters')
footer = {'text': text, 'icon_url': icon_url, 'proxy_icon_url': proxy_icon_url}
return self
func set_image(url: String, width: int = -1, height: int = -1, proxy_url: String = ''):
assert(Helpers.is_valid_str(url), 'Invalid Type: image url of Embed must be a valid String')
image = {
'url': url,
'width': width if width != -1 else null,
'height': height if height != -1 else null,
'proxy_url': proxy_url if Helpers.is_valid_str(proxy_url) else null
}
return self
func set_thumbnail(url: String, width: int = -1, height: int = -1, proxy_url: String = ''):
assert(Helpers.is_valid_str(url), 'Embed thumbnail url must be a valid String')
thumbnail = {
'url': url,
'width': width if width != -1 else null,
'height': height if height != -1 else null,
'proxy_url': proxy_url if Helpers.is_valid_str(proxy_url) else null
}
return self
func set_video(url: String, width: int = -1, height: int = -1, proxy_url: String = ''):
assert(Helpers.is_valid_str(url), 'Invalid Type: video url of Embed must be a valid String')
video = {
'url': url,
'width': width if width != -1 else null,
'height': height if height != -1 else null,
'proxy_url': proxy_url if Helpers.is_valid_str(proxy_url) else null
}
return self
func set_provider(name: String, url: String = ''):
assert(
Helpers.is_valid_str(name), 'Invalid Type: provider name of Embed must be a valid String'
)
provider = {'name': name, 'url': url if Helpers.is_valid_str(url) else null}
return self
func set_author(
name: String, url: String = '', icon_url: String = '', proxy_icon_url: String = ''
):
assert(Helpers.is_valid_str(name), 'Invalid Type: author name of Embed must be a valid String')
assert(name.length() <= 256, 'Embed author name must be <= 256 characters')
author = {
'name': name,
'url': url if Helpers.is_valid_str(url) else null,
'icon_url': icon_url if Helpers.is_valid_str(icon_url) else null,
'proxy_icon_url': proxy_icon_url if Helpers.is_valid_str(proxy_icon_url) else null
}
return self
func add_field(name: String, value: String, inline: bool = false, index = -1):
assert(Helpers.is_valid_str(name), 'Invalid Type: field name of Embed must be a valid String')
assert(Helpers.is_valid_str(value), 'Invalid Type: field value of Embed must be a valid String')
assert(name.length() <= 256, 'Embed field name must be <= 256 characters')
assert(value.length() <= 1024, 'Embed field value must be <= 1024 characters')
assert(fields.size() <= 25, 'Embed can have a max of 25 fields')
var new_field = {'name': name, 'value': value, 'inline': inline}
if index == -1:
fields.append(new_field)
else:
fields.insert(index, new_field)
return self
func slice_fields(index: int, delete_count: int = 1, replace_fields: Array = []):
var n = fields.size()
assert(Helpers.is_num(index), 'Missing index must be provided to Embed.slice_fields')
assert(index > -1 and index < n, 'index out of bounds in Embed.slice_fields')
var max_deletable = n - index
assert(delete_count <= max_deletable, 'delete_count out of bounds in Embed.slice_fields')
while delete_count != 0:
fields.remove_at(index)
delete_count -= 1
if replace_fields.size() != 0:
# add fields
for field in replace_fields:
var inline = false
if field.size() == 3:
inline = field[2]
add_field(field[0], field[1], inline, index)
index += 1
return self
func _to_string(pretty: bool = false) -> String:
return JSON.stringify(_to_dict(), '\t') if pretty else JSON.stringify(_to_dict())
func print():
print(_to_string(true))
func _to_dict() -> Dictionary:
var total = title + description
if footer and footer.text:
total += footer.text
if author and author.name:
total += author.name
for field in fields:
total += field.name
total += field.value
total = str(total).length()
assert(total <= 6000, 'Embed content must be <= 6000 characters in total')
return {
'title': title,
'type': type,
'description': description,
'url': url,
'timestamp': timestamp,
'color': color,
'footer': footer,
'image': image,
'thumbnail': thumbnail,
'video': video,
'provider': provider,
'author': author,
'fields': fields
}

View file

@ -0,0 +1 @@
uid://cx0cqy20w5ygt

View file

@ -0,0 +1,75 @@
class_name Helpers
"""
General purpose Helpers functions
used by discord.gd plugin
"""
# Returns true if value if an int or real float
static func is_num(value) -> bool:
return typeof(value) == TYPE_INT or typeof(value) == TYPE_FLOAT
# Returns true if value is a string
static func is_str(value) -> bool:
return typeof(value) == TYPE_STRING
# Returns true if the string has more than 1 character
static func is_valid_str(value) -> bool:
return is_str(value) and value.length() > 0
# Return a ISO 8601 timestamp as a String
static func make_iso_string(datetime: Dictionary = Time.get_datetime_dict_from_system(true)) -> String:
var iso_string = '%s-%02d-%02dT%02d:%02d:%02d' % [datetime.year, datetime.month, datetime.day, datetime.hour, datetime.minute, datetime.second]
return iso_string
# Pretty prints a Dictionary
static func print_dict(d: Dictionary) -> void:
print(JSON.stringify(d, '\t'))
# Saves a Dictionary to a file for debugging large dictionaries
static func save_dict(d: Dictionary, filename = 'saved_dict') -> void:
assert(typeof(d) == TYPE_DICTIONARY, 'type of d is not Dictionary in save_dict')
var file = FileAccess.open('user://%s%s.json' % [filename, str(Time.get_ticks_msec())], FileAccess.WRITE)
file.store_string(JSON.stringify(d, '\t'))
file.close()
print('Dictionary saved to file')
# Converts a raw image bytes to a png Image
static func to_png_image(bytes: PackedByteArray) -> Image:
var image = Image.new()
image.load_png_from_buffer(bytes)
return image
# Converts a Image to ImageTexture
static func to_image_texture(image: Image) -> ImageTexture:
var texture = ImageTexture.new()
texture.create_from_image(image)
return texture
# Ensures that the String's length is less than or equal to the specified length
static func assert_length(variable: String, length: int, msg: String):
assert(variable.length() <= length, msg)
# Convert the ISO string to a unix timestamp
static func iso2unix(iso_string: String) -> int:
var date := iso_string.split("T")[0].split("-")
var time := iso_string.split("T")[1].trim_suffix("Z").split(":")
var datetime = {
year = date[0],
month = date[1],
day = date[2],
hour = time[0],
minute = time[1],
second = time[2],
}
return Time.get_unix_time_from_datetime_dict(datetime)

View file

@ -0,0 +1 @@
uid://q15tb8w4uum

View file

@ -0,0 +1,190 @@
class_name Message
"""
Represents a Discord Message.
"""
var id: String
var channel_id: String
var guild_id: String
var content: String
var timestamp: String
var edited_timestamp: String
var webhook_id: String
var type: String
var author: User
var member: Dictionary
var activity: Dictionary
var message_reference: Dictionary
var referenced_message: Dictionary
var tts: bool
var mention_everyone: bool
var pinned: bool
var mentions: Array
var mention_roles: Array
var mention_channels: Array
var attachments: Array
var components: Array
var embeds: Array
var reactions: Array
var flags: MessageFlags
var nonce
var MESSAGE_TYPES = {
'0': 'DEFAULT',
'1': 'RECIPIENT_ADD',
'2': 'RECIPIENT_REMOVE',
'3': 'CALL',
'4': 'CHANNEL_NAME_CHANGE',
'5': 'CHANNEL_ICON_CHANGE',
'6': 'CHANNEL_PINNED_MESSAGE',
'7': 'GUILD_MEMBER_JOIN',
'8': 'USER_PREMIUM_GUILD_SUBSCRIPTION',
'9': 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1',
'10': 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2',
'11': 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3',
'12': 'CHANNEL_FOLLOW_ADD',
'14': 'GUILD_DISCOVERY_DISQUALIFIED',
'15': 'GUILD_DISCOVERY_REQUALIFIED',
'16': 'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING',
'17': 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING',
'18': 'THREAD_CREATED',
'19': 'REPLY',
'20': 'APPLICATION_COMMAND',
'21': 'THREAD_STARTER_MESSAGE',
'22': 'GUILD_INVITE_REMINDER'
}
func _init(message: Dictionary):
# Compulsory
assert(typeof(message) == TYPE_DICTIONARY, 'Invalid type: message must be a Dictionary')
assert(message.id, 'Message must have an id')
id = message.id
assert(message.has('type'), 'Message must have a type')
assert(message.has('channel_id') and message.channel_id and Helpers.is_valid_str(message.channel_id), 'Message must have a valid channel_id')
channel_id = message.channel_id
if MESSAGE_TYPES.get(str(int(message.type))):
type = MESSAGE_TYPES.get(str(int(message.type)))
else:
assert(false, 'Message must have a valid type')
assert(message.has('author'), 'Message must have an author')
# Check if the message is sent by webhook
if message.has('webhook_id') and Helpers.is_str(message.webhook_id) and message.webhook_id.length() > 0:
# webhook sent a message
pass
else:
# sent by user
assert(message.author is User, 'author attribute of Mesage must be of type User')
author = message.author
if message.has('flags'):
flags = MessageFlags.new(message.flags)
# if message.channel.type != 'DM':
# assert(message.has('guild_id') and message.guild_id and Helpers.is_valid_str(message.guild_id), 'Message must have a valid guild_id')
# guild_id = message.guild_id
if message.has('guild_id') and message.guild_id:
guild_id = message.guild_id
#if not message.has('webhook_id'):
#assert(message.content.length() > 0 or message.embeds.size() > 0 or message.components.size() > 0 or message.attachments.size() > 0, 'Message must have a content or at least one of (embeds, components or attachments)')
content = message.content
assert(message.timestamp, 'Message must have a timestamp')
timestamp = message.timestamp
# Optional
if message.has('edited_timestamp') and message.edited_timestamp != null:
edited_timestamp = message.edited_timestamp
if message.has('tts'):
tts = true if message.tts else false
if message.has('mention_everyone'):
mention_everyone = true if message.mention_everyone else false
if message.has('mentions') and typeof(message.mentions) == TYPE_ARRAY and message.mentions.size() > 0:
mentions = message.mentions
if message.has('member') and message.member:
member = message.member
if message.has('mention_roles') and message.mention_roles:
mention_roles = message.mention_roles
if message.has('mention_channels') and message.mention_channels:
mention_channels = message.mention_channels
if message.has('attachments') and message.attachments:
attachments = message.attachments
if message.has('components') and message.components:
components = message.components
if message.has('embeds') and message.embeds:
embeds = message.embeds
if message.has('reactions') and message.reactions:
reactions = message.reactions
if message.has('pinned') and typeof(message.pinned) == TYPE_BOOL:
pinned = message.pinned
if message.has('message_reference') and message.message_reference:
message_reference = message.message_reference
if message.has('referenced_message') and message.referenced_message:
referenced_message = message.referenced_message
func _to_string(pretty: bool = false):
var data = {
'id': id,
'channel_id': channel_id,
'guild_id': guild_id,
'author': author,
'member': member,
'content': content,
'timestamp': timestamp,
'edited_timestamp': edited_timestamp,
'tts': tts,
'mention_everyone': mention_everyone,
'mentions': mentions,
'mention_roles': mention_roles,
'mention_channels': mention_channels,
'attachments': attachments,
'components': components,
'embeds': embeds,
'reactions': reactions,
'pinned': pinned,
'type': type,
'message_reference': message_reference,
'referenced_message': referenced_message,
'flags': flags.bitfield
}
return JSON.stringify(data, '\t') if pretty else JSON.stringify(data)
func print():
print(_to_string(true))
func has(attribute):
return true if self[attribute] else false
func slice_attachments(index: int, delete_count: int = 1, replace_attachments: Array = []):
var n = attachments.size()
assert(Helpers.is_num(index), 'index must be provided to Message.slice_attachments')
assert(index > -1 and index < n, 'index out of bounds in Message.slice_attachments')
var max_deletable = n - index
assert(delete_count <= max_deletable, 'delete_count out of bounds in Message.attachments')
while delete_count != 0:
attachments.remove_at(index)
delete_count -= 1
if replace_attachments.size() > 0:
for attachment in replace_attachments:
assert(attachment.has('id') and Helpers.is_valid_str(attachment.id), 'Missing id for attachment in replace_attachments in slice_attachments')
attachments.append_array(replace_attachments)
return self

View file

@ -0,0 +1 @@
uid://dtoffh2nlc7di

View file

@ -0,0 +1,68 @@
class_name MessageActionRow
"""
Represnts a Discord message action row which has components
"""
var components: Array
func add_component(component, index = -1):
assert(components.size() + 1 <= 5, 'MessageActionRow cannot have more than 5 components.')
assert(component.type != 1, 'MessageActionRow cannot contain another MessageActionRow.')
var same_custom_id = false
for _component in components:
if Helpers.is_valid_str(_component.get_custom_id()):
if _component.get_custom_id() == component.get_custom_id():
same_custom_id = true
break
assert(same_custom_id == false, 'MessageActionRow must contain components with unique custom_id')
if index == -1:
components.append(component)
else:
components.insert(index, component)
return self
func slice_components(index: int, delete_count: int = 1, replace_components: Array = []):
var n = components.size()
assert(Helpers.is_num(index), 'index must be provided to MessageActionRow.slice_components')
assert(index > -1 and index < n, 'index out of bounds in MessageActionRow.slice_components')
var max_deletable = n - index
assert(delete_count <= max_deletable, 'delete_count out of bounds in MessageActionRow.slice_components')
while delete_count != 0:
components.remove_at(index)
delete_count -= 1
if replace_components.size() != 0:
# add components
for component in replace_components:
add_component(component, index)
index += 1
return self
func _to_string(pretty: bool = false) -> String:
return JSON.stringify(_to_dict(), '\t') if pretty else JSON.stringify(_to_dict())
func print():
print(_to_string(true))
func _to_dict() -> Dictionary:
assert(components.size() <= 5, 'MessageActionRow cannot have more than 5 components.')
var _components = []
for component in components:
_components.append(component._to_dict())
return {
'type': 1,
'components': _components
}

View file

@ -0,0 +1 @@
uid://dr83pxgdkl46x

View file

@ -0,0 +1,101 @@
class_name MessageButton
"""
Represents a Discord message button.
"""
var _STYLES = {0: 'DEFAULT', 1: 'PRIMARY', 2: 'SECONDARY', 3: 'SUCCESS', 4: 'DANGER', 5: 'LINK'}
enum STYLES { DEFAULT, PRIMARY, SECONDARY, SUCCESS, DANGER, LINK }
var label: String: set = set_label, get = get_label
var custom_id: String: set = set_custom_id, get = get_custom_id
var url: String: set = set_url, get = get_url
var disabled: bool = false: set = set_disabled, get = get_disabled
var emoji: Dictionary
var _style: set = set_style
var type: int = 2
func set_style(style_number: int):
_style = style_number
return self
func get_style_string():
return _STYLES[_style]
func set_label(new_label: String):
assert(new_label.length() <= 80, 'label of MessageButton must be max 80 characters.')
label = new_label
return self
func get_label() -> String:
return label
func set_custom_id(new_custom_id):
assert(new_custom_id.length() <= 80, 'custom_id of MessageButton must be max 100 characters.')
custom_id = new_custom_id
return self
func get_custom_id() -> String:
return custom_id
func set_url(new_url):
url = new_url
return self
func get_url() -> String:
return url
func set_disabled(new_value: bool):
disabled = new_value
return self
func get_disabled() -> bool:
return disabled
func set_emoji(new_emoji: Dictionary):
emoji = new_emoji
return self
func get_emoji() -> Dictionary:
return emoji
func _to_string(pretty: bool = false) -> String:
return JSON.stringify(_to_dict(), '\t') if pretty else JSON.stringify(_to_dict())
func print():
print(_to_string(true))
func _to_dict() -> Dictionary:
# Default style is primary
if _style == 0:
_style = 1
if _style == STYLES.LINK:
# Must have a url
assert(Helpers.is_valid_str(url), 'A LINK MessageButton must have a url.')
return {'type': type, 'style': _style, 'label': label, 'url': url, 'disabled': disabled}
else:
assert(Helpers.is_valid_str(custom_id), 'A button must have a custom_id.')
return {
'type': type,
'style': _style,
'label': label,
'custom_id': custom_id,
'disabled': disabled,
'emoji': emoji if emoji else null
}

View file

@ -0,0 +1 @@
uid://c2mdufrdsqisx

View file

@ -0,0 +1,28 @@
class_name MessageFlags extends BitField
"""
Represents a bitfield of Discord message flags.
"""
func _init(bits = default_bit):
default_bit = 0
if bits == null:
bits = default_bit
FLAGS = {
'CROSSPOSTED': 1 << 0,
'IS_CROSSPOST': 1 << 1,
'SUPPRESS_EMBEDS': 1 << 2,
'SOURCE_MESSAGE_DELETED': 1 << 3,
'URGENT': 1 << 4,
'HAS_THREAD': 1 << 5,
'EPHEMERAL': 1 << 6,
'LOADING': 1 << 7,
}
bitfield = resolve(bits)
func missing(bits):
var BF = load('res://addons/discord_gd/classes/message_flags.gd')
return BF.new(bits).remove(self).to_array()

View file

@ -0,0 +1 @@
uid://dbkf1n3hfcdj8

View file

@ -0,0 +1,67 @@
class_name Permissions extends BitField
"""
Represents a bitfield of Discord permissions.
"""
var ALL
const DEFAULT = 104324673
func _init(bits = default_bit):
default_bit = 0
if bits == null:
bits = default_bit
FLAGS = {
'CREATE_INSTANT_INVITE': 1 << 0,
'KICK_MEMBERS': 1 << 1,
'BAN_MEMBERS': 1 << 2,
'ADMINISTRATOR': 1 << 3,
'MANAGE_CHANNELS': 1 << 4,
'MANAGE_GUILD': 1 << 5,
'ADD_REACTIONS': 1 << 6,
'VIEW_AUDIT_LOG': 1 << 7,
'PRIORITY_SPEAKER': 1 << 8,
'STREAM': 1 << 9,
'VIEW_CHANNEL': 1 << 10,
'SEND_MESSAGES': 1 << 11,
'SEND_TTS_MESSAGES': 1 << 12,
'MANAGE_MESSAGES': 1 << 13,
'EMBED_LINKS': 1 << 14,
'ATTACH_FILES': 1 << 15,
'READ_MESSAGE_HISTORY': 1 << 16,
'MENTION_EVERYONE': 1 << 17,
'USE_EXTERNAL_EMOJIS': 1 << 18,
'VIEW_GUILD_INSIGHTS': 1 << 19,
'CONNECT': 1 << 20,
'SPEAK': 1 << 21,
'MUTE_MEMBERS': 1 << 22,
'DEAFEN_MEMBERS': 1 << 23,
'MOVE_MEMBERS': 1 << 24,
'USE_VAD': 1 << 25,
'CHANGE_NICKNAME': 1 << 26,
'MANAGE_NICKNAMES': 1 << 27,
'MANAGE_ROLES': 1 << 28,
'MANAGE_WEBHOOKS': 1 << 29,
'MANAGE_EMOJIS_AND_STICKERS': 1 << 30,
'USE_APPLICATION_COMMANDS': 1 << 31,
'REQUEST_TO_SPEAK': 1 << 32,
'MANAGE_THREADS': 1 << 34,
'USE_PUBLIC_THREADS': 1 << 35,
'USE_PRIVATE_THREADS': 1 << 36,
'USE_EXTERNAL_STICKERS': 1 << 37,
}
bitfield = resolve(bits)
var values = FLAGS.values()
var prev = default_bit
for value in values:
prev |= value
ALL = prev
func missing(bits):
var BF = load('res://addons/discord_gd/classes/permissions.gd')
return BF.new(bits).remove(self).to_array()

View file

@ -0,0 +1 @@
uid://pov5klkwcdw3

View file

@ -0,0 +1,124 @@
class_name SelectMenu
"""
Represents a Discord select menu.
"""
var custom_id: String: set = set_custom_id, get = get_custom_id
var placeholder: String: set = set_placeholder, get = get_placeholder
var options: Array: set = set_options, get = get_options
var min_values = 1: set = set_min_values, get = get_min_values
var max_values = 1: set = set_max_values, get = get_max_values
var disabled: bool = false: set = set_disabled, get = get_disabled
var type: int = 3
func set_custom_id(new_custom_id):
Helpers.assert_length(new_custom_id, 100, 'custom_id of SelectMenu cannot be more than 100 characters.')
custom_id = new_custom_id
return self
func get_custom_id() -> String:
return custom_id
func add_option(value: String, label: String, data: Dictionary = {}):
assert(options.size() <= 25, 'options of SelectMenu cannot have more than 25 options')
assert(Helpers.is_valid_str(value), 'value of SelectMenu option must be a valid String')
Helpers.assert_length(value, 100, 'value of SelectMenu option cannot be more than 100 characters')
assert(Helpers.is_valid_str(label), 'SelectMenu option must have a label')
Helpers.assert_length(label, 100, 'label of SelectMenu option cannot be more than 100 characters')
# Parse data
#{description: "", emoji: {}, default = false}
var _data = {
'value': value,
'label': label
}
if data.has('description'):
assert(typeof(data.description) == TYPE_STRING, 'description of SelectMenu option must be a String')
Helpers.assert_length(data.description, 100, 'description of SelectMenu cannot be more than 100 characters')
_data['description'] = data.description
if data.has('emoji'):
assert(typeof(data.emoji) == TYPE_DICTIONARY, 'emoji of SelectMenu option must be a Dictionary')
_data['emoji'] = data.emoji
if data.has('default'):
assert(typeof(data.default) == TYPE_BOOL, 'default of SelectMenu option must be a bool')
_data['default'] = data.default
options.append(_data)
return self
func set_options(new_options: Array):
options = new_options
return self
func get_options() -> Array:
return options
func set_placeholder(new_placeholder: String):
Helpers.assert_length(new_placeholder, 100, 'placeholder of SelectMenu cannot be more than 100 characters.')
placeholder = new_placeholder
return self
func get_placeholder() -> String:
return placeholder
func set_min_values(new_min_values: int):
assert(new_min_values <= 25, 'min_values of SelectMenu cannot be more than 25')
min_values = new_min_values
return self
func get_min_values() -> int:
return min_values
func set_max_values(new_max_values: int):
assert(new_max_values <= 25, 'max_values of SelectMenu cannot be more than 25')
max_values = new_max_values
return self
func get_max_values() -> int:
return max_values
func set_disabled(new_value: bool):
disabled = new_value
return self
func get_disabled() -> bool:
return disabled
func _to_string(pretty: bool = false) -> String:
return JSON.stringify(_to_dict(), '\t') if pretty else JSON.stringify(_to_dict())
func print():
print(_to_string(true))
func _to_dict() -> Dictionary:
# Default style is primary
assert(Helpers.is_valid_str(custom_id), 'A button must have a custom_id.')
return {
'type': type,
'custom_id': custom_id,
'options': options,
'placeholder': placeholder,
'min_values': min_values,
'max_values': max_values,
'disabled': disabled,
}

View file

@ -0,0 +1 @@
uid://v5s48xly81hk

View file

@ -0,0 +1,156 @@
class_name User
"""
Represents a Discord User.
"""
var id: String
var username: String
var discriminator: String
var avatar: String
# Optional
var bot: bool
var system: bool
var mfa_enabled: bool
var locale: String
var verified: bool
var email: String
var flags: int
var premium_type: int
var public_flags: int
var client
const AVATAR_URL_FORMATS = ['webp', 'png', 'jpg', 'jpeg', 'gif']
const AVATAR_URL_SIZES = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
func get_display_avatar_url(options: Dictionary = {}) -> String:
"""
options {
format: String, one of webp, png, jpg, jpeg, gif (default png),
size: int, one of 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 (default 256),
dynamic: bool, if true the format will automatically change to gif for animated avatars (default false)
}
"""
if options.has('format'):
assert(options.format in AVATAR_URL_FORMATS, 'Invalid avatar_url provided to get_display_avatar')
else:
options.format = 'png'
if options.has('size'):
assert(int(options.size) in AVATAR_URL_SIZES, 'Invalid size provided to get_display_avatar')
else:
options.size = 256
if options.has('dynamic'):
assert(typeof(options.dynamic) == TYPE_BOOL, 'dynamic attribute must be of type bool in get_display_avatar')
if Helpers.is_valid_str(avatar) and avatar.begins_with('a_'):
options.format = 'gif'
else:
options.dynamic = false
if not Helpers.is_valid_str(avatar):
return get_default_avatar_url()
return client._cdn_base + '/avatars/%s/%s.%s?size=%s' % [id, avatar, options.format, options.size]
func get_default_avatar_url() -> String:
var moduloed_discriminator = int(discriminator) % 5
return client._cdn_base + '/embed/avatars/%s.png' % moduloed_discriminator
func get_display_avatar(options: Dictionary = {}) -> PackedByteArray:
var png_bytes = await client._send_get_cdn(get_display_avatar_url(options))
return png_bytes
func get_default_avatar() -> PackedByteArray:
var png_bytes = await client._send_get_cdn(get_default_avatar_url())
return png_bytes
func _init(_client, user):
client = _client
# Compulsory
assert(user.has('id'), 'User must have an id')
assert(user.has('username'), 'User must have a username')
assert(user.has('discriminator'), 'User must have a discriminator')
id = user.id
username = user.username
discriminator = user.discriminator
if user.avatar:
avatar = user.avatar
# Optional
if user.has('bot') and user.bot != null:
assert(typeof(user.bot) == TYPE_BOOL, 'bot attribute of User must be bool')
bot = user.bot
else:
bot = false
if user.has('system') and user.system != null:
assert(typeof(user.system) == TYPE_BOOL, 'system attribute of User must be bool')
system = user.system
else:
system = false
if user.has('mfa_enabled') and user.mfa_enabled != null:
assert(typeof(user.mfa_enabled) == TYPE_BOOL, 'mfa_enabled attribute of User must be bool')
mfa_enabled = user.mfa_enabled
else:
mfa_enabled = false
if user.has('verified') and user.verified != null:
assert(typeof(user.verified) == TYPE_BOOL, 'verified attribute of User must be bool')
verified = user.verified
else:
verified = false
if user.has('locale') and user.locale != null:
assert(typeof(user.locale) == TYPE_STRING, 'locale attribute of User must be String')
locale = user.locale
if user.has('email') and user.email != null:
assert(typeof(user.email) == TYPE_STRING, 'email attribute of User must be String')
email = user.email
if user.has('flags') and user.flags != null:
assert(Helpers.is_num(user.flags), 'flags attribute of User must be int')
flags = user.flags
if user.has('premium_type') and user.premium_type != null:
assert(Helpers.is_num(user.premium_type), 'premium_type attribute of User must be int')
premium_type = user.premium_type
if user.has('public_flags') and user.public_flags != null:
assert(Helpers.is_num(user.public_flags), 'public_flags attribute of User must be int')
public_flags = user.public_flags
func _to_string(pretty: bool = false):
var data = {
'id': id,
'username': username,
'discriminator': discriminator,
'avatar': avatar,
'bot': bot,
'system': system,
'mfa_enabled': mfa_enabled,
'locale': locale,
'verified': verified,
'email': email,
'flags': flags,
'premium_type': premium_type,
'public_flags': public_flags
}
return JSON.stringify(data, '\t') if pretty else JSON.stringify(data)
func print():
print(_to_string(true))

View file

@ -0,0 +1 @@
uid://1uaxjy7fw8f2