Skip to content

Commit 462cd98

Browse files
committed
Dynamic help and config resolves #84
1 parent c2f4cb4 commit 462cd98

File tree

5 files changed

+171
-36
lines changed

5 files changed

+171
-36
lines changed

bot.py

+13-30
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,23 @@
5151

5252
from core.paginator import PaginatorSession
5353
from core.api import Github, ModmailApiClient
54-
from core.thread import ThreadManager, Thread
54+
from core.thread import ThreadManager
55+
from core.config import ConfigManager
5556

5657
line = Fore.BLACK + Style.BRIGHT + '-------------------------' + Style.RESET_ALL
5758

5859
class ModmailBot(commands.Bot):
5960

60-
'''Commands directly related to Modmail functionality.'''
61+
mutable_config_keys = ['prefix', 'status', 'guild_id', 'mention']
6162

6263
def __init__(self):
6364
super().__init__(command_prefix=self.get_pre)
6465
self.start_time = datetime.datetime.utcnow()
6566
self.threads = ThreadManager(self)
67+
self.session = aiohttp.ClientSession(loop=self.loop)
68+
self.config = ConfigManager(self)
69+
self.modmail_api = ModmailApiClient(self)
6670
self.data_task = self.loop.create_task(self.data_loop())
67-
6871
self._add_commands()
6972

7073
def _add_commands(self):
@@ -79,10 +82,6 @@ def _add_commands(self):
7982
print('Author: kyb3r' + Style.RESET_ALL)
8083
print(line + Fore.CYAN)
8184

82-
for attr in dir(self):
83-
cmd = getattr(self, attr)
84-
if isinstance(cmd, commands.Command):
85-
self.add_command(cmd)
8685

8786
for file in os.listdir('cogs'):
8887
if not file.endswith('.py'):
@@ -101,32 +100,18 @@ def run(self):
101100
super().run(self.token)
102101
finally:
103102
print(Fore.CYAN + ' Shutting down bot' + Style.RESET_ALL)
104-
105-
@property
106-
def config(self):
107-
try:
108-
with open('config.json') as f:
109-
config = json.load(f)
110-
except FileNotFoundError:
111-
config = {}
112-
config.update(os.environ)
113-
return config
114103

115104
@property
116105
def snippets(self):
117-
return {
118-
key.split('_')[1].lower(): val
119-
for key, val in self.config.items()
120-
if key.startswith('SNIPPET_')
121-
}
106+
return self.config.get('snippets', {})
122107

123108
@property
124109
def token(self):
125-
return self.config.get('TOKEN')
110+
return self.config.token
126111

127112
@property
128113
def guild_id(self):
129-
return int(self.config.get('GUILD_ID'))
114+
return int(self.config.guild_id)
130115

131116
@property
132117
def guild(self):
@@ -146,16 +131,14 @@ def blocked_users(self):
146131
@staticmethod
147132
async def get_pre(bot, message):
148133
'''Returns the prefix.'''
149-
p = bot.config.get('PREFIX') or 'm.'
134+
p = bot.config.get('prefix') or 'm.'
150135
return [p, f'<@{bot.user.id}> ', f'<@!{bot.user.id}> ']
151136

152137
async def on_connect(self):
153138
print(line)
154139
print(Fore.CYAN + 'Connected to gateway.')
155-
156-
self.session = aiohttp.ClientSession()
157-
self.modmail_api = ModmailApiClient(self)
158-
status = os.getenv('STATUS') or self.config.get('STATUS')
140+
141+
status = self.config.get('status')
159142
if status:
160143
await self.change_presence(activity=discord.Game(status))
161144

@@ -201,7 +184,7 @@ async def on_message(self, message):
201184
if isinstance(message.channel, discord.DMChannel):
202185
return await self.process_modmail(message)
203186

204-
prefix = self.config.get('PREFIX', 'm.')
187+
prefix = self.config.get('prefix', 'm.')
205188

206189
if message.content.startswith(prefix):
207190
cmd = message.content[len(prefix):].strip()

cogs/utility.py

+88-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from discord.ext import commands
33
import traceback
44
import inspect
5+
import json
56
import io
67
import textwrap
78
from contextlib import redirect_stdout
@@ -10,6 +11,8 @@
1011
from core.paginator import PaginatorSession
1112
from core.decorators import auth_required, owner_only, trigger_typing
1213

14+
from prettytable import PrettyTable
15+
1316
class Utility:
1417
'''General commands that provide utility'''
1518

@@ -19,7 +22,7 @@ def __init__(self, bot):
1922
def format_cog_help(self, ctx, cog):
2023
"""Formats the text for a cog help"""
2124
sigs = []
22-
prefix = self.bot.config.get('PREFIX', 'm.')
25+
prefix = self.bot.config.get('prefix', 'm.')
2326

2427
for cmd in self.bot.commands:
2528
if cmd.hidden:
@@ -72,7 +75,7 @@ def format_cog_help(self, ctx, cog):
7275

7376
def format_command_help(self, ctx, cmd):
7477
'''Formats command help.'''
75-
prefix = self.bot.config.get('PREFIX', 'm.')
78+
prefix = self.bot.config.get('prefix', 'm.')
7679
em = discord.Embed(
7780
color=discord.Color.green(),
7881
description=cmd.help
@@ -138,7 +141,7 @@ async def help(self, ctx, *, command: str=None):
138141

139142
pages = []
140143

141-
prefix = self.bot.config.get('PREFIX', 'm.')
144+
prefix = self.bot.config.get('prefix', 'm.')
142145

143146
for _, cog in sorted(self.bot.cogs.items()):
144147
em = self.format_cog_help(ctx, cog)
@@ -205,7 +208,7 @@ async def github(self, ctx):
205208
data = await self.bot.modmail_api.get_user_info()
206209
print(data)
207210

208-
prefix = self.bot.config.get('PREFIX', 'm.')
211+
prefix = self.bot.config.get('prefix', 'm.')
209212

210213
em = discord.Embed(title='Github')
211214

@@ -282,6 +285,87 @@ async def ping(self, ctx):
282285
em.color = 0x00FF00
283286
await ctx.send(embed=em)
284287

288+
@commands.group()
289+
@owner_only()
290+
async def config(self, ctx):
291+
'''Change configuration for the bot.'''
292+
if ctx.invoked_subcommand is None:
293+
cmd = self.bot.get_command('help')
294+
await ctx.invoke(cmd, command='config')
295+
296+
@config.command(name='set')
297+
async def _set(self, ctx, key: str.lower, *, value):
298+
'''Sets a configuration variable and its value'''
299+
300+
em = discord.Embed(
301+
title='Success',
302+
color=discord.Color.green(),
303+
description=f'Set `{key}` to `{value}`'
304+
)
305+
306+
if key not in self.bot.mutable_config_keys:
307+
em.title='Error'
308+
em.color=discord.Color.green()
309+
em.description=f'{key} is an invalid key.'
310+
valid_keys = [f'`{k}`' for k in self.bot.mutable_config_keys]
311+
em.add_field(name='Valid keys', value=', '.join(valid_keys))
312+
else:
313+
await self.bot.config.update({key: value})
314+
315+
await ctx.send(embed=em)
316+
317+
@config.command(name='del')
318+
async def _del(self, ctx, key: str.lower):
319+
'''Sets a specified key from the config to nothing.'''
320+
em = discord.Embed(
321+
title='Success',
322+
color=discord.Color.green(),
323+
description=f'Set `{key}` to nothing.'
324+
)
325+
326+
if key not in self.bot.mutable_config_keys:
327+
em.title='Error'
328+
em.color=discord.Color.green()
329+
em.description=f'{key} is an invalid key.'
330+
valid_keys = [f'`{k}`' for k in self.bot.mutable_config_keys]
331+
em.add_field(name='Valid keys', value=', '.join(valid_keys))
332+
else:
333+
self.bot.config.cache[key] = None
334+
await self.bot.config.update()
335+
336+
await ctx.send(embed=em)
337+
338+
@config.command(name='get')
339+
async def get(self, ctx, key=None):
340+
'''Shows the config variables that are currently set.'''
341+
em = discord.Embed(color=discord.Color.green())
342+
em.set_author(name='Current config', icon_url=self.bot.user.avatar_url)
343+
344+
if key and key not in self.bot.mutable_config_keys:
345+
em.title='Error'
346+
em.color=discord.Color.green()
347+
em.description=f'`{key}` is an invalid key.'
348+
valid_keys = [f'`{k}`' for k in self.bot.mutable_config_keys]
349+
em.add_field(name='Valid keys', value=', '.join(valid_keys))
350+
elif key:
351+
em.set_author(name='Config variable', icon_url=self.bot.user.avatar_url)
352+
em.description = f'`{key}` is set to `{self.bot.config.get(key)}`'
353+
else:
354+
em.description = 'Here is a list of currently set configuration variables.'
355+
356+
config = {
357+
k: v for k, v in self.bot.config.cache.items()
358+
if v and k in self.bot.mutable_config_keys
359+
}
360+
361+
for k, v in reversed(list(config.items())):
362+
em.add_field(name=k, value=f'`{v}`')
363+
364+
await ctx.send(embed=em)
365+
366+
367+
368+
285369
@commands.command(hidden=True, name='eval')
286370
@owner_only()
287371
async def _eval(self, ctx, *, body):

core/api.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ class ModmailApiClient(ApiClient):
3030
base = 'https://api.modmail.tk'
3131
github = base + '/github'
3232
logs = base + '/logs'
33+
config = base + '/config'
3334

3435
def __init__(self, bot):
3536
super().__init__(bot)
36-
self.token = bot.config.get('MODMAIL_API_TOKEN')
37+
self.token = bot.config.get('modmail_api_token')
3738
if self.token:
3839
self.headers = {
3940
'Authorization': 'Bearer ' + self.token
@@ -53,6 +54,15 @@ def get_user_logs(self, user_id):
5354

5455
def get_log(self, channel_id):
5556
return self.request(self.logs + '/' + str(channel_id))
57+
58+
def get_config(self):
59+
return self.request(self.config)
60+
61+
def update_config(self, data):
62+
valid_keys = ['prefix', 'status', 'owners', 'guild_id', 'mention', 'snippets']
63+
64+
data = {k: v for k, v in data.items() if k in valid_keys}
65+
return self.request(self.config, method='PATCH', payload=data)
5666

5767
def get_log_url(self, recipient, channel, creator):
5868
return self.request(self.logs + '/key', payload={

core/config.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import asyncio
2+
import os
3+
import json
4+
import box
5+
6+
class ConfigManager:
7+
8+
def __init__(self, bot):
9+
self.bot = bot
10+
self.cache = box.Box()
11+
self._modified = True
12+
self.populate_cache()
13+
14+
@property
15+
def api(self):
16+
return self.bot.modmail_api
17+
18+
def populate_cache(self):
19+
try:
20+
data = json.load(open('config.json'))
21+
except FileNotFoundError:
22+
data = {}
23+
finally:
24+
data.update(os.environ)
25+
data = {k.lower(): v for k, v in data.items()}
26+
self.cache = data
27+
28+
self.bot.loop.create_task(self.refresh())
29+
30+
async def update(self, data=None):
31+
'''Updates the config with data from the cache'''
32+
self._modified = False
33+
if data is not None:
34+
self.cache.update(data)
35+
await self.api.update_config(self.cache)
36+
37+
async def refresh(self):
38+
'''Refreshes internal cache with data from database'''
39+
data = await self.api.get_config()
40+
self.cache.update(data)
41+
42+
def __getattr__(self, value):
43+
return self.cache[value]
44+
45+
def get(self, value, default=None):
46+
return self.cache.get(value) or default
47+
48+
@property
49+
def modified(self):
50+
return self._modified
51+
52+
@modified.setter
53+
def modified(self, flag):
54+
'''If set to true, update() will be called'''
55+
if flag is True:
56+
asyncio.create_task(self.update())
57+
self._modified = True

core/decorators.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async def wrapper(self, ctx, *args, **kwargs):
2424

2525
def owner_only():
2626
async def predicate(ctx):
27-
allowed = [int(x) for x in ctx.bot.config.get('OWNERS', '0').split(',')]
27+
allowed = [int(x) for x in str(ctx.bot.config.get('owners', '0')).split(',')]
28+
print(allowed)
2829
return ctx.author.id in allowed
2930
return commands.check(predicate)

0 commit comments

Comments
 (0)