barebones prototype

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

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/

6
Library.tscn Normal file
View file

@ -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")

12
Main.tscn Normal file
View file

@ -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")

View file

@ -1 +1 @@
# FabsocBot
# FabsocBot

View file

@ -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.

111
addons/discord_gd/README.md Normal file
View file

@ -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
<br>
<img alt="Godot4" src="https://img.shields.io/badge/-Godot 4.x-478CBF?style=for-the-badge&logo=godotengine&logoWidth=20&logoColor=white" />
![Make Discord bots in Godot image](https://raw.githubusercontent.com/3ddelano/discord.gd/refs/heads/main/discord_gd_thumbnail.jpg)
#### 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)
<a href="https://gdaimcp.com?ref=discordgd-readme" target="_blank">
<img src="https://gdaimcp.com/images/og/gdai-mcp.png" width="400" />
</a>
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
<a href="https://www.buymeacoffee.com/3ddelano" target="_blank"><img height="41" width="174" src="https://cdn.buymeacoffee.com/buttons/v2/default-red.png" alt="Buy Me A Coffee" width="150" ></a>
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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

1308
addons/discord_gd/discord.gd Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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"

View file

@ -0,0 +1,9 @@
@tool
extends EditorPlugin
"""
Discord.gd
Nothing to do here,
check discord.gd script file
"""

View file

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

View file

@ -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")

View file

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

5
datatypes/librarySave.gd Normal file
View file

@ -0,0 +1,5 @@
extends Resource
class_name LibrarySave
@export var spools : Array[Spool]
@export var printers : Array[Printer]

View file

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

2
datatypes/printer.gd Normal file
View file

@ -0,0 +1,2 @@
extends Resource
class_name Printer

1
datatypes/printer.gd.uid Normal file
View file

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

7
datatypes/spool.gd Normal file
View file

@ -0,0 +1,7 @@
extends Resource
class_name Spool
@export var name : String
@export var material : String
@export var link : String
# tags?

1
datatypes/spool.gd.uid Normal file
View file

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

29
demo.tscn Normal file
View file

@ -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

42
export_presets.cfg Normal file
View file

@ -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}\""

BIN
fabcatserver.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

40
fabcatserver.png.import Normal file
View file

@ -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

1
icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 995 B

43
icon.svg.import Normal file
View file

@ -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

40
library.gd Normal file
View file

@ -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)

1
library.gd.uid Normal file
View file

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

205
main.gd Normal file
View file

@ -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)

1
main.gd.uid Normal file
View file

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

29
project.godot Normal file
View file

@ -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"

View file

@ -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")

View file

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

38
test.gd Normal file
View file

@ -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
}
]
})

1
test.gd.uid Normal file
View file

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