forked from viveksantayana/geas-bot
Updated documentation.
Changed virtual environment settings. Beginning change to data structure.
This commit is contained in:
parent
ac1b6b0169
commit
1d355e3b2d
24
CHANGELOG.md
Normal file
24
CHANGELOG.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Major Changes in Version 3
|
||||||
|
- Discards the database engine in favour of data storage in `.yml` files
|
||||||
|
`The database engine was overkill. It only slowed down the Bot's performance, made it more fragile, and caused unnecessary complexity while not really providing any necessary utility.
|
||||||
|
This also makes server migration much easier.
|
||||||
|
I went with .yml files over .json because I preferred the readability of the former, and to align it with the Docker-Compose files elsewhere in the directory.
|
||||||
|
I cannot believe how much I got into the debate between these two formats.
|
||||||
|
Seriously, the database was overkill and such a bad idea.
|
||||||
|
It was fun to learn but was such a bad idea.`
|
||||||
|
- Implements client-side `/commands` as the core method of interaction
|
||||||
|
`This takes advantage of a new Discord feature to programme `/commands` into the console via Bot integration rather than natively at the bot.
|
||||||
|
This offers more robust command handling, with client-side data validation, option entries, input menus, etc.
|
||||||
|
This does have the drawback of requiring a more complex API interaction in order for the bot to function, but it should improve the bot's functionality overall.`
|
||||||
|
- Uses Discord Buttons and Select Menus to replace Reaction Role features
|
||||||
|
`In keeping with adopting more robust Discord features to deprecate old ones, this new version of the Bot tries to make use of more powerful Discord features instead of contorting Reaction Roles and other previous features into the shape of something they were not built for.
|
||||||
|
While it is unclear what the direction of such developments in Discord will be going forward, and it is unclear how future API changes will affect this, one way or another this is using new Discord features in the way they are meant to be used rather than excessive hacking.`
|
||||||
|
- Provides a level of abstraction in many of its recognition of the various roles and channels referenced in its functioning.
|
||||||
|
`The bot no longer has specific channels (like help or committee notification channels) or roles (like administrators or different categories of membership) hard-coded into it for it to interact with them.
|
||||||
|
This makes the code a lot more complex to debug and maintain, especially when handling defaults or fail-safes for when the relevant roles or channels are deleted, but has some added flexibility and makes the bot a lot less dependent a precise server setup, thus making it a lot more brittle in the long run.`
|
||||||
|
- Refactors the old code for more clarity in the bot's code
|
||||||
|
`Adds some descriptions and explanatory text to the commands themselves so they can be accessed and displayed via the bot help command, making the bot a lot more usable.
|
||||||
|
Also presents the code in a more readable format, with clearer indentation and parameter naming for core functions.
|
||||||
|
The code has a lot more in-line comments explaining how it works so that it can be maintained by other people.`
|
4
COMMANDS.md
Normal file
4
COMMANDS.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Bot Commands
|
||||||
|
A full list of bot commands can be retrieved using the `-help` command in the bot.
|
||||||
|
The commands have full descriptions of their function as well as syntax that can be accessed via the help command.
|
||||||
|
This is a reference file of all of the commands available, as well as the various cogs that control them.
|
145
README.md
145
README.md
@ -1,21 +1,24 @@
|
|||||||
# Geas Server Bot
|
# Geas Server Bot
|
||||||
|
|
||||||
|
```
|
||||||
|
(Currently a work in progress. The bot is still not in a state to run, and none of its features have been programmed..)
|
||||||
|
```
|
||||||
|
|
||||||
This is a bot I wrote to manage the Discord server for Geas, the Edinburgh University Table-Top Role-Playing Society, during our move to an on-line format.
|
This is a bot I wrote to manage the Discord server for Geas, the Edinburgh University Table-Top Role-Playing Society, during our move to an on-line format.
|
||||||
The bot is designed to create and manage channels and roles for gaming groups in order to replicate our in-person pitch events on a Discord space as far as possible.
|
The bot is designed to create and manage channels and roles for gaming groups in order to replicate our in-person pitch events on a Discord space as far as possible.
|
||||||
The bot is written in Python, and was the first Python coding project I wrote, so it has a special place in my heart.
|
The bot is written in Python, and was the first Python coding project I wrote, so it has a special place in my heart.
|
||||||
The first version I am committing to the repository is version 2.1, and I previously handled the version control manually, so migrating old versions to Git would be a pain.
|
The first version I committed to the repository is version 2.1, and I previously handled the version control manually, so migrating old versions to Git would be a pain.
|
||||||
|
Version 3 was the second major upgrade, taking advantage of some of the recent changes to the Discord API.
|
||||||
|
|
||||||
## Bot Setup
|
## Setup
|
||||||
The Bot is dockerised and uses docker-compose for deployment, so it is fairly straightforward to set up.
|
The Bot is dockerised and uses docker-compose for deployment, so it is fairly straightforward to deploy an instance of.
|
||||||
Clone the repository, install Docker and Docker Compose, navigate to the root directory (that contains the `docker-compose.yml` file), and use `docker-compose up -d` to set up and run the bot.
|
Clone the repository, install Docker and Docker Compose, navigate to the root directory (that contains the `docker-compose.yml` file), and use `docker-compose up -d` to set up and run the bot.
|
||||||
The bot uses two containers that are networked internally:
|
The bot runs on one Docker container with the instance of the app as well as storage for its data and configuration.
|
||||||
> 1. A python app that runs the bot, and
|
The bot uses docker-compose to mount an external volume to allow for persisting file storage and easy migration.
|
||||||
> 2. A MongoDB database that stores the bot's data for persistence.
|
It no longer uses a database engine because it never really benefitted from the various database manipulation tools in the earlier version, and was not worth the complexity.
|
||||||
|
|
||||||
The database is not exposed externally to the network, and can only be accessed by the Bot in the network of containers.
|
|
||||||
|
|
||||||
The bot authenticates using an API key, which I have kept private in a `.env` file that I have not uploaded to the repository.
|
The bot authenticates using an API key, which I have kept private in a `.env` file that I have not uploaded to the repository.
|
||||||
In order to set up your own instance of the bot, you will need to create two copies of the `.env` file, one in the root directory and one in the `app` folder, and enter the respective values for the API keys for the Geas Server Bot and the Test Bot.
|
In order to set up your own instance of the bot, you will need to provide the following values in a `.env` file in the root directory.
|
||||||
|
|
||||||
You will also need this database to set up a username and password for the MongoDB database.
|
You will also need this database to set up a username and password for the MongoDB database.
|
||||||
The specific username and password don't matter as the bot refers back to the environment variable when authenticating.
|
The specific username and password don't matter as the bot refers back to the environment variable when authenticating.
|
||||||
@ -23,16 +26,15 @@ The specific username and password don't matter as the bot refers back to the en
|
|||||||
The following is the template for the `.env` file, with the variable names as are referenced in the bot's code:
|
The following is the template for the `.env` file, with the variable names as are referenced in the bot's code:
|
||||||
`.env` file:
|
`.env` file:
|
||||||
```
|
```
|
||||||
BOT_TOKEN=
|
BOT_TOKEN=(API token for the production version of the bot.)
|
||||||
TEST_TOKEN=
|
TEST_TOKEN=(API token for any test instance.)
|
||||||
MONGO_INITDB_ROOT_USERNAME=
|
CONFIG=(Path to config file. The bot defaults to './data/config.yml' if not provided.)
|
||||||
MONGO_INITDB_ROOT_PASSWORD=
|
DATA=(Path to data file. The bot defaults to './data/data.yml' if not provided.)
|
||||||
BOT_VERSION=2.1.1
|
BOT_VERSION=(verson string)
|
||||||
|
BOT_MAINTAINER_ID=(Discord user ID of the person maintaining the bot to enable debug features.)
|
||||||
```
|
```
|
||||||
The correct API keys need to be entered in the environment variables in the `.env` file, and for a copy of this file to be placed in the root and the `app` directories.
|
The correct API keys need to be entered in the environment variables in the `.env` file, and for a copy of this file to be placed in the root and the `app` directories.
|
||||||
|
|
||||||
Also create a new folder in the root directory called `db`. This is to ensure there is persistence of data when the bot is updated.
|
|
||||||
|
|
||||||
**N.B.**: When the bot is first run, it is configured to log in as the Test Bot, and not the main Geas Server Bot, as a safety measure.
|
**N.B.**: When the bot is first run, it is configured to log in as the Test Bot, and not the main Geas Server Bot, as a safety measure.
|
||||||
To change this, navigate to the last line of the file `bot.py` and change the line:
|
To change this, navigate to the last line of the file `bot.py` and change the line:
|
||||||
```
|
```
|
||||||
@ -44,27 +46,100 @@ client.run(os.getenv('BOT_TOKEN'))
|
|||||||
```
|
```
|
||||||
in order for to authenticate as the correct bot.
|
in order for to authenticate as the correct bot.
|
||||||
|
|
||||||
## Bot Structure
|
## File Structure
|
||||||
The bot is divided into the following files:
|
|
||||||
```
|
```
|
||||||
app folder
|
|-- app
|
||||||
| bot.py -- bot core functionality and code entrypoint
|
| |-- bot.py
|
||||||
| Dockerfile -- Docker instructions on building the bot
|
| |-- Dockerfile
|
||||||
| requirements.txt -- Dependencies to be installed
|
| |-- requirements.txt
|
||||||
----cogs -- Individual modules for specific features
|
| |-- cogs
|
||||||
GameManagement.py -- adding or kicking players
|
| | |-- botcommands
|
||||||
HelpNotifier.py -- notifications for Help channel
|
| | | `-- prefix.py
|
||||||
MembershipRestriction.py -- restrictions unverified users
|
| | |-- dev
|
||||||
MembershipVerification.py -- membership verification system
|
| | |-- events
|
||||||
PitchMenu.py -- automation for generating menus for game pitches
|
| | | |-- on_connect.py
|
||||||
|
| | | |-- on_guild_channel_delete.py
|
||||||
|
| | | |-- on_guild_join.py
|
||||||
|
| | | |-- on_guild_remove.py
|
||||||
|
| | | |-- on_guild_role_create.py
|
||||||
|
| | | |-- on_guild_role_delete.py
|
||||||
|
| | | |-- on_guild_role_update.py
|
||||||
|
| | | |-- on_guild_update.py
|
||||||
|
| | | `-- on_ready.py
|
||||||
|
| | `-- slashcommands
|
||||||
|
| | | |--
|
||||||
|
| | | `--
|
||||||
|
| |-- data
|
||||||
|
| | |-- .gitkeep
|
||||||
|
| | |-- config.yml
|
||||||
|
| | `-- data.yml
|
||||||
|
|
|
||||||
|
|
|
||||||
|
|-- .env
|
||||||
|
|-- .gitignore
|
||||||
|
|-- CHANGELOG.md
|
||||||
|
|-- COMMANDS.md
|
||||||
|
|-- docker-compse.yml
|
||||||
|
|-- LICENSE
|
||||||
|
|-- README.md
|
||||||
|
|-- resources.md
|
||||||
|
`-- TODO.md
|
||||||
|
|
||||||
|
GameManagement.py -- adding or kicking players
|
||||||
|
HelpNotifier.py -- notifications for Help channel
|
||||||
|
MembershipRestriction.py -- restrictions unverified users
|
||||||
|
MembershipVerification.py -- membership verification system
|
||||||
|
PitchMenu.py -- automation for generating menus for game pitches
|
||||||
```
|
```
|
||||||
|
|
||||||
Many of the specific features, such as the bot's prefix, the roles it recognises as Committee, the channels it recognises as the Help or Membership Verification channels, are all hard-coded into the Bot.
|
## Data Structure
|
||||||
This is because the bot was only ever supposed to be used on one server, so did not need the flexibility of adapting to multiple channels.
|
|
||||||
In the future, if I ever tinker with this in the future, I might try and add flexibility in the channels and roles it defines for its various functions.
|
|
||||||
|
|
||||||
I might also, in future incarnations, not use a database.
|
The bot holds data in two `.yml` files, `config.yml` for client configurations for each guild it is in and `data.yml` to hold the actual data regarding game and channel set-up.
|
||||||
It was fun to learn how to use a database, but it is overkill.
|
I was considering merging them into one file, but given how different the two concerns were I ended up splitting the files.
|
||||||
|
I had initially condsiders a `.ini` file for the configuration settings and `.json` for the data, but I decided to use `.yml` for both just to avoid unnecessary complexity.
|
||||||
|
|
||||||
## Bot Commands
|
### `config.yml` Structure
|
||||||
A full list of bot commands can be retrieved using the `-help` command in the bot, and this might be an easier way of retrieving the commands than having a separate copy in the documentation.
|
This tree gives the list of various keys for the `.yml` dictionary as well as the types of different data expected.
|
||||||
|
The entire configuration file is essentially a dictionary with other dictionaries, strings, integers, and lists as values.
|
||||||
|
All values in the dictionary are referenced first by a string of the guild id integer.
|
||||||
|
Remember to convert the guild ID to strings during several operations, and be careful to compare like for like in any logics.
|
||||||
|
```
|
||||||
|
(guild id string):
|
||||||
|
channels:
|
||||||
|
help: (id integer)
|
||||||
|
mod: (id integer)
|
||||||
|
signup: (id integer)
|
||||||
|
configured: (boolean)
|
||||||
|
membership:
|
||||||
|
(type string): (id integer)
|
||||||
|
name: (string)
|
||||||
|
owner: (id integer)
|
||||||
|
prefix: (string)
|
||||||
|
roles:
|
||||||
|
admin: (list)
|
||||||
|
- (id integer)
|
||||||
|
committee: (id integer)
|
||||||
|
bots: (id integer)
|
||||||
|
newcomer: (id integer)
|
||||||
|
returning: (id integer)
|
||||||
|
student: (id integer)
|
||||||
|
timeslots: (list)
|
||||||
|
- (string)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `data.yml` Structure
|
||||||
|
Just like above, the `data.yml` file is also a dictionary of dictionaries that is indexed by a string of the guild id.
|
||||||
|
It stores only the relevant data necessary for the code to function.
|
||||||
|
It only holds, for instance, ID numbers rather than user handles, Discord discriminators, or names.
|
||||||
|
```
|
||||||
|
(guild id string):
|
||||||
|
(timeslot string):
|
||||||
|
role: (role id integer)
|
||||||
|
gm: (user id integer)
|
||||||
|
name: (string)
|
||||||
|
players:
|
||||||
|
current: (integer)
|
||||||
|
max: (integer)
|
||||||
|
min: (integer)
|
||||||
|
system: (string)
|
||||||
|
```
|
12
TODO.md
12
TODO.md
@ -3,6 +3,8 @@
|
|||||||
## Bot Architecture
|
## Bot Architecture
|
||||||
- [x] Simplify directory tree
|
- [x] Simplify directory tree
|
||||||
- [x] Split event listeners into individual cogs.
|
- [x] Split event listeners into individual cogs.
|
||||||
|
- [ ] Update with re-organised data and config structure
|
||||||
|
> - [ ] Correct references to data in existing cogs.
|
||||||
|
|
||||||
## Bot Functionality
|
## Bot Functionality
|
||||||
- [ ] 'Delete Commands' Function
|
- [ ] 'Delete Commands' Function
|
||||||
@ -15,12 +17,12 @@
|
|||||||
- [ ] Error handlers
|
- [ ] Error handlers
|
||||||
- [ ] Debug Features
|
- [ ] Debug Features
|
||||||
- [ ] Help Channel Event Listener
|
- [ ] Help Channel Event Listener
|
||||||
> - [ ] Add Config key for Help Channel
|
> - [X] Add Config key for Help Channel
|
||||||
- [ ] Slash Command Buttons or
|
- [ ] Slash Command Buttons or
|
||||||
- [ ] Reaction listener selectors
|
- [ ] Reaction listener selectors
|
||||||
- [ ] Member Verification
|
- [ ] Member Verification
|
||||||
> - [ ] Add Config key membership signup channels
|
> - [X] Add Config key membership signup channels
|
||||||
> - [ ] Add config keys: Membership Category Roles
|
> - [X] Add config keys: Membership Category Roles
|
||||||
> - [ ] Message Receive listener
|
> - [ ] Message Receive listener
|
||||||
> - [ ] Message React listener or buttons
|
> - [ ] Message React listener or buttons
|
||||||
- [ ] Membership Restriction
|
- [ ] Membership Restriction
|
||||||
@ -53,3 +55,7 @@
|
|||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
- [ ] Review documentation
|
- [ ] Review documentation
|
||||||
|
> - [ ] Finalise README.md
|
||||||
|
> - [ ] CHANGELOG.md
|
||||||
|
> - [ ] COMMANDS.md
|
||||||
|
> - [ ] resources.md
|
42
app/bot.py
42
app/bot.py
@ -23,13 +23,13 @@ def yaml_dump(data:dict, filepath:str):
|
|||||||
yaml.dump(data, file)
|
yaml.dump(data, file)
|
||||||
|
|
||||||
# Locate or create config file
|
# Locate or create config file
|
||||||
configFile = './data/config.yml'
|
configFile = os.getenv('CONFIG') if os.getenv('CONFIG').endswith('.yml') else './data/config.yml'
|
||||||
|
|
||||||
if not os.path.exists(configFile):
|
if not os.path.exists(configFile):
|
||||||
yaml_dump({},configFile)
|
yaml_dump({},configFile)
|
||||||
|
|
||||||
# Locate or create data file
|
# Locate or create data file
|
||||||
dataFile = './data/data.yml'
|
dataFile = os.getenv('DATA') if os.getenv('DATA').endswith('.yml') else './data/data.yml'
|
||||||
|
|
||||||
if not os.path.exists(dataFile):
|
if not os.path.exists(dataFile):
|
||||||
yaml_dump({},dataFile)
|
yaml_dump({},dataFile)
|
||||||
@ -63,7 +63,6 @@ slash = SlashCommand(
|
|||||||
# sync_on_reload is an important parameter that will become relevant when having to reload cogs on changing bot configs.
|
# sync_on_reload is an important parameter that will become relevant when having to reload cogs on changing bot configs.
|
||||||
|
|
||||||
# Define Config keys
|
# Define Config keys
|
||||||
configKeys = ['adminroles', 'committeerole', 'botrole', 'modchannel', 'name', 'owner', 'prefix']
|
|
||||||
|
|
||||||
def setConfig(guild:discord.Guild):
|
def setConfig(guild:discord.Guild):
|
||||||
#### Check if the bot is missing any config entries for the guilds it is in, and if it is then add it in.
|
#### Check if the bot is missing any config entries for the guilds it is in, and if it is then add it in.
|
||||||
@ -71,29 +70,34 @@ def setConfig(guild:discord.Guild):
|
|||||||
#### Because the bot connects to Discord after it loads, it will not be able to introspect and see what guilds it is part of before the commands are first loaded, and it will only add new guilds to the config files after it has already connected.
|
#### Because the bot connects to Discord after it loads, it will not be able to introspect and see what guilds it is part of before the commands are first loaded, and it will only add new guilds to the config files after it has already connected.
|
||||||
#### The Bot will first need to set up all of its configurations, and then begin loading all other commands once it is ready.
|
#### The Bot will first need to set up all of its configurations, and then begin loading all other commands once it is ready.
|
||||||
conf = yaml_load(configFile)
|
conf = yaml_load(configFile)
|
||||||
if str(guild.id) not in conf:
|
guildStr = str(guild.id)
|
||||||
conf[str(guild.id)] = {}
|
if guildStr not in conf:
|
||||||
if 'name' not in conf[str(guild.id)] or conf[str(guild.id)]['name'] != guild.name:
|
conf[guildStr] = {}
|
||||||
conf[str(guild.id)]['name'] = guild.name
|
if 'name' not in conf[guildStr] or conf[guildStr]['name'] != guild.name:
|
||||||
if 'owner' not in conf[str(guild.id)] or conf[str(guild.id)]['owner'] != guild.owner_id:
|
conf[guildStr]['name'] = guild.name
|
||||||
conf[str(guild.id)]['owner'] = guild.owner_id
|
if 'configured' not in conf[guildStr] or conf[guildStr]['configured'] is not bool:
|
||||||
if 'adminroles' not in conf[str(guild.id)] or (type(conf[str(guild.id)]['adminroles']) is not list or len(conf[str(guild.id)]['adminroles']) == 0 or None in conf[str(guild.id)]['adminroles']):
|
conf[guildStr]['configured'] = False
|
||||||
conf[str(guild.id)]['adminroles'] = []
|
if 'owner' not in conf[guildStr] or conf[guildStr]['owner'] != guild.owner_id:
|
||||||
for role in guild.roles:
|
conf[guildStr]['owner'] = guild.owner_id
|
||||||
if not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator:
|
if 'roles' not in conf[guildStr] or (type(conf[guildStr])['roles'] is not dict or len(conf[guildStr]['roles']) == 0 or None in conf[guildStr]['roles']):
|
||||||
conf[str(guild.id)]['adminroles'].append(role.id)
|
conf[guildStr]['roles'] = {}
|
||||||
if 'prefix' not in conf[str(guild.id)]:
|
if 'admin' not in conf[guildStr]['roles'] or (type(conf[guildStr]['roles']['admin']) is not list or len(conf[guildStr]['roles']['admin']) == 0 or None in conf[guildStr]['roles']['admin']):
|
||||||
conf[str(guild.id)]['prefix'] = '-'
|
conf[guildStr]['roles']['admin'] = []
|
||||||
if 'modchannel' not in conf[str(guild.id)]:
|
for role in guild.roles:
|
||||||
|
if not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator:
|
||||||
|
conf[guildStr]['roles']['admin'].append(role.id)
|
||||||
|
if 'prefix' not in conf[guildStr]:
|
||||||
|
conf[guildStr]['prefix'] = '-'
|
||||||
|
if 'modchannel' not in conf[guildStr]:
|
||||||
if guild.system_channel is None:
|
if guild.system_channel is None:
|
||||||
p = len(guild.channels)
|
p = len(guild.channels)
|
||||||
c = None
|
c = None
|
||||||
for t in guild.text_channels:
|
for t in guild.text_channels:
|
||||||
if t.position < p:
|
if t.position < p:
|
||||||
p = t.position
|
p = t.position
|
||||||
conf[str(guild.id)]['modchannel'] = t.id
|
conf[guildStr]['modchannel'] = t.id
|
||||||
else:
|
else:
|
||||||
conf[str(guild.id)]['modchannel'] = guild.system_channel.id
|
conf[guildStr]['modchannel'] = guild.system_channel.id
|
||||||
yaml_dump(conf, configFile)
|
yaml_dump(conf, configFile)
|
||||||
|
|
||||||
def clearConfig(guildKey:str):
|
def clearConfig(guildKey:str):
|
||||||
|
Binary file not shown.
@ -13,13 +13,15 @@
|
|||||||
`N.B.: this is an important security feature in Discord's API, and commands will not be configured unless the applications.commands scope is configured correctly.`
|
`N.B.: this is an important security feature in Discord's API, and commands will not be configured unless the applications.commands scope is configured correctly.`
|
||||||
> 2. [How to add Slash Commands, including sub-commands](https://discord-py-slash-command.readthedocs.io/en/latest/faq.html#:~:text=If%20your%20slash%20commands%20don,commands%20scope%20in%20that%20guild.)
|
> 2. [How to add Slash Commands, including sub-commands](https://discord-py-slash-command.readthedocs.io/en/latest/faq.html#:~:text=If%20your%20slash%20commands%20don,commands%20scope%20in%20that%20guild.)
|
||||||
> 3. [Slash Command Cogs Module](https://discord-py-slash-command.readthedocs.io/en/latest/discord_slash.cog_ext.html?highlight=cog#discord_slash.cog_ext.cog_subcommand)
|
> 3. [Slash Command Cogs Module](https://discord-py-slash-command.readthedocs.io/en/latest/discord_slash.cog_ext.html?highlight=cog#discord_slash.cog_ext.cog_subcommand)
|
||||||
|
3. [Discord Components Documentation](https://discord-components.readthedocs.io/en/0.5.2.4/)
|
||||||
|
|
||||||
## Tutorials
|
## YouTube Tutorials
|
||||||
1. [YouTube Tutorial by Lucas, starting from the basics](https://www.youtube.com/watch?v=nW8c7vT6Hl4)
|
1. [Starting from the basics by Lucas](https://www.youtube.com/watch?v=nW8c7vT6Hl4)
|
||||||
2. [YouTube tutorial on introduction to Cogs](https://www.youtube.com/watch?v=vQw8cFfZPx0)
|
2. [Introduction to Cogs](https://www.youtube.com/watch?v=vQw8cFfZPx0)
|
||||||
3. [YouTube tutorial on dynamic prefixes for different servers](https://www.youtube.com/watch?v=yrHbGhem6I4)
|
3. [Dynamic prefixes for different servers](https://www.youtube.com/watch?v=yrHbGhem6I4)
|
||||||
4. [YouTube tutorial on using the new Slash Command API](https://www.youtube.com/watch?v=CLQ8gfb2jh4)
|
4. [Using the new Slash Command API](https://www.youtube.com/watch?v=CLQ8gfb2jh4)
|
||||||
5.
|
5. [Using embeds with Discord buttons](https://www.youtube.com/watch?v=lDZE_Muwgio)
|
||||||
|
6. [Discord Buttons](https://www.youtube.com/watch?v=oAlTicmBwOk)
|
||||||
|
|
||||||
## Communities
|
## Communities
|
||||||
|
|
Loading…
Reference in New Issue
Block a user