예제 #1
0
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.")
예제 #2
0
 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())
예제 #3
0
 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)})."
     )
예제 #4
0
 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."))
예제 #5
0
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)
예제 #6
0
    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))
예제 #7
0
 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()
예제 #8
0
    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)
예제 #9
0
    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)
예제 #10
0
 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."))
예제 #11
0
    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()
예제 #12
0
    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())))
예제 #13
0
 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)
예제 #14
0
    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)
예제 #15
0
 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())))
예제 #16
0
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)
예제 #17
0
    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!"))
예제 #18
0
    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()
예제 #19
0
 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))
예제 #20
0
 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())))
예제 #21
0
 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")
예제 #22
0
    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)))
예제 #23
0
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.")
예제 #24
0
    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.")))
예제 #25
0
    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)
예제 #26
0
    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
예제 #27
0
    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)
예제 #28
0
    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"))