async def on_command_error(ctx, err): if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return if isinstance(err, commands.CommandInvokeError) and isinstance( err.original, ValueError): return await ctx.send(embed=util.error_embed( str(err.original), title=f"Error in {ctx.invoked_with}")) # TODO: really should find a way to detect ALL user errors here? if isinstance(err, (commands.UserInputError)): return await ctx.send(embed=util.error_embed( str(err), title=f"Error in {ctx.invoked_with}")) try: command_errors.inc() trace = re.sub( "\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__))) logging.error("Command error occured (in %s)", ctx.invoked_with, exc_info=err) await ctx.send(embed=util.error_embed( util.gen_codeblock(trace), title=f"Internal error in {ctx.invoked_with}")) await achievement.achieve(ctx.bot, ctx.message, "error") except Exception as e: logging.exception("Error in command error handling.")
async def remind(self, ctx, time, *, reminder): reminder = reminder.strip() if len(reminder) > 512: await ctx.send(embed=util.error_embed( "Maximum reminder length is 512 characters", "Foolish user error")) return extra_data = { "author_id": ctx.author.id, "channel_id": ctx.message.channel.id, "message_id": ctx.message.id, "guild_id": ctx.message.guild and ctx.message.guild.id, "original_time_spec": time } tz = await util.get_user_timezone(ctx) try: now = datetime.now(tz=timezone.utc) time = util.parse_time(time, tz) except: await ctx.send(embed=util.error_embed( "Invalid time (wrong format/too large months or years)")) return utc_time, local_time = util.in_timezone(time, tz) id = (await self.bot.database.execute_insert( "INSERT INTO reminders (remind_timestamp, created_timestamp, reminder, expired, extra) VALUES (?, ?, ?, ?, ?)", (utc_time.timestamp(), now.timestamp(), reminder, 0, util.json_encode(extra_data))))["last_insert_rowid()"] await self.bot.database.commit() await ctx.send( f"Reminder scheduled for {util.format_time(local_time)} ({util.format_timedelta(now, utc_time)})." ) self.insert_reminder(id, utc_time.timestamp())
async def remind(ctx, time, *, reminder): reminder = reminder.strip() if len(reminder) > 512: await ctx.send(embed=util.error_embed( "Maximum reminder length is 512 characters", "Foolish user error")) return extra_data = { "author_id": ctx.author.id, "channel_id": ctx.message.channel.id, "message_id": ctx.message.id, "guild_id": ctx.message.guild and ctx.message.guild.id, "original_time_spec": time } try: now = datetime.now(tz=timezone.utc) time = util.parse_time(time) except: await ctx.send(embed=util.error_embed( "Invalid time (wrong format/too large/non-integer months or years)" )) return await bot.database.execute( "INSERT INTO reminders (remind_timestamp, created_timestamp, reminder, expired, extra) VALUES (?, ?, ?, ?, ?)", (time.timestamp(), now.timestamp(), reminder, 0, util.json_encode(extra_data))) await bot.database.commit() await ctx.send( f"Reminder scheduled for {util.format_time(time)} ({util.format_timedelta(now, time)})." )
async def end_contest(self, ctx): if not DB.get_current_contest(): return await ctx.channel.send( embed=error_embed("No contests are active right now.")) await self.handle_end_contest() return await ctx.channel.send( embed=error_embed("Contest forcefully ended."))
async def exec(ctx, *, arg): match = re.match(EXEC_REGEX, arg, flags=re.DOTALL) if match == None: await ctx.send( embed=util.error_embed("Invalid format. Expected a codeblock.")) return flags_raw = match.group(1) flags = exec_flag_parser.parse_args(flags_raw.split()) lang = flags.language or match.group(2) if not lang: await ctx.send(embed=util.error_embed( "No language specified. Use the -L flag or add a language to your codeblock." )) return lang = lang.strip() code = match.group(3) async with ctx.typing(): ok, real_lang, result, debug = await tio.run(lang, code) if not ok: await ctx.send(embed=util.error_embed(util.gen_codeblock(result), "Execution failed")) else: out = result if flags.verbose: debug_block = "\n" + util.gen_codeblock( f"""{debug}\nLanguage: {real_lang}""") out = out[:2000 - len(debug_block)] + debug_block else: out = out[:2000] await ctx.send(out)
async def change_end_time(self, ctx, end_time_str: str): if not DB.is_contest_running(): return await ctx.channel.send(embed=error_embed("No contests are active.")) contest: Contest = DB.get_current_contest() try: end_time = datetime.strptime(end_time_str, "%m/%d/%y %H:%M") except ValueError: return await ctx.channel.send(embed=error_embed("Invalid time input.")) DB.change_end_time(end_time) await ctx.channel.send(embed=success_embed( "Contest now runs until (UTC): {}".format(end_time_str) )) embed = contest_post_embed(end_time) try: msg = await Settings.sign_up_channel.fetch_message(contest.post_id) await msg.edit(embed=embed) except Exception as e: logging.error("Failed to change contest post for contest {}\n".format( contest.id) + str(e))
async def refresh_schedule(self, ctx): if DB.get_current_contest(): return await ctx.channel.send( embed=error_embed("A contest is already active.")) if not DB.get_ready_contest(): return await ctx.channel.send( embed=error_embed("No new contests to start.")) return await self.start_contest_from_schedule()
async def ban(self, ctx, user: discord.Member): if not DB.is_contest_running(): return await ctx.send( embed=error_embed("No active contest to ban from.")) if not user: return await ctx.send(embed=error_embed("Invalid user.")) DB.ban_user(DB.get_current_contest_id(), user.id) user.send(embed=error_embed( "You have been banned from participating in the contest.")) Logger.banned_user(ctx.author, user)
async def unban(self, ctx, user: discord.Member): if not DB.is_contest_running(): return await ctx.send( embed=error_embed("No active contest to unban from.")) if not user: return await ctx.send(embed=error_embed("Invalid user.")) DB.unban_user(DB.get_current_contest_id(), user.id) user.send( embed=success_embed("You have been unbanned from the contest. \ All characters in this contest before the ban can be edited again." )) Logger.unbanned_user(ctx.author, user)
async def start(self): self.old_char: Character = DB.get_character( self.contest_id, self.user.id) if self.old_char: return await self.show_old_char_menu() else: return await self.user.send(embed=error_embed("You don't have a character to edit."))
async def upload_submission(self): member: discord.Member = discord.utils.get( Settings.guild.members, id=self.user.id) if member is None: logging.error("Member not in guild. Aborting submission.") return delta_points = self.old_char.get_delta_points(self.keywords) if not delta_points: return await self.user.send(embed=error_embed("Your submission was not submitted since you did not add any points.")) embed = discord.Embed(title="{}\t({})".format( member.display_name, self.old_char.rotmg_class)) embed.add_field(name="Items/Achievements", value="`{}`".format(str(self.keywords)), inline=False) embed.add_field( name="Points", value="**{}**".format(str(delta_points)), inline=False) embed.add_field(name="Proof", value="[image]({})".format( str(self.img_url)), inline=False) embed.set_image(url=self.img_url) self.post = await Settings.submission_channel.send(embed=embed) await self.post.add_reaction(Settings.accept_emoji) await self.post.add_reaction(Settings.reject_emoji) await self.finished()
async def py(ctx, *, code): "Executes Python. You may supply a codeblock. Comments in the form #timeout:([0-9]+) will be used as a timeout specifier. React with :x: to stop, probably." timeout = 5.0 timeout_match = re.search("#timeout:([0-9]+)", code, re.IGNORECASE) if timeout_match: timeout = int(timeout_match.group(1)) if timeout == 0: timeout = None code = util.extract_codeblock(code) try: loc = { **locals(), "bot": bot, "ctx": ctx, "db": bot.database, "util": util, "eventbus": eventbus } def check(re, u): return str(re.emoji) == "❌" and u == ctx.author result = None async def run(): nonlocal result result = await util.async_exec(code, loc, globals()) halt_task = asyncio.create_task( bot.wait_for("reaction_add", check=check)) exec_task = asyncio.create_task(run()) done, pending = await asyncio.wait( (exec_task, halt_task), timeout=timeout, return_when=asyncio.FIRST_COMPLETED) for task in done: task.result() # get exceptions for task in pending: task.cancel() if result != None: if isinstance(result, str): await ctx.send(result[:2000]) else: await ctx.send(util.gen_codeblock(repr(result))) except (TimeoutError, asyncio.CancelledError): await ctx.send(embed=util.error_embed("Timed out.")) except BaseException as e: await ctx.send(embed=util.error_embed( util.gen_codeblock(traceback.format_exc())))
async def connect(ctx, thing="main", channel_id: int = None): thing_url = util.config["radio_urls"].get(thing, None) if thing_url == None: return await ctx.send(embed=util.error_embed("No such radio thing.")) if channel_id: channel = await bot.fetch_channel(channel_id) if not channel: return await ctx.send(embed=util.error_embed("No such channel.")) else: voice = ctx.author.voice if not voice: return await ctx.send(embed=util.error_embed("You are not in a voice channel.")) if voice.mute: return await ctx.send(embed=util.error_embed("You are muted.")) channel = voice.channel existing = ctx.guild.voice_client if existing: await existing.disconnect() vc = await channel.connect() src = HTTPSource(thing_url) await src.start() vc.play(src)
async def update_leaderboard(self, ctx): if not DB.is_contest_running(): return await ctx.channel.send(embed=error_embed("No contests are active.")) await Leaderboard.update() await Leaderboard.display(self.bot) await ctx.channel.send(embed=success_embed("Leaderboard updated.")) return await Logger.updated_leaderboard(ctx.author)
async def py(ctx, *, code): code = util.extract_codeblock(code) try: loc = {**locals(), "bot": bot, "ctx": ctx, "db": bot.database} result = await asyncio.wait_for(util.async_exec( code, loc, globals()), timeout=5.0) if result != None: if isinstance(result, str): await ctx.send(result[:1999]) else: await ctx.send(util.gen_codeblock(repr(result))) except TimeoutError: await ctx.send(embed=util.error_embed("Timed out.")) except BaseException as e: await ctx.send(embed=util.error_embed( util.gen_codeblock(traceback.format_exc())))
async def on_command_error(ctx, err): #print(ctx, err) if isinstance(err, (commands.CommandNotFound, commands.CheckFailure)): return if isinstance(err, commands.MissingRequiredArgument): return await ctx.send(embed=util.error_embed(str(err))) try: trace = re.sub( "\n\n+", "\n", "\n".join(traceback.format_exception(err, err, err.__traceback__))) #print(trace) logging.error("command error occured (in %s)", ctx.invoked_with, exc_info=err) await ctx.send(embed=util.error_embed(util.gen_codeblock(trace), title="Internal error")) except Exception as e: print("meta-error:", e)
async def on_raw_reaction_add(self, payload): try: user: discord.User = self.bot.get_user(payload.user_id) member: discord.Member = Settings.guild.get_member(payload.user_id) except Exception: return if user == self.bot.user or not user: return if not DB.is_contest_running(): return is_on_contest_post = DB.get_contest_post_id() == payload.message_id if not is_on_contest_post: return if payload.emoji == Settings.grave_emoji: if Settings.contestant_role not in member.roles: return await user.send(embed=error_embed( "You need to sign up before you can submit or edit a character." )) try: return await ProcessManager.spawn( payload.user_id, NewCharacter(self.bot, user, DB.get_current_contest_id())) except BusyException: return await user.send(embed=user_busy_embed()) elif str(payload.emoji) == Settings.edit_emoji: if Settings.contestant_role not in member.roles: return await user.send(embed=error_embed( "You need to sign up before you can submit or edit a character." )) try: return await ProcessManager.spawn( payload.user_id, EditCharacter(self.bot, user, DB.get_current_contest_id())) except BusyException: return await user.send(embed=user_busy_embed()) elif str(payload.emoji) == Settings.accept_emoji: if Settings.contestant_role not in member.roles: await member.add_roles(Settings.contestant_role) return await user.send(embed=success_embed( "You are now part of the contest. Good luck!"))
async def show_current_character(self): self.old_char: Character = DB.get_character_by_id(self.character_id) if not self.old_char: return await self.user.send( embed=error_embed("That character doesn't exist.")) title_embed = discord.Embed(title="Current Character:") embed = character_embed(self.old_char) await self.user.send(embed=title_embed) await self.user.send(embed=embed) return await self.keyword_menu()
async def view_bans(self, ctx): if not DB.is_contest_running(): return await ctx.send(embed=error_embed("No active contest.")) data = DB.get_ban_list(DB.get_current_contest_id()) mentions = [] for i in data: user = self.bot.get_user(int(i)) if not user: continue mentions.append(user.mention) mention_str = '\n'.join(i for i in mentions) return await ctx.send(embed=success_embed("Banned users:\n " + mention_str))
async def sql(ctx, *, code): code = util.extract_codeblock(code) try: csr = bot.database.execute(code) out = "" async with csr as cursor: async for row in cursor: out += " ".join(map(repr, row)) + "\n" await ctx.send(util.gen_codeblock(out)) await bot.database.commit() except Exception as e: await ctx.send(embed=util.error_embed( util.gen_codeblock(traceback.format_exc())))
async def delete(self, ctx, *keys): "Delete the specified keys (smallest scope first)." for key in keys: row = await self.get_userdata(ctx.author.id, ctx.guild and ctx.guild.id, key) if not row: return await ctx.send( embed=util.error_embed(f"No such key {key}")) await self.bot.database.execute( "DELETE FROM user_data WHERE user_id = ? AND guild_id = ? AND key = ?", (ctx.author.id, row["guild_id"], key)) await self.bot.database.commit() await ctx.send(f"**{key}** deleted")
async def schedule_contest(self, ctx, start_time_str: str, end_time_str: str): try: start_time = datetime.strptime(start_time_str, "%m/%d/%y %H:%M") end_time = datetime.strptime(end_time_str, "%m/%d/%y %H:%M") except ValueError: return await ctx.channel.send( embed=error_embed("Invalid time input.")) DB.schedule_contest(start_time, end_time) return await ctx.channel.send(embed=success_embed( "Contest added.\nStart time (UTC): {}\nEnd time (UTC): {}".format( start_time_str, end_time)))
async def delete(ctx, *, raw_target): target = await clean(ctx, raw_target.strip().replace("\n", " ")) if len(target) > 256: await ctx.send( embed=util.error_embed("Deletion target must be max 256 chars")) return async with ctx.typing(): await ctx.send(f"Deleting {target}...") await asyncio.sleep(1) await bot.database.execute( "INSERT INTO deleted_items (timestamp, item) VALUES (?, ?)", (util.timestamp(), target)) await bot.database.commit() await ctx.send(f"Deleted {target} successfully.")
async def hangup(self, ctx): channel_info = await self.get_channel_config(ctx.channel.id) addr = channel_info["id"] if not channel_info: return await ctx.send( embed=util.error_embed("Not in a phone channel.")) from_here = await self.bot.database.execute_fetchone( "SELECT * FROM calls WHERE from_id = ?", (addr, )) to_here = await self.bot.database.execute_fetchone( "SELECT * FROM calls WHERE to_id = ?", (addr, )) if (not to_here) and (not from_here): return await ctx.send( embed=util.error_embed("No calls are active.")) other = None if from_here: other = from_here["to_id"] await self.bot.database.execute( "DELETE FROM calls WHERE from_id = ? AND to_id = ?", (addr, other)) elif to_here: other = to_here["from_id"] await self.bot.database.execute( "DELETE FROM calls WHERE to_id = ? AND from_id = ?", (addr, other)) await self.bot.database.commit() other_channel = (await self.get_addr_config(other))["channel_id"] await eventbus.remove_bridge_link(self.bot.database, ("discord", other_channel), ("discord", ctx.channel.id)) await asyncio.gather( ctx.send(embed=util.info_embed("Hung up", f"Call to {other} disconnected.")), self.bot.get_channel(other_channel).send(embed=util.info_embed( "Hung up", f"Call to {addr} disconnected.")))
async def profile(self, ctx, other_user: typing.Optional[discord.Member] = None): if not DB.is_contest_running(): return await ctx.send(embed=error_embed("No contests are active.")) user: discord.Member = other_user if other_user else ctx.author assert (user) embeds = [ discord.Embed(title="{}'s Characters".format(user.display_name)) ] char_list = DB.get_characters_by_user(DB.get_current_contest_id(), user.id) embeds += [character_embed(c) for c in char_list] for e in embeds: await ctx.author.send(embed=e)
async def on_raw_reaction_add(self, payload): try: user: discord.User = self.bot.get_user(payload.user_id) member: discord.Member = Settings.guild.get_member(payload.user_id) except Exception: return if user == self.bot.user or not user: return if payload.channel_id != Settings.submission_channel.id: return if str(payload.emoji) == Settings.accept_emoji: # Accept submission submission, user_id = DB.accept_submission(payload.message_id) if not submission: return try: msg = await Settings.submission_channel.fetch_message(payload.message_id) await msg.delete() other_user = self.bot.get_user(user_id) await other_user.send(embed=success_embed("You submission with ID `{}` was accepted!".format(submission.post_id))) return await Logger.accepted_submission(user, submission) except Exception as e: logging.error( "Failed to delete submission message and/or notify user.\n" + str(e)) return elif str(payload.emoji) == Settings.reject_emoji: # Reject submission submission, user_id = DB.get_submission(payload.message_id) if not submission: return try: msg = await Settings.submission_channel.fetch_message(payload.message_id) await msg.delete() other_user = self.bot.get_user(user_id) await other_user.send(embed=error_embed("You submission with ID `{}` was denied!".format(submission.post_id))) return await Logger.rejected_submission(user, submission) except Exception as e: logging.error( "Failed to delete submission message and/or notify user.\n" + str(e)) return
async def view_schedule(self, ctx): schedule: List[Contest] = DB.get_schedule() if not schedule: return await ctx.channel.send( embed=error_embed("No upcoming contests.")) table = [] for contest in schedule: start_time_str = contest.start_time.strftime("%m/%d/%y %H:%M") end_time_str = contest.end_time.strftime("%m/%d/%y %H:%M") table.append([str(contest.id), start_time_str, end_time_str]) embed = discord.Embed(title="Upcoming Contests", color=0x00FF00) embed.description = "All times are in UTC." table_str = tabulate(table, headers=["ID", "Start Time", "End Time"]) embed.add_field(name="Schedule", value="```{}```".format(table_str), inline=False) return await ctx.channel.send(embed=embed)
async def dial(self, ctx, address): # basic checks - ensure this is a phone channel and has no other open calls channel_info = await self.get_channel_config(ctx.channel.id) if not channel_info: return await ctx.send( embed=util.error_embed("Not in a phone channel.")) originating_address = channel_info["id"] if address == originating_address: return await ctx.send(embed=util.error_embed( "A channel cannot dial itself. That means *you*, Gibson.")) recv_info = await self.get_addr_config(address) if not recv_info: return await ctx.send(embed=util.error_embed( "Destination address not found. Please check for typos and/or antimemes." )) current_call = await self.bot.database.execute_fetchone( "SELECT * FROM calls WHERE from_id = ?", (originating_address, )) if current_call: return await ctx.send(embed=util.error_embed( f"A call is already open (to {current_call['to_id']}) from this channel. Currently, only one outgoing call is permitted at a time." )) # post embed in the receiving channel prompting people to accept/decline call recv_channel = self.bot.get_channel(recv_info["channel_id"]) _, call_message = await asyncio.gather( ctx.send(embed=util.info_embed("Outgoing call", f"Dialing {address}...")), recv_channel.send(embed=util.info_embed( "Incoming call", f"Call from {originating_address}. Click :white_check_mark: to accept or :negative_squared_cross_mark: to decline." ))) # add clickable reactions to it await asyncio.gather(call_message.add_reaction("✅"), call_message.add_reaction("❎")) def check(re, u): return (str(re.emoji) == "✅" or str(re.emoji) == "❎") and u != self.bot.user reaction = None # wait until someone clicks the reactions, or time out and say so try: reaction, user = await self.bot.wait_for( "reaction_add", timeout=util.config["call_timeout"], check=check) except asyncio.TimeoutError: await asyncio.gather( ctx.send(embed=util.error_embed( "Timed out", "Outgoing call timed out - the other end did not pick up.") ), recv_channel.send(embed=util.error_embed( "Timed out", "Call timed out - no response in time"))) await asyncio.gather(call_message.remove_reaction("✅", self.bot.user), call_message.remove_reaction("❎", self.bot.user)) em = str(reaction.emoji) if reaction else "❎" if em == "✅": # accept call await self.bot.database.execute( "INSERT INTO calls VALUES (?, ?, ?)", (originating_address, address, util.timestamp())) await self.bot.database.commit() await eventbus.add_bridge_link(self.bot.database, ("discord", ctx.channel.id), ("discord", recv_channel.id), "telephone") await asyncio.gather( ctx.send(embed=util.info_embed( "Outgoing call", "Call accepted and connected.")), recv_channel.send(embed=util.info_embed( "Incoming call", "Call accepted and connected."))) elif em == "❎": # drop call await ctx.send(embed=util.error_embed("Your call was declined.", "Call declined"))