15 July Build

Implemented YAML
Implemented basic client introspection for guild metadata
Added todo tracker
This commit is contained in:
Vivek Santayana 2021-07-15 09:03:44 +01:00
parent c123186984
commit ef6c49b5f8
13 changed files with 563 additions and 24 deletions

8
.gitignore vendored
View File

@ -88,14 +88,14 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
@ -140,6 +140,10 @@ dmypy.json
# Cython debug symbols
cython_debug/
# Local Dev Env Configs
Scripts/
pyvenv.cfg
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"python.pythonPath": "./app/Scripts/python.exe",
}

24
TODO.md Normal file
View File

@ -0,0 +1,24 @@
# To Do
## Bot Architecture
[] Simplify directory tree
## Bot Functionality
[] Delete Commands Function
[] Register Commands Function
[] Infer Permissions from Config
[] Dynamic Command Prefixes
[] Infer Games from Server Structure
## Event Listeners
### Review Configs When
[] Guild Changing Ownership
[] Roles Modified
[] Mod Channel Deleted
## Commands
[] Migrate existing bot commands
## Misc
[] Review documentation

View File

@ -1,9 +1,12 @@
# Import Dependencies
import os
import pymongo
import configparser
import discord
from discord.ext import commands, tasks
configFile = './config.ini'
dataDir = './data'
# Set Intents
intents = discord.Intents.all()
intents.typing = True
@ -17,8 +20,7 @@ p = '¬'
# Define Global State Dictionary
state = {}
# Create Clients
dbClient = pymongo.MongoClient(host='geasbot-db', username=os.environ['MONGO_INITDB_ROOT_USERNAME'], password=os.environ['MONGO_INITDB_ROOT_PASSWORD'], authsource='admin', serverSelectionTimeoutMS=1000)
# Clients
client = commands.Bot(command_prefix=p, description=f'Geas Server Bot v {os.getenv("BOT_VERSION")}. This is a bot to facilitate setting up game channels on the Geas server. The prefix for the bot is {p}. You can interact with and manipulate game channels that have been created with this bot by @-mentioning the relevant role associated with the game.', intents=intents)
# Define Game Times Dictionary
@ -203,6 +205,7 @@ async def on_command_error(ctx,error):
# On Ready
@client.event
async def on_ready():
await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=f'{p} commands'))
print(f'Bot has logged in as {client.user.name} responding to prefix {p}')
print(f'Geas Server Bot version {os.getenv("BOT_VERSION")} by Vivek Santayana')
@ -327,7 +330,6 @@ async def defineBotrole(ctx, arg):
if not arg.startswith('<@&'):
raise commands.CommandError('Invalid argument. The argument must @ a role.')
dbName = str(ctx.guild.id)
db = dbClient[dbName]
colName = 'settings'
r = ctx.guild.get_role(int(arg[3:-1]))
if dbName not in state:

0
app/data/.gitkeep Normal file
View File

4
app/data/config.yml Normal file
View File

@ -0,0 +1,4 @@
'864651943820525609':
adminroles: []
name: Test
owner: 493694762210033664

117
app/dev.py Normal file
View File

@ -0,0 +1,117 @@
import os # OS Locations
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import yaml # Parser for yaml files for config settings.
import json # Json Library to manage json Data files
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
## Define YAML functions
def yaml_load(filepath):
### Loads a YAML file
with open(filepath, 'r') as file:
data = yaml.load(file)
return data
def yaml_dump(data, filepath):
### Dumps a YAML file
with open(filepath, 'w') as file:
yaml.dump(data, file)
# Locate or create config File
configFile = './data/config.yml'
if not os.path.exists(configFile):
yaml_dump({},configFile)
# Locate Cogs Directory
cogsDir = 'dev_cogs'
## Logging configuration imported boilerplate from Discord Py Docs
logger = logging.getLogger('discord')
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
logger.addHandler(handler)
# Define Clients
client = commands.Bot(
intents=discord.Intents.all(),
command_prefix='¬'
)
slash = SlashCommand(
client,
sync_commands = True,
sync_on_cog_reload = True
)
# sync_on_reload is an important parameter that will become relevant when having to reload cogs on changing bot configs.
# Define Config keys
configKeys = ['adminroles', 'botrole', 'modchannel', 'name', 'owner', 'prefix']
# Create list of guild IDs the bot is currently in
# guild_ids = [864651943820525609]
guild_ids = []
# Retrieve IDs from config file
conf = yaml_load(configFile)
for guild in conf:
if int(guild) not in guild_ids:
guild_ids.append(int(guild))
# Will check on bot ready if any other guilds are missing fron configs.
@client.command(name='foobartest')
async def foobartest(ctx):
f = await utils.manage_commands.get_all_commands(client.user.id,os.getenv('TEST_3_TOKEN'),guild_id=ctx.guild.id)
print(f)
def loadCommands():
for cogfile in os.listdir(f'./{cogsDir}/commands'):
logging.info('Loading commands.')
if cogfile.endswith('.py'):
c = cogfile[:-3]
client.load_extension(f'{cogsDir}.commands.{c}')
print(f'Loaded Cog ./{cogsDir}/{c}')
logging.info(f'Loaded Cog ./app/{cogsDir}/commands/{c}')
def unloadCommands():
for cogfile in os.listdir(f'./{cogsDir}/commands'):
logging.info('Unloading commands.')
if cogfile.endswith('.py'):
c = cogfile[:-3]
client.unload_extension(f'{cogsDir}.commands.{c}')
print(f'Unloaded Cog ./{cogsDir}/{c}')
logging.info(f'Unloaded Cog ./{cogsDir}/commands/{c}')
def reloadCommands():
unloadCommands()
loadCommands()
def loadAllCogs():
for cogfile in os.listdir(f'./{cogsDir}'):
logging.info('Loading cogs.')
if cogfile.endswith('.py'):
c = cogfile[:-3]
client.load_extension(f'{cogsDir}.{c}')
print(f'Loaded Cog ./{cogsDir}/{c}')
logging.info(f'Loaded Cog ./app/{cogsDir}/{c}')
def unloadAllCogs():
for cogfile in os.listdir(f'./{cogsDir}'):
logging.info('Unloading cogs.')
if cogfile.endswith('.py'):
c = cogfile[:-3]
client.unload_extension(f'{cogsDir}.{c}')
print(f'Unloaded Cog ./{cogsDir}/{c}')
logging.info(f'Unloaded Cog ./{cogsDir}/{c}')
def reloadAllCogs():
unloadAllCogs()
loadAllCogs()
loadAllCogs()
client.run(os.getenv('TEST_3_TOKEN'))

View File

@ -0,0 +1,37 @@
import os # OS Locations
import yaml # YAML parser for Bot config files
import json # Json Library to manage json Data files
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
from dev import guild_ids
##### Configuration Cog
class Configuration(commands.Cog):
def __init__(self, client):
self.client = client
@cog_ext.cog_slash(
# base='botrole',
# subcommand_group='configure',
name='configure',
description='Parameter to define the role assigned to the dice bots.',
# base_description='Command to configure the various guild parameters.',
# subcommand_group_description='These are configuration commands to set up the various guild parameters.',
guild_ids=guild_ids
# options=[
# create_option(
# name='botrole',
# description='The role that the dice bots are assigned in order to access the text channels.'
# type=8,
# required=True
# )
# ]
)
async def _configure(self, ctx:SlashContext, option):
await ctx.send(f'The `botrole` for the guild `{ctx.guild.name}` has been set to `{option}`.')
def setup(client):
client.add_cog(Configuration(client))

View File

@ -0,0 +1,51 @@
import os
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
from dev import guild_ids, unloadAllCogs, loadAllCogs, reloadAllCogs
##### Debug Cog
class Debug(commands.Cog):
def __init__(self, client):
self.client = client
@cog_ext.cog_slash(
name='reload',
description='Reloads all cogs',
guild_ids=guild_ids
)
async def _reload(self, ctx:SlashContext):
reloadAllCogs()
await ctx.send('Reloading Cogs.')
@cog_ext.cog_slash(
name='deleteAll',
description='Deletes all Slash Commands',
guild_ids=guild_ids
)
async def _deleteAll(self, ctx:SlashContext):
await utils.manage_commands.remove_all_commands(
bot_id=self.client.user.id,
bot_token=os.getenv('TEST_3_TOKEN'),
guild_ids=None
)
await utils.manage_commands.remove_all_commands(
bot_id=self.client.user.id,
bot_token=os.getenv('TEST_3_TOKEN'),
guild_ids=guild_ids
)
await ctx.send('Deleted all commands.')
@commands.command(
name='reloadAll'
)
async def _reloadAll(self, ctx):
await ctx.send('Reloading all cogs.')
reloadAllCogs()
def setup(client):
client.add_cog(Debug(client))

90
app/dev_cogs/events.py Normal file
View File

@ -0,0 +1,90 @@
import os # OS Locations
import yaml # YAML parser for Bot config files
import json # Json Library to manage json Data files
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
import logging
from dev import configFile, guild_ids, unloadAllCogs, loadAllCogs, reloadAllCogs, loadCommands, unloadCommands, reloadCommands, logger, handler, yaml_load, yaml_dump
##### Event Listener Cog
class Events(commands.Cog):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_connect(self): ## Actions for when bot logs in and enters ready state
print('Bot has connected.')
logging.info('Bot has connected.')
#### Check if the bot is missing any config entries for the guilds it is in, and if it is then add it in.
#### N.B.: The way the commands work, the bot will have to list specific guilds in which it will synchronise the commands when it is defining them. So it needs to give a list of all the guilds it is part of when the bot loads, which it draws from the config files.
#### 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.
#### So the bot will have to wait until it first connects, then check if any guilds are missing, and if there are any missing guilds it will need to add them to the list of guilds it has configured, then re-load all of the commands and re-sync them.
gFlag = False
print('Checking for guilds with missing configs.') #### The bot will try to update its configs for any missing values. It will check the accuracy of these values if any relevant parameters of the guild change later.
conf = yaml_load(configFile)
for g in self.client.guilds:
if g.id not in guild_ids:
gFlag = True
guild_ids.append(g.id)
conf[str(g.id)] = {}
if 'name' not in conf[str(g.id)]:
conf[str(g.id)]['name'] = g.name
if 'owner' not in conf[str(g.id)]:
conf[str(g.id)]['owner'] = await self.client.fetch_guild(g.id).owner.id
if 'adminroles' not in conf[str(g.id)]:
conf[str(g.id)]['adminroles'] = []
for role in g.roles:
if not (role.is_bot_managed() or role.is_integration()) and role.permissions.administrator:
conf[str(g.id)]['adminroles'].append(role.id)
yaml_dump(conf, configFile)
if gFlag:
reloadAllCogs()
## Because the bot will need to re-sync all the commands with an updated list of guilds, it needs to re-load all cogs.
## This should hopefully work with the setting declared earlier to sync on cog reload.
#### Delete Configs for Guilds that the Bot is no longer in
conf = yaml_load(configFile)
a = []
for g in self.client.guilds:
a.append(str(g.id))
for g in list(conf):
if g not in a:
del conf[g]
yaml_dump(conf, configFile)
# for g in self.client.guilds:
@commands.Cog.listener()
async def on_guild_join(self, guild): ## Actions for when bot is added to a new guild for the first time.
if guild.id not in guild_ids:
guild_ids.append(guild.id)
o = f'Adding config entry for guild {g.name}.'
print(o)
logging.info(o)
conf = yaml_load(configFile)
conf[str(g.id)] = {}
yaml_dump(conf, configFile)
reloadAllCogs()
## Same reason as above, when the bot is added to a new guild, it will need to re-load all cogs and re-sync commands.
@commands.Cog.listener()
async def on_guild_remove(self, guild): ## Actions for when the bot is removed from a guild.
if guild.id in guild_ids:
guild_ids.remove(guild.id)
o = f'Removing config entry for guild {g.name}.'
print(o)
logging.info(o)
conf = yaml_load(configFile)
conf.pop(str(guild.id), None)
yaml_dump(conf, configFile)
reloadAllCogs()
def setup(client):
client.add_cog(Events(client))

189
app/dev_old.py Normal file
View File

@ -0,0 +1,189 @@
import os # OS Locations
from dotenv import load_dotenv # Import OS variables from Dotenv file.
load_dotenv() # Load Dotenv. Delete this for production
import configparser # Config ini parser for Bot config files
import json # Json Library to manage json Data files
import discord # Main Lib
from discord.ext import commands # Commands module
from discord_slash import SlashCommand, SlashContext, cog_ext, utils # Slash Command Library
from discord_slash.utils.manage_commands import create_choice, create_option # Slash Command features
# Locate configuration file
configFile = './data/config.ini'
# Create empty list of current guilds, to be populated Bot load
guild_ids = []
# Config keys
configKeys = ['botrole', 'committeerole', 'modchannel', 'prefix']
# Bot Prefix Function for Regular String Prefix (non-Slash commands)
def getPrefix(client,message):
conf = configparser.ConfigParser()
conf.read(configFile)
if message.guild.id in conf.sections():
if 'prefix' in conf[message.guild.id]:
return conf[message.guild.id]['prefix']
pass
# Define bot client and initialise Slash Command client
client = commands.Bot(command_prefix= '¬', intents=discord.Intents.all()) # Regular client set up, but I did not know you could pass a function as a prefix!
slash = SlashCommand(client, sync_commands=True) # Enable Slash Command Functionality
# Starting Bot Initialisation
@client.event
async def on_guild_join(guild):
pass
# @slash.slash(
# name='configure',
# description='Configuration command to set up the various parameters for the bot on the server.'
# guild_ids=guild_ids,
# options = [
# create_option(
# name='parameter1',
# description='Please select the first parameter you would like to configure.',
# type=3,
# required=True
# )
# ]
# )
# On Ready
@client.event
async def on_ready():
print('Bot Ready')
for guild in client.guilds:
guild_ids.append(guild.id)
channel = discord.utils.get(guild.text_channels,position=1)
print(channel)
print(guild_ids)
conf = configparser.ConfigParser()
conf.read(configFile)
undef = []
if guild.id in conf.sections(): # Check if there are configs for the guild, and check if configs are complete
for i in configKeys:
if i not in conf[guild.id]:
undef.append(i)
if len(undef) == 0: # If none of the key values are undefined, ignore it.
channel = discord.utils.get(guild.text_channels,id=conf[guild.id]['modchannel'])
output = f'`{client.user.display_name}` has already been configured for the guild `{guild.name}`. \n'
output = ''.join([output, f'The `botrole` for the guild `{guild.name}` is {discord.utils.get(guild.roles,id=conf[guild.id]["botrole"]).mention}\n'])
output = ''.join([output, f'The `committeerole` for the guild `{guild.name}` is {discord.utils.get(guild.roles,id=conf[guild.id]["committeerole"]).mention}\n'])
output = ''.join([output, f'The `modchannel` for the guild `{guild.name}` is {channel.mention}\n'])
output = ''.join([output, f'The `prefix` for the guild `{guild.name}` is `{conf[guild.id]["prefix"]}`'])
await channel.send(output)
break
if len(undef) == 0:
undef = configKeys.copy()
output = f'`{client.user.display_name}` has not been configured for the guild `{guild.name}`. Please define:\n'
for u in undef:
output = ''.join([output, f'`{u}`\n'])
output = ''.join([output, f'using the `/configure` command.'])
await channel.send(output)
@slash.slash(
name='hello',
description='Hello World command',
guild_ids=guild_ids,
options = [
create_option(
name='option',
description='choose your word',
required=True,
option_type=3
)
]
)
async def _hello(ctx:SlashContext, option:str):
await ctx.send(option)
@slash.slash(
name='mypfp',
description='Displays profile picture',
guild_ids=guild_ids
)
async def pfp(ctx):
embed = discord.Embed(
title=f'Avatar of {ctx.author.display_name}',
color=discord.Color.teal()
).set_image(url=ctx.author.avatar_url)
await ctx.send(embed=embed)
class Cogs(commands.Cog):
def __init__(self, client):
self.client = client
@commands.command(description='Sends Pong')
async def ping(self, ctx):
await ctx.send('pong')
@cog_ext.cog_slash(name='Ping', description='Sends Pong')
async def ping(self, ctx):
await ctx.send('pong')
@client.command(name='foo')
async def foo(ctx):
f = await utils.manage_commands.get_all_commands(client.user.id,os.getenv('TEST_3_TOKEN'),guild_id=ctx.guild.id)
print(f)
def setup(bot):
client.add_cog(Cogs(client))
###### Configuration Cog
# class Configuration(commands.Cog):
# def __init__(self, client):
# self.client = client
# @cog_ext.cog_slash(
# # base='botrole',
# # subcommand_group='configure',
# name='configure',
# description='Parameter to define the role that is assigned to the dice bots in this guild so they can access in-game text channels.',
# # base_description='Command to configure the various guild parameters.',
# # subcommand_group_description='These are configuration commands to set up the various guild parameters.',
# guild_ids=guild_ids
# # options=[
# # create_option(
# # name='botrole',
# # description='The role that the dice bots are assigned in order to access the text channels.'
# # type=8,
# # required=True
# # )
# # ]
# )
# async def _configure(self, ctx:SlashContext, option):
# await ctx.send(f'The `botrole` for the guild `{ctx.guild.name}` has been set to `{option.mention}`.')
# def setup(client):
# client.add_cog(Configuration(client))
client.run(os.getenv('TEST_3_TOKEN'))
# guilds = [
# 'guild1',
# 'guild2',
# 'guild3'
# ]
# configFile = './config.ini'
# config = configparser.RawConfigParser()
# for g in guilds:
# config.add_section(g)
# config.set(g,'botrole',f'<Bot Role> for {g}')
# config.set(g,'committeerole',f'<Committee Role> for {g}')
# with open(configFile,'w') as c:
# config.write(c)
# parser = configparser.ConfigParser()
# parser.read(configFile)
# print(parser.sections())
# print(parser['guild1']['botrole'])

View File

@ -1,4 +1,4 @@
version: '3.4'
version: '3.1'
services:
geasbot-app:
@ -6,24 +6,7 @@ services:
volumes:
- ./app:/usr/src/app
restart: always
depends_on:
- geasbot-db
environment:
- BOT_TOKEN=${BOT_TOKEN}
- TEST_TOKEN=${TEST_TOKEN}
- MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD}
- BOT_VERSION=${BOT_VERSION}
geasbot-db:
image: mongo:latest
volumes:
- ./db:/data/db
expose:
- "27017"
environment:
- BOT_TOKEN=${BOT_TOKEN}
- TEST_TOKEN=${TEST_TOKEN}
- MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD}
- BOT_VERSION=${BOT_VERSION}

33
maintenance/resources.md Normal file
View File

@ -0,0 +1,33 @@
# Resources for Maintaining the Bot
## Documentation
1. [Discord Py Documentation](https://discordpy.readthedocs.io/en/stable/index.html)
> 1. [Quickstart Guide](https://discordpy.readthedocs.io/en/stable/quickstart.html)
> 2. [Set up of Discord Bot Account](https://discordpy.readthedocs.io/en/stable/discord.html)
> 3. [**Important**: Primer to Gateway Intents](https://discordpy.readthedocs.io/en/stable/intents.html)
`N.B.: this is an important security feature of Discord that is now mandatory to configure and imposes restructions on some of the Bot's functionality unless appropriately configured. Keep an eye on this.`
> 4. [Repository with example code](https://github.com/Rapptz/discord.py/tree/v1.7.3/examples)
> 5. [Logging Setup](https://discordpy.readthedocs.io/en/stable/logging.html)
2. [Discord Py Slash Command Documentation](https://discord-py-slash-command.readthedocs.io/en/latest/index.html)
> 1. [Discord Py Slash Command Authentication](https://discord-py-slash-command.readthedocs.io/en/latest/quickstart.html)
`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.)
> 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)
## Tutorials
1. [YouTube Tutorial by Lucas, starting from the basics](https://www.youtube.com/watch?v=nW8c7vT6Hl4)
2. [YouTube tutorial on 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)
4. [YouTube tutorial on using the new Slash Command API](https://www.youtube.com/watch?v=CLQ8gfb2jh4)
5.
## Communities
### Discord Py
1. [Discord Py server](https://discord.gg/r3sSKJJ): Discord server to talk to others in the community
2. [Discord Py Github issue tracker](https://github.com/Rapptz/discord.py/issues): place to report bugs and issues with the API
3. [Discord Py discussion page](https://github.com/Rapptz/discord.py/discussions): wiki for any other discussions
### Discord Py Slash Commands
1. [Discord Py Slash Commands Discord Server](https://discord.gg/KkgMBVuEkx): Discord server with a forum to ask questions in
2. [Discord Py Slash Commands Issue Tracker on GitHub](https://github.com/discord-py-slash-commands/discord-py-interactions/issues)