Skip to content

Commit 93add23

Browse files
committed
Implement all non-premium commands as slash commands
1 parent bd22e04 commit 93add23

File tree

13 files changed

+1810
-0
lines changed

13 files changed

+1810
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea/
2+
__pycache__/
3+
.DS_Store

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Command Worker
2+
3+
Receives interaction data from discord and responds to them.
4+
Long running tasks are forwarded to one of the background workers using redis

bot.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from xenon import *
2+
from xenon.cmd import *
3+
import dc_interactions as dc
4+
from motor.motor_asyncio import AsyncIOMotorClient
5+
import aioredis
6+
import json
7+
from os import environ as env
8+
import random
9+
import asyncio
10+
import traceback
11+
import sys
12+
from datetime import datetime
13+
14+
15+
class Xenon(dc.InteractionBot):
16+
def __init__(self, **kwargs):
17+
super().__init__(**kwargs, ctx_klass=CustomContext)
18+
self.mongo = AsyncIOMotorClient(env.get("MONGO_URL", "mongodb://localhost"))
19+
self.db = self.mongo.xenon
20+
self.redis = None
21+
self.http = None
22+
self.relay = None
23+
24+
self._receiver = aioredis.pubsub.Receiver()
25+
26+
async def on_command_error(self, ctx, e):
27+
if isinstance(e, asyncio.CancelledError):
28+
raise e
29+
30+
else:
31+
tb = "".join(traceback.format_exception(type(e), e, e.__traceback__))
32+
print("Command Error:\n", tb, file=sys.stderr)
33+
34+
error_id = unique_id()
35+
await self.redis.setex(f"cmd:errors:{error_id}", 60 * 60 * 24, json.dumps({
36+
"command": ctx.command.full_name,
37+
"timestamp": datetime.utcnow().timestamp(),
38+
"author": ctx.author.id,
39+
"traceback": tb
40+
}))
41+
await ctx.respond_with_source(**create_message(
42+
"An unexpected error occurred. Please report this on the "
43+
"[Support Server](https://xenon.bot/discord).\n\n"
44+
f"**Error Code**: `{error_id.upper()}`",
45+
f=Format.ERROR
46+
))
47+
48+
async def execute_command(self, command, payload, remaining_options):
49+
await self.redis.hincrby("cmd:commands", command.full_name, 1)
50+
51+
# Global rate limits to prevent abuse
52+
block_bucket = payload.guild_id or payload.member["user"]["id"]
53+
is_blacklisted = await self.redis.exists(f"cmd:blacklist:{block_bucket}")
54+
if is_blacklisted:
55+
await self.redis.incr("cmd:commands:blocked")
56+
await self.redis.setex(f"cmd:blacklist:{block_bucket}", random.randint(60 * 15, 60 * 60), 1)
57+
return dc.InteractionResponse.message(**create_message(
58+
"You are being **blocked from using Xenon commands** due to exceeding internal rate limits. "
59+
"These rate limits are in place to protect our infrastructure. Please be patient and wait a few hours "
60+
"before trying to run another command.",
61+
embed=False,
62+
f=Format.ERROR
63+
), ephemeral=True)
64+
65+
cmd_count = int(await self.redis.get(f"cmd:commands:{block_bucket}") or 0)
66+
if cmd_count > 5:
67+
await self.redis.setex(f"cmd:blacklist:{block_bucket}", random.randint(60 * 15, 60 * 60), 1)
68+
69+
else:
70+
await self.redis.setex(f"cmd:commands:{block_bucket}", 2, cmd_count + 1)
71+
72+
# Apply morph
73+
morph_target = await self.redis.get(f"cmd:morph:{payload.member['user']['id']}")
74+
if morph_target is not None:
75+
try:
76+
member = await self.http.get_guild_member(payload.guild_id, morph_target.decode())
77+
except rest.HTTPNotFound:
78+
pass
79+
else:
80+
guild = await self.http.get_guild(payload.guild_id)
81+
if guild.owner_id == member.id:
82+
perms = Permissions.all()
83+
84+
else:
85+
perms = Permissions.none()
86+
roles = sorted(guild.roles, key=lambda r: r.position)
87+
for role in roles:
88+
if role.id in member.roles:
89+
perms.value |= role.permissions.value
90+
91+
payload.morph_source = payload.member['user']['id']
92+
payload.member = member.to_dict()
93+
payload.member["permissions"] = perms.value
94+
95+
return await super().execute_command(command, payload, remaining_options)
96+
97+
async def gateway_subscriber(self):
98+
await self.redis.subscribe(
99+
self._receiver.channel("gateway:events:message_reaction_add")
100+
)
101+
async for channel, msg in self._receiver.iter():
102+
event_name = channel.name.decode().replace("gateway:events:", "")
103+
self.dispatch(event_name, json.loads(msg))
104+
105+
async def prepare(self):
106+
self.redis = await aioredis.create_redis_pool(env.get("REDIS_URL", "redis://localhost"))
107+
108+
self.relay = com.Relay(self.redis)
109+
self.relay.start_reader()
110+
111+
ratelimits = rest.RedisRatelimitHandler(self.redis)
112+
self.http = rest.HTTPClient(self.token, ratelimits)
113+
114+
self.loop.create_task(self.gateway_subscriber())
115+
116+
await super().prepare()
117+
# await self.flush_commands()
118+
self.loop.create_task(self.push_commands())
119+
120+
def make_request(self, method, path, data=None, **params):
121+
req = rest.Request(method, path, **params)
122+
self.http.start_request(req, json=data)
123+
return req

modules/admin.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import dc_interactions as dc
2+
from os import environ as env
3+
from xenon.cmd import *
4+
import inspect
5+
import textwrap
6+
import traceback
7+
import json
8+
from datetime import datetime
9+
10+
ADMIN_GUILD_ID = env.get("ADMIN_GUILD_ID", "496683369665658880")
11+
12+
13+
class AdminModule(dc.Module):
14+
@dc.Module.command(guild_id=ADMIN_GUILD_ID)
15+
async def admin(self, ctx):
16+
"""
17+
Manage the admin commands for a server
18+
"""
19+
20+
@admin.sub_command()
21+
@checks.is_bot_owner
22+
async def enable(self, ctx, guild_id=None):
23+
"""
24+
Add the admin commands to a server
25+
"""
26+
guild_id = guild_id or ctx.guild_id
27+
await ctx.ack_with_source()
28+
29+
for command in self.commands:
30+
if command.register:
31+
continue
32+
33+
await self.bot.create_command(command, guild_id=guild_id)
34+
35+
await ctx.respond_with_source(**create_message(
36+
f"Admin commands are now available on the server with the id `{guild_id}`",
37+
f=Format.SUCCESS
38+
))
39+
40+
@admin.sub_command()
41+
@checks.is_bot_owner
42+
async def disable(self, ctx, guild_id=None):
43+
"""
44+
Remove the admin commands from a server
45+
"""
46+
guild_id = guild_id or ctx.guild_id
47+
await ctx.ack_with_source()
48+
49+
existing_commands = await ctx.bot.fetch_commands(guild_id=guild_id)
50+
for command in self.commands:
51+
if command.register:
52+
continue
53+
54+
for ex_cmd in existing_commands:
55+
if ex_cmd["name"] == command.name:
56+
await self.bot.delete_command(ex_cmd["id"], guild_id=guild_id)
57+
58+
await ctx.respond_with_source(**create_message(
59+
f"Admin commands are no longer available on the server with the id `{guild_id}`",
60+
f=Format.SUCCESS
61+
))
62+
63+
@dc.Module.command(register=False)
64+
@checks.is_bot_owner
65+
async def maintenance(self, ctx):
66+
"""
67+
Enable or disable the maintenance mode
68+
"""
69+
current = await self.bot.redis.exists("cmd:maintenance")
70+
if current:
71+
await self.bot.redis.delete("cmd:maintenance")
72+
await ctx.respond_with_source(**create_message(
73+
"**Disabled maintenance** mode.",
74+
f=Format.SUCCESS
75+
))
76+
77+
else:
78+
await self.bot.redis.set("cmd:maintenance", "1")
79+
await ctx.respond_with_source(**create_message(
80+
"**Enabled maintenance** mode.",
81+
f=Format.SUCCESS
82+
))
83+
84+
@dc.Module.command(register=False)
85+
async def morph(self, ctx, user_id=None):
86+
"""
87+
Morph into and execute commands as a different user
88+
"""
89+
if user_id is None:
90+
morph_source = getattr(ctx.payload, "morph_source", None)
91+
if morph_source is None:
92+
await ctx.respond_with_source(**create_message(
93+
"You are already executing commands as yourself.",
94+
f=Format.SUCCESS
95+
))
96+
return
97+
98+
await ctx.bot.redis.delete(f"cmd:morph:{morph_source}")
99+
await ctx.respond_with_source(**create_message(
100+
"You are now executing commands as yourself.",
101+
f=Format.SUCCESS
102+
))
103+
return
104+
105+
check_result = await checks.is_bot_owner.run(ctx)
106+
if check_result is not True:
107+
return
108+
109+
await ctx.bot.redis.set(f"cmd:morph:{ctx.author.id}", user_id)
110+
await ctx.respond_with_source(**create_message(
111+
f"You are now executing commands as <@{user_id}>.",
112+
f=Format.SUCCESS
113+
))
114+
115+
@dc.Module.command(register=False)
116+
@checks.is_bot_owner
117+
async def eval(self, ctx, expression):
118+
"""
119+
Evaluate a python expression
120+
"""
121+
if expression.startswith("await "):
122+
expression = expression[6:]
123+
124+
try:
125+
result = eval(expression)
126+
if inspect.isawaitable(result):
127+
result = await result
128+
129+
except Exception as e:
130+
tb = "".join(traceback.format_exception(type(e), e, e.__traceback__))
131+
await ctx.respond_with_source(**create_message(
132+
f"```py\n{tb[:1900]}```",
133+
title="Eval Error",
134+
f=Format.SUCCESS
135+
))
136+
137+
else:
138+
await ctx.respond_with_source(**create_message(
139+
f"```py\n{result}```",
140+
title="Eval Result",
141+
f=Format.SUCCESS
142+
))
143+
144+
@dc.Module.command(register=False)
145+
@checks.is_bot_owner
146+
async def exec(self, ctx, snippet):
147+
"""
148+
Execute a python code snippet
149+
"""
150+
if snippet.startswith('```') and snippet.endswith('```'):
151+
snippet = '\n'.join(snippet.split('\n')[1:-1])
152+
153+
snippet = snippet.strip("` \n")
154+
wrapped = f"async def func():\n{textwrap.indent(snippet, ' ')}"
155+
156+
env = {
157+
"ctx": ctx,
158+
"self": self,
159+
"bot": ctx.bot,
160+
"http": ctx.bot.http,
161+
"redis": ctx.bot.redis
162+
}
163+
164+
try:
165+
exec(wrapped, env)
166+
result = await env["func"]()
167+
168+
except Exception as e:
169+
tb = "".join(traceback.format_exception(type(e), e, e.__traceback__))
170+
await ctx.respond_with_source(**create_message(
171+
f"```py\n{tb[:1900]}```",
172+
title="Exec Error",
173+
f=Format.SUCCESS
174+
))
175+
176+
else:
177+
await ctx.respond_with_source(**create_message(
178+
f"```py\n{result}```",
179+
title="Exec Result",
180+
f=Format.SUCCESS
181+
))
182+
183+
@dc.Module.command(register=False)
184+
@checks.is_bot_owner
185+
async def redis(self, ctx, cmd):
186+
"""
187+
Execute a redis command
188+
"""
189+
result = await ctx.bot.redis.execute(*cmd.split(" "))
190+
await ctx.respond_with_source(**create_message(
191+
f"```py\n{result}\n```",
192+
title="Redis Result",
193+
f=Format.SUCCESS
194+
))
195+
196+
@dc.Module.command(register=False)
197+
@checks.is_bot_owner
198+
async def error(self, ctx, error_id: str.lower):
199+
"""
200+
Show information about a command error
201+
"""
202+
error = await ctx.bot.redis.get(f"cmd:errors:{error_id}")
203+
if error is None:
204+
await ctx.respond_with_source(**create_message(
205+
f"**Unknown error** with the id `{error_id.upper()}`.",
206+
f=Format.ERROR
207+
))
208+
return
209+
210+
data = json.loads(error)
211+
embeds = [{
212+
"title": "Command Error",
213+
"color": Format.ERROR.color,
214+
"fields": [
215+
{
216+
"name": "Command",
217+
"value": data["command"],
218+
"inline": True
219+
},
220+
{
221+
"name": "Author",
222+
"value": f"<@{data['author']}>",
223+
"inline": True
224+
},
225+
{
226+
"name": "Timestamp",
227+
"value": datetime_to_string(datetime.fromtimestamp(data["timestamp"])) + " UTC",
228+
"inline": True
229+
}
230+
]
231+
}]
232+
233+
current = ""
234+
for line in data["traceback"].splitlines():
235+
if (len(current) + len(line)) > 2000:
236+
embeds.append({
237+
"color": Format.ERROR.color,
238+
"description": f"```py\n{current}```"
239+
})
240+
current = ""
241+
242+
else:
243+
current += f"\n{line}"
244+
245+
if len(current) > 0:
246+
embeds.append({
247+
"color": Format.ERROR.color,
248+
"description": f"```py\n{current}```"
249+
})
250+
251+
while len(embeds) > 0:
252+
await ctx.respond_with_source(embeds=embeds[:3])
253+
embeds = embeds[3:]
254+
255+
@dc.Module.command(register=False)
256+
@checks.is_bot_owner
257+
async def blacklist(self, ctx):
258+
"""
259+
Manage the blacklist
260+
"""

0 commit comments

Comments
 (0)