diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f28239b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+charset = utf-8
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8ad74f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0af181c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Godot 4+ specific ignores
+.godot/
+/android/
diff --git a/Library.tscn b/Library.tscn
new file mode 100644
index 0000000..79abcde
--- /dev/null
+++ b/Library.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://1oww0utk77w7"]
+
+[ext_resource type="Script" uid="uid://hkh1ewsuji8m" path="res://library.gd" id="1_gqcys"]
+
+[node name="Library" type="Node"]
+script = ExtResource("1_gqcys")
diff --git a/Main.tscn b/Main.tscn
new file mode 100644
index 0000000..227d678
--- /dev/null
+++ b/Main.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://cd60nfxe4lnq1"]
+
+[ext_resource type="Script" uid="uid://cus8nh0g3yyj2" path="res://main.gd" id="1_glv2v"]
+
+[node name="Main" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_glv2v")
diff --git a/README.md b/README.md
index 6e732b6..766f804 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# FabsocBot
\ No newline at end of file
+# FabsocBot
diff --git a/addons/discord_gd/LICENSE.md b/addons/discord_gd/LICENSE.md
new file mode 100644
index 0000000..f67213a
--- /dev/null
+++ b/addons/discord_gd/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-present Delano Lourenco
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/addons/discord_gd/README.md b/addons/discord_gd/README.md
new file mode 100644
index 0000000..0824885
--- /dev/null
+++ b/addons/discord_gd/README.md
@@ -0,0 +1,111 @@
+Discord.gd
+=========================================
+###### (Get it from Godot Asset Library - https://godotengine.org/asset-library/asset/1010)
+
+
+### A Godot plugin to interact with the Discord Bot API. Make Discord Bots in Godot!
+
+> 100% GDScript
+
+
+
+
+
+
+
+#### Godot version compatibility
+
+- Godot 4.x - [main branch](https://github.com/3ddelano/discord.gd/tree/main)
+- Godot 3.x - [godot3 branch](https://github.com/3ddelano/discord.gd/tree/godot3)
+
+Features
+--------------
+
+- Make a Discord Bot in less than 10 lines of code
+- Supports `Buttons` and `SelectMenus`
+- Supports `Application Commands` aka `Slash Commands`
+- Uses Godot signals to emit events like `bot_ready`, `guild_create`, `message_create`, `message_delete`, etc.
+- Get User Avatar and Guild Icon as Godot's `ImageTexture`
+- Uses coroutine async functions i.e Promises
+
+
+## [🚀 Check out out GDAI MCP from the creator of Discord.gd](https://gdaimcp.com?ref=discordgd-readme)
+
+
+
+
+Supercharge your Godot 4.2+ workflow with GDAI MCP – the ultimate Godot MCP server that lets AI tools like Claude, Cursor, Windsurf, VSCode and more automate scene creation, node editing, reading godot errors, creating scripts, debugging, and more.
+
+Vibe code like never before!
+
+### 🔗 **[https://gdaimcp.com](https://gdaimcp.com?ref=discordgd-readme)**
+
+
+Installation
+--------------
+
+This is a regular plugin for Godot.
+Copy the contents of `addons/discord_gd` into the `addons/` folder in the same directory as your project, and activate it in your project settings.
+
+The plugin now comes with no extra assets to stay lightweight.
+If you want to try an example scene, you can see the examples from: [Discord.gd Examples](https://github.com/3ddelano/discord_gd_examples)
+
+> For in-depth installation instructions check the [Installation Wiki](https://3ddelano.github.io/discord.gd/installation)
+
+> Note: You will need a valid Discord Bot token available at [Discord Applications](https://discord.com/developers/applications)
+
+
+Getting Started
+----------
+
+1. After activating the plugin. There will be a new `DiscordBot` node added to Godot.
+Click on any node in the scene tree of your scene for example `Root` and add the `DiscordBot` node as a child.
+
+2. Connect the various signals (`bot_ready`, `guild_create`, `message_create`, `message_delete`, etc) of the `DiscordBot` node to the parent node, either through the editor or in the script using the `connect()` method.
+
+3. Attach a script to the `Root` node.
+
+```GDScript
+extends Node2D
+
+func _ready():
+ var discord_bot = $DiscordBot
+ discord_bot.TOKEN = "your_bot_token_here"
+ discord_bot.login()
+ discord_bot.bot_ready.connect(_on_DiscordBot_bot_ready)
+ discord_bot.message_create.connect(_on_DiscordBot_message_create)
+
+func _on_DiscordBot_bot_ready(bot: DiscordBot):
+ print("Logged in as %s#%s" % [bot.user.username, bot.user.discriminator])
+ print("Listening on %d channels and %d guilds." % [bot.channels.size(), bot.guilds.size()])
+
+func _on_DiscordBot_message_create(bot: DiscordBot, msg: Message, channel: Dictionary):
+ print("New message from %s: %s" % [msg.author.username, msg.content])
+
+ if msg.author.bot:
+ return
+
+ await bot.reply(msg, "Hi!")
+```
+
+[Documentation](https://3ddelano.github.io/discord.gd)
+----------
+
+
+Contributing
+-----------
+
+This plugin is a non-profit project developped by voluntary contributors.
+
+### Supporters
+
+```
+- YaBoyTwiz#6733
+```
+
+### Support the project development
+
+
+Want to support in other ways? Contact me on Discord: `@3ddelano#6033`
+
+For doubts / help / bugs / problems / suggestions do join: [3ddelano Cafe](https://discord.gg/FZY9TqW)
diff --git a/addons/discord_gd/classes/application_command.gd b/addons/discord_gd/classes/application_command.gd
new file mode 100644
index 0000000..d809361
--- /dev/null
+++ b/addons/discord_gd/classes/application_command.gd
@@ -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
+ }
diff --git a/addons/discord_gd/classes/application_command.gd.uid b/addons/discord_gd/classes/application_command.gd.uid
new file mode 100644
index 0000000..77cd648
--- /dev/null
+++ b/addons/discord_gd/classes/application_command.gd.uid
@@ -0,0 +1 @@
+uid://cpk5b2jequ6my
diff --git a/addons/discord_gd/classes/bit_field.gd b/addons/discord_gd/classes/bit_field.gd
new file mode 100644
index 0000000..e4e2c13
--- /dev/null
+++ b/addons/discord_gd/classes/bit_field.gd
@@ -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)
diff --git a/addons/discord_gd/classes/bit_field.gd.uid b/addons/discord_gd/classes/bit_field.gd.uid
new file mode 100644
index 0000000..eaa59af
--- /dev/null
+++ b/addons/discord_gd/classes/bit_field.gd.uid
@@ -0,0 +1 @@
+uid://csgxh04sf3r75
diff --git a/addons/discord_gd/classes/discord_interaction.gd b/addons/discord_gd/classes/discord_interaction.gd
new file mode 100644
index 0000000..dcde269
--- /dev/null
+++ b/addons/discord_gd/classes/discord_interaction.gd
@@ -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,
+ }
diff --git a/addons/discord_gd/classes/discord_interaction.gd.uid b/addons/discord_gd/classes/discord_interaction.gd.uid
new file mode 100644
index 0000000..3622e24
--- /dev/null
+++ b/addons/discord_gd/classes/discord_interaction.gd.uid
@@ -0,0 +1 @@
+uid://dklo5j3tsw3qa
diff --git a/addons/discord_gd/classes/embed.gd b/addons/discord_gd/classes/embed.gd
new file mode 100644
index 0000000..47fffef
--- /dev/null
+++ b/addons/discord_gd/classes/embed.gd
@@ -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
+ }
diff --git a/addons/discord_gd/classes/embed.gd.uid b/addons/discord_gd/classes/embed.gd.uid
new file mode 100644
index 0000000..cf17385
--- /dev/null
+++ b/addons/discord_gd/classes/embed.gd.uid
@@ -0,0 +1 @@
+uid://cx0cqy20w5ygt
diff --git a/addons/discord_gd/classes/helpers.gd b/addons/discord_gd/classes/helpers.gd
new file mode 100644
index 0000000..587bb10
--- /dev/null
+++ b/addons/discord_gd/classes/helpers.gd
@@ -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)
diff --git a/addons/discord_gd/classes/helpers.gd.uid b/addons/discord_gd/classes/helpers.gd.uid
new file mode 100644
index 0000000..be6b512
--- /dev/null
+++ b/addons/discord_gd/classes/helpers.gd.uid
@@ -0,0 +1 @@
+uid://q15tb8w4uum
diff --git a/addons/discord_gd/classes/message.gd b/addons/discord_gd/classes/message.gd
new file mode 100644
index 0000000..01beb69
--- /dev/null
+++ b/addons/discord_gd/classes/message.gd
@@ -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
diff --git a/addons/discord_gd/classes/message.gd.uid b/addons/discord_gd/classes/message.gd.uid
new file mode 100644
index 0000000..b41ac8b
--- /dev/null
+++ b/addons/discord_gd/classes/message.gd.uid
@@ -0,0 +1 @@
+uid://dtoffh2nlc7di
diff --git a/addons/discord_gd/classes/message_action_row.gd b/addons/discord_gd/classes/message_action_row.gd
new file mode 100644
index 0000000..9a39ae0
--- /dev/null
+++ b/addons/discord_gd/classes/message_action_row.gd
@@ -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
+ }
diff --git a/addons/discord_gd/classes/message_action_row.gd.uid b/addons/discord_gd/classes/message_action_row.gd.uid
new file mode 100644
index 0000000..e925c5a
--- /dev/null
+++ b/addons/discord_gd/classes/message_action_row.gd.uid
@@ -0,0 +1 @@
+uid://dr83pxgdkl46x
diff --git a/addons/discord_gd/classes/message_button.gd b/addons/discord_gd/classes/message_button.gd
new file mode 100644
index 0000000..386f04a
--- /dev/null
+++ b/addons/discord_gd/classes/message_button.gd
@@ -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
+ }
diff --git a/addons/discord_gd/classes/message_button.gd.uid b/addons/discord_gd/classes/message_button.gd.uid
new file mode 100644
index 0000000..2f03e02
--- /dev/null
+++ b/addons/discord_gd/classes/message_button.gd.uid
@@ -0,0 +1 @@
+uid://c2mdufrdsqisx
diff --git a/addons/discord_gd/classes/message_flags.gd b/addons/discord_gd/classes/message_flags.gd
new file mode 100644
index 0000000..28fca44
--- /dev/null
+++ b/addons/discord_gd/classes/message_flags.gd
@@ -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()
diff --git a/addons/discord_gd/classes/message_flags.gd.uid b/addons/discord_gd/classes/message_flags.gd.uid
new file mode 100644
index 0000000..a4aa948
--- /dev/null
+++ b/addons/discord_gd/classes/message_flags.gd.uid
@@ -0,0 +1 @@
+uid://dbkf1n3hfcdj8
diff --git a/addons/discord_gd/classes/permissions.gd b/addons/discord_gd/classes/permissions.gd
new file mode 100644
index 0000000..386aae3
--- /dev/null
+++ b/addons/discord_gd/classes/permissions.gd
@@ -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()
diff --git a/addons/discord_gd/classes/permissions.gd.uid b/addons/discord_gd/classes/permissions.gd.uid
new file mode 100644
index 0000000..b72f199
--- /dev/null
+++ b/addons/discord_gd/classes/permissions.gd.uid
@@ -0,0 +1 @@
+uid://pov5klkwcdw3
diff --git a/addons/discord_gd/classes/select_menu.gd b/addons/discord_gd/classes/select_menu.gd
new file mode 100644
index 0000000..6ac6e3a
--- /dev/null
+++ b/addons/discord_gd/classes/select_menu.gd
@@ -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,
+ }
diff --git a/addons/discord_gd/classes/select_menu.gd.uid b/addons/discord_gd/classes/select_menu.gd.uid
new file mode 100644
index 0000000..3f4dbba
--- /dev/null
+++ b/addons/discord_gd/classes/select_menu.gd.uid
@@ -0,0 +1 @@
+uid://v5s48xly81hk
diff --git a/addons/discord_gd/classes/user.gd b/addons/discord_gd/classes/user.gd
new file mode 100644
index 0000000..9e65cbc
--- /dev/null
+++ b/addons/discord_gd/classes/user.gd
@@ -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))
diff --git a/addons/discord_gd/classes/user.gd.uid b/addons/discord_gd/classes/user.gd.uid
new file mode 100644
index 0000000..d987ae3
--- /dev/null
+++ b/addons/discord_gd/classes/user.gd.uid
@@ -0,0 +1 @@
+uid://1uaxjy7fw8f2
diff --git a/addons/discord_gd/discord.gd b/addons/discord_gd/discord.gd
new file mode 100644
index 0000000..cafa572
--- /dev/null
+++ b/addons/discord_gd/discord.gd
@@ -0,0 +1,1308 @@
+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
+var VERBOSE: bool = false
+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}
+ _send_dict_wss(response_payload)
+ _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
diff --git a/addons/discord_gd/discord.gd.uid b/addons/discord_gd/discord.gd.uid
new file mode 100644
index 0000000..ddfe311
--- /dev/null
+++ b/addons/discord_gd/discord.gd.uid
@@ -0,0 +1 @@
+uid://dqss6smw7w8nh
diff --git a/addons/discord_gd/plugin.cfg b/addons/discord_gd/plugin.cfg
new file mode 100644
index 0000000..6dd0d2b
--- /dev/null
+++ b/addons/discord_gd/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="discord.gd"
+description="A Discord bot API wrapper for Godot."
+author="Delano Lourenco"
+version="2.0.0"
+script="plugin.gd"
diff --git a/addons/discord_gd/plugin.gd b/addons/discord_gd/plugin.gd
new file mode 100644
index 0000000..d3e6204
--- /dev/null
+++ b/addons/discord_gd/plugin.gd
@@ -0,0 +1,9 @@
+@tool
+extends EditorPlugin
+
+"""
+Discord.gd
+
+Nothing to do here,
+check discord.gd script file
+"""
diff --git a/addons/discord_gd/plugin.gd.uid b/addons/discord_gd/plugin.gd.uid
new file mode 100644
index 0000000..6b212f4
--- /dev/null
+++ b/addons/discord_gd/plugin.gd.uid
@@ -0,0 +1 @@
+uid://h5i4g1mvqc8u
diff --git a/application_cmds/test_appcmd.gd b/application_cmds/test_appcmd.gd
new file mode 100644
index 0000000..ccae219
--- /dev/null
+++ b/application_cmds/test_appcmd.gd
@@ -0,0 +1,18 @@
+extends RefCounted
+
+#func on_ready(main, bot: DiscordBot) -> void:
+# pass
+#
+#func on_autocomplete(main, bot: DiscordBot, interaction: DiscordInteraction, options: Array) -> void:
+# pass
+
+func execute(main, bot: DiscordBot, interaction: DiscordInteraction, options: Array) -> void:
+ print("hiya!")
+ interaction.reply(
+ {
+ "content" : "meow meow! hearing you loud and clear tabby!"
+ }
+ )
+ pass
+
+var data = ApplicationCommand.new().set_name("meow_test").set_description("meow_desc")
diff --git a/application_cmds/test_appcmd.gd.uid b/application_cmds/test_appcmd.gd.uid
new file mode 100644
index 0000000..e52e5aa
--- /dev/null
+++ b/application_cmds/test_appcmd.gd.uid
@@ -0,0 +1 @@
+uid://d3dwi5xxpdp2x
diff --git a/datatypes/librarySave.gd b/datatypes/librarySave.gd
new file mode 100644
index 0000000..eba4b2e
--- /dev/null
+++ b/datatypes/librarySave.gd
@@ -0,0 +1,5 @@
+extends Resource
+class_name LibrarySave
+
+@export var spools : Array[Spool]
+@export var printers : Array[Printer]
diff --git a/datatypes/librarySave.gd.uid b/datatypes/librarySave.gd.uid
new file mode 100644
index 0000000..bcb6076
--- /dev/null
+++ b/datatypes/librarySave.gd.uid
@@ -0,0 +1 @@
+uid://c634rg4aki0oy
diff --git a/datatypes/printer.gd b/datatypes/printer.gd
new file mode 100644
index 0000000..399a7c6
--- /dev/null
+++ b/datatypes/printer.gd
@@ -0,0 +1,2 @@
+extends Resource
+class_name Printer
diff --git a/datatypes/printer.gd.uid b/datatypes/printer.gd.uid
new file mode 100644
index 0000000..de011db
--- /dev/null
+++ b/datatypes/printer.gd.uid
@@ -0,0 +1 @@
+uid://bnyoyarpn5qml
diff --git a/datatypes/spool.gd b/datatypes/spool.gd
new file mode 100644
index 0000000..9a5db1d
--- /dev/null
+++ b/datatypes/spool.gd
@@ -0,0 +1,7 @@
+extends Resource
+class_name Spool
+
+@export var name : String
+@export var material : String
+@export var link : String
+# tags?
diff --git a/datatypes/spool.gd.uid b/datatypes/spool.gd.uid
new file mode 100644
index 0000000..4cf2b1c
--- /dev/null
+++ b/datatypes/spool.gd.uid
@@ -0,0 +1 @@
+uid://daj15kpbagd33
diff --git a/demo.tscn b/demo.tscn
new file mode 100644
index 0000000..3c596dc
--- /dev/null
+++ b/demo.tscn
@@ -0,0 +1,29 @@
+[gd_scene load_steps=4 format=3 uid="uid://dyob0p2l7g5fj"]
+
+[ext_resource type="Script" uid="uid://bv8cjh12rwg4t" path="res://test.gd" id="1_0bhed"]
+[ext_resource type="Script" uid="uid://dqss6smw7w8nh" path="res://addons/discord_gd/discord.gd" id="2_m0rpm"]
+[ext_resource type="Texture2D" uid="uid://dta0nr1cvl70v" path="res://fabcatserver.png" id="3_m0rpm"]
+
+[node name="Control" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_0bhed")
+
+[node name="DiscordBot" type="Node" parent="."]
+script = ExtResource("2_m0rpm")
+metadata/_custom_type_script = "uid://dqss6smw7w8nh"
+
+[node name="TextureRect" type="TextureRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = ExtResource("3_m0rpm")
+expand_mode = 3
+stretch_mode = 5
diff --git a/export_presets.cfg b/export_presets.cfg
new file mode 100644
index 0000000..44da1fc
--- /dev/null
+++ b/export_presets.cfg
@@ -0,0 +1,42 @@
+[preset.0]
+
+name="Linux"
+platform="Linux"
+runnable=true
+advanced_options=false
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter=""
+export_path="../../Exports/Fabcat/FabsocBot.x86_64"
+patches=PackedStringArray()
+encryption_include_filters=""
+encryption_exclude_filters=""
+seed=0
+encrypt_pck=false
+encrypt_directory=false
+script_export_mode=2
+
+[preset.0.options]
+
+custom_template/debug=""
+custom_template/release=""
+debug/export_console_wrapper=1
+binary_format/embed_pck=false
+texture_format/s3tc_bptc=true
+texture_format/etc2_astc=false
+shader_baker/enabled=false
+binary_format/architecture="x86_64"
+ssh_remote_deploy/enabled=false
+ssh_remote_deploy/host="user@host_ip"
+ssh_remote_deploy/port="22"
+ssh_remote_deploy/extra_args_ssh=""
+ssh_remote_deploy/extra_args_scp=""
+ssh_remote_deploy/run_script="#!/usr/bin/env bash
+export DISPLAY=:0
+unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
+\"{temp_dir}/{exe_name}\" {cmd_args}"
+ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
+kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
+rm -rf \"{temp_dir}\""
diff --git a/fabcatserver.png b/fabcatserver.png
new file mode 100644
index 0000000..7dff896
Binary files /dev/null and b/fabcatserver.png differ
diff --git a/fabcatserver.png.import b/fabcatserver.png.import
new file mode 100644
index 0000000..1f2a12a
--- /dev/null
+++ b/fabcatserver.png.import
@@ -0,0 +1,40 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dta0nr1cvl70v"
+path="res://.godot/imported/fabcatserver.png-5b3643fea72e10ebca565cbc1124bb9e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://fabcatserver.png"
+dest_files=["res://.godot/imported/fabcatserver.png-5b3643fea72e10ebca565cbc1124bb9e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..c6bbb7d
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 0000000..5f75feb
--- /dev/null
+++ b/icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b4bahh7qjl64"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/library.gd b/library.gd
new file mode 100644
index 0000000..06fbb3e
--- /dev/null
+++ b/library.gd
@@ -0,0 +1,40 @@
+extends Node
+
+var save_path : String = "user://librarySave.tres"
+var save : LibrarySave
+
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+ load_data()
+
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _process(delta: float) -> void:
+ pass
+
+func load_data():
+ # check if save file exists
+ print("real? " + str(FileAccess.file_exists(save_path)))
+ if FileAccess.file_exists(save_path):
+ print("yes, loading...")
+ save = ResourceLoader.load(save_path) as LibrarySave
+ print(save)
+ #save = load(save_path) as GameSave
+
+
+ else:
+ print("nope, creating...")
+ save_new()
+ pass
+
+func save_data():
+ ResourceSaver.save(save, save_path)
+ pass
+
+func save_new():
+ #data = FileAccess.open(save_path, FileAccess.WRITE_READ)
+ save = LibrarySave.new()
+ var error = ResourceSaver.save(save, save_path)
+ print(error)
+
diff --git a/library.gd.uid b/library.gd.uid
new file mode 100644
index 0000000..5fa26b1
--- /dev/null
+++ b/library.gd.uid
@@ -0,0 +1 @@
+uid://hkh1ewsuji8m
diff --git a/main.gd b/main.gd
new file mode 100644
index 0000000..c0c5211
--- /dev/null
+++ b/main.gd
@@ -0,0 +1,205 @@
+extends Node
+signal interaction_create(world, bot, interaction, data)
+
+#const prefix = "gd."
+
+var interactions = {}
+var application_commands = {}
+
+
+#func _load_bot_token() -> String:
+ ## read from .env file DISORD_BOT_TOKEN
+ #var lines = FileAccess.get_file_as_string(".env").split("\n")
+ #for line in lines:
+ #if line.begins_with("DISCORD_BOT_TOKEN="):
+ #return line.split("=")[1]
+ #return ""
+
+func _ready() -> void:
+ var bot = DiscordBot.new()
+ add_child(bot)
+
+ # Try to read token from global environment variables
+ var token = OS.get_environment("DISCORD_BOT_TOKEN")
+ if not token or (token and len(token) < 10):
+ # Read token from local .env file
+ #token = _load_bot_token()
+ token = "MTQzMzAyNTMwMzYzODQ0MjAzNQ.G3r4My.IdnvCw6xTBfoitEzvhPgxeErSDgcMCsznmLnvI"
+
+ bot.TOKEN = token
+ #bot.INTENTS = 4609
+ bot.bot_ready.connect(_on_bot_ready)
+ #bot.message_create.connect(_on_message_create)
+ bot.interaction_create.connect(_on_interaction_create)
+ bot.login()
+ #_load_commands(bot)
+ _load_application_commands(bot)
+
+func _on_bot_ready(bot: DiscordBot):
+ print("Logged in as " + bot.user.username + "#" + bot.user.discriminator)
+ bot.set_presence({
+ "activity": {
+ "type": "Watching",
+ "name": "Watching the printers"
+ }
+ })
+
+ # Registering commands is not needed on every run,
+ # only register the command if you change any options
+ # or if you add/remove commands
+
+ # For development use the single server command and
+ # once you have the commands working you can register
+ # them as global commands
+
+ # -----Single server (updates instantly)
+ #_register_application_commands(bot, "guild_id_here")
+ _register_application_commands(bot, "679917161195765822")
+
+ # -----Global (may take upto 1hr to update)
+ #_register_application_commands(bot)
+
+
+#func _on_message_create(bot: DiscordBot, message: Message, channel: Dictionary) -> void:
+ #if message.author.bot or not message.content.begins_with(prefix):
+ #return
+#
+ ## Make sure to use trim_prefix() instead of lstrip()
+ #var raw_content = message.content.trim_prefix(prefix)
+ #var tokens = []
+ #var r = RegEx.new()
+ #r.compile("\\S+") # Negated whitespace character class
+ #for token in r.search_all(raw_content):
+ #tokens.append(token.get_string())
+ #var cmd_or_alias = tokens[0].to_lower()
+ #tokens.remove(0) # Remove the command name from the tokens
+ #var args = tokens
+ #_handle_command(bot, message, channel, cmd_or_alias, args)
+
+
+#func _load_commands(bot: DiscordBot) -> void:
+ #var cmd_path = "res://cmds/"
+ #var dir = DirAccess.open(cmd_path)
+ #if not dir:
+ #push_error("An error occurred when trying to open /cmds/ folder.")
+ #return
+#
+ #dir.list_dir_begin()
+ #while true:
+ #var file = dir.get_next()
+ #if file == "": # End of files
+ #break
+ #elif not file.begins_with(".") and (file.ends_with(".gd") or file.ends_with(".gdc")):
+ #var script = load(cmd_path + file.get_basename() + ".gd").new()
+ #var data = script.help
+#
+ ## Ensure that the commands don't have the default help values
+ #assert(data.name != "test_name" and data.category != "test_category" and data.description != "test_description" and script.get_usage(prefix) != "test usage", "Must change default values for Command in " + file)
+#
+ #if not data.enabled:
+ #continue
+#
+ #if script.has_method("on_ready"):
+ #script.on_ready(self, bot)
+#
+ #commands[data.name] = script
+#
+ #if not data.has("aliases"):
+ #continue
+#
+ #for alias in data.aliases:
+ #assert(not command_aliases.has(alias), "Duplicate cmd aliases found in cmd: " + file)
+#
+ #command_aliases[alias] = data.name
+#
+ #print("Loaded " + str(commands.size()) + " cmds")
+ #dir.list_dir_end()
+
+func _load_application_commands(bot: DiscordBot) -> void:
+ var app_cmd_path = "res://application_cmds/"
+ var dir = DirAccess.open(app_cmd_path)
+
+ if not dir:
+ push_error("An error occurred when trying to open /application_cmds/ folder.")
+ return
+
+ dir.list_dir_begin()
+ while true:
+ var file = dir.get_next()
+ if file == "": # End of files
+ break
+ elif not file.begins_with(".") and (file.ends_with(".gd") or file.ends_with(".gdc")):
+ var script = load(app_cmd_path + file.get_basename() + ".gd").new()
+ var data = script.data
+
+ # Ensure that the commands don't have the default help values
+ assert(data.name != "test_name" and data.description != "test_description", "Must change default values for ApplicationCommand in " + file)
+
+ if script.has_method("on_ready"):
+ script.on_ready(self, bot)
+
+ application_commands[data.name] = script
+
+ print("Loaded " + str(application_commands.size()) + " application cmds")
+ dir.list_dir_end()
+
+func _register_application_commands(bot, guild_id: String = "") -> void:
+ var application_commands_data = []
+ for app_cmd in application_commands.values():
+ application_commands_data.append(app_cmd.data)
+
+ bot.register_commands(application_commands_data, guild_id)
+
+#func _handle_command(bot: DiscordBot, message: Message, channel: Dictionary, cmd_or_alias: String, args: Array):
+ #var cmd = null
+ #if command_aliases.has(cmd_or_alias):
+ #cmd = commands[command_aliases[cmd_or_alias]]
+ #elif commands.has(cmd_or_alias):
+ #cmd = commands[cmd_or_alias]
+#
+ #if cmd == null:
+ #return
+#
+ #print("CMD: " + cmd.help.name + " by " + message.author.username + "#" + message.author.discriminator + " (" + message.author.id + ")")
+#
+ #cmd.on_message(self, bot, message, channel, args)
+
+func remove_components_from_interaction(interaction: DiscordInteraction, msg = ":robot: Components have timed out!") -> void:
+ var embed = Embed.new().set_description(msg)
+ var new_embeds = interaction.message.embeds + [embed]
+ interaction.update({
+ "content": interaction.message.content,
+ "embeds": new_embeds,
+ "components": []
+ })
+
+func _on_interaction_create(bot: DiscordBot, interaction: DiscordInteraction):
+ # Handle ApplicationCommand
+ if interaction.is_command() or interaction.is_autocomplete():
+ var cmd_name = interaction.data.name
+ if application_commands.has(cmd_name):
+ # The application command was found, so execute it
+ var app_cmd = application_commands[cmd_name]
+ var options = interaction.data.options if interaction.data.has("options") else []
+
+ if interaction.is_autocomplete():
+ # It is an autocomplete command
+ app_cmd.on_autocomplete(self, bot, interaction, options)
+ return
+
+ print("APP_CMD: " + app_cmd.data.name + " by " + interaction.member.user.username + "#" + interaction.member.user.discriminator + " (" + interaction.member.user.id + ")")
+ app_cmd.execute(self, bot, interaction, options)
+ else:
+ # The application command was not found
+ interaction.reply({
+ "ephemeral": true,
+ "content": ":electric_plug: The requested command was not found."
+ })
+ return
+
+ var msg_id = interaction.message.id
+ # Emit the interaction to the normal (text) commands
+ if interactions.has(msg_id):
+ emit_signal("interaction_create", self, bot, interaction, interactions[msg_id])
+ else:
+ remove_components_from_interaction(interaction)
diff --git a/main.gd.uid b/main.gd.uid
new file mode 100644
index 0000000..b362105
--- /dev/null
+++ b/main.gd.uid
@@ -0,0 +1 @@
+uid://cus8nh0g3yyj2
diff --git a/project.godot b/project.godot
new file mode 100644
index 0000000..c0e9473
--- /dev/null
+++ b/project.godot
@@ -0,0 +1,29 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="FabsocBot"
+run/main_scene="uid://dyob0p2l7g5fj"
+config/features=PackedStringArray("4.5", "GL Compatibility")
+config/icon="uid://dta0nr1cvl70v"
+
+[autoload]
+
+Library="*res://Library.tscn"
+
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/discord_gd/plugin.cfg")
+
+[rendering]
+
+renderer/rendering_method="gl_compatibility"
+renderer/rendering_method.mobile="gl_compatibility"
diff --git a/templates/application_command.gd b/templates/application_command.gd
new file mode 100644
index 0000000..14b9b45
--- /dev/null
+++ b/templates/application_command.gd
@@ -0,0 +1,12 @@
+extends RefCounted
+
+#func on_ready(main, bot: DiscordBot) -> void:
+# pass
+#
+#func on_autocomplete(main, bot: DiscordBot, interaction: DiscordInteraction, options: Array) -> void:
+# pass
+
+func execute(main, bot: DiscordBot, interaction: DiscordInteraction, options: Array) -> void:
+ pass
+
+var data = ApplicationCommand.new().set_name("test_name").set_description("test_description")
diff --git a/templates/application_command.gd.uid b/templates/application_command.gd.uid
new file mode 100644
index 0000000..a79b69e
--- /dev/null
+++ b/templates/application_command.gd.uid
@@ -0,0 +1 @@
+uid://db2x4erxyi6sw
diff --git a/test.gd b/test.gd
new file mode 100644
index 0000000..59cd259
--- /dev/null
+++ b/test.gd
@@ -0,0 +1,38 @@
+extends Control
+
+func _ready():
+ var discord_bot = $DiscordBot
+ discord_bot.TOKEN = "MTQzMzAyNTMwMzYzODQ0MjAzNQ.G3r4My.IdnvCw6xTBfoitEzvhPgxeErSDgcMCsznmLnvI"
+ discord_bot.login()
+ discord_bot.bot_ready.connect(_on_DiscordBot_bot_ready)
+ discord_bot.message_create.connect(_on_DiscordBot_message_create)
+
+func _on_DiscordBot_bot_ready(bot: DiscordBot):
+ print("Logged in as %s#%s" % [bot.user.username, bot.user.discriminator])
+ print("Listening on %d channels and %d guilds." % [bot.channels.size(), bot.guilds.size()])
+
+func _on_DiscordBot_message_create(bot: DiscordBot, msg: Message, channel: Dictionary):
+ print("New message from %s: %s" % [msg.author.username, msg.content])
+
+ if msg.author.bot:
+ return
+
+ send_screenshot(bot, msg)
+
+ await bot.reply(msg, "meow!")
+
+func send_screenshot(bot: DiscordBot, msg: Message):
+ var image : Image = get_viewport().get_texture().get_image()
+ #image.flip_y()
+ var bytes = image.save_png_to_buffer()
+
+ #send
+ bot.send(msg, {
+ "files":[
+ {
+ "name" : "screenshot.png",
+ "media_type" : "image/png",
+ "data" : bytes
+ }
+ ]
+ })
diff --git a/test.gd.uid b/test.gd.uid
new file mode 100644
index 0000000..3cf3012
--- /dev/null
+++ b/test.gd.uid
@@ -0,0 +1 @@
+uid://bv8cjh12rwg4t