コード例 #1
0
class NewMembers(commands.Cog):
    """Private feedback system."""

    config = None
    delete_after = 15

    def __init__(self, bot):
        self.bot = bot
        NewMembers.bot = self
        self.cogset = dict()
        self.roles = dict()
        self.db = None
        self.tguild = None
        self.jobstore = SQLAlchemyJobStore(
            url=
            fr'sqlite:///{pathlib.Path.cwd() / "data" / "jobs" / "newmembers_jobstore.sqlite"}'
        )
        jobstores = {"default": self.jobstore}
        self.scheduler = AsyncIOScheduler(jobstores=jobstores)
        self.scheduler.add_listener(self.job_missed, events.EVENT_JOB_MISSED)

# -------------------- LOCAL COG STUFF --------------------

    async def connect_db(self):
        """
        Connects to the database using variables set in the dblogin.py file.
        """

        credentials = {
            "user": dblogin.user,
            "password": dblogin.pwrd,
            "database": dblogin.name,
            "host": dblogin.host
        }
        self.db = await asyncpg.create_pool(**credentials)

        return

    async def disconnet_db(self):
        """
        Closes the connection to the database.
        """
        await self.db.close()

        return

    @asyncio.coroutine
    async def cog_command_error(self, ctx, error):
        print('Ignoring exception in {}'.format(ctx.invoked_with),
              file=sys.stderr)
        print(error)
        return

    def cog_unload(self):
        pass

# -------------------- STATIC METHODS --------------------

    @staticmethod
    def time_pat_to_secs(t):
        '''
        Converts a string in format <xDxHxMxS> (d for days, h for hours, M for minutes, S for seconds) to amount of seconds.
        eg: 3d5h would be 77 hours

        Args:
            (str) or (int)

        Returns:
            (int) or (None)
        '''

        try:
            total_seconds = int(t)
            return total_seconds

        except ValueError:
            valid = False

            #===== if input doesn't match basic pattern
            if (re.match(r"(\d+[DHMSdhms])+", t)):

                #=== if all acsii chars in the string are unique
                letters = re.findall(r"[DHMSdhms]", t)
                if len(letters) == len(set(letters)):

                    #= if more then 1 letter side by side
                    #= ie. if t was 2dh30m then after the split you'd have ['', 'dh', 'm', '']
                    if not ([i for i in re.split(r"[0-9]", t) if len(i) > 1]):

                        # if letters are in order.
                        if letters == sorted(
                                letters,
                                key=lambda letters: ["d", "h", "m", "s"].index(
                                    letters[0])):
                            valid = True

            if valid:
                total_seconds = int()

                for data in re.findall(r'(\d+[DHMSdhms])', t):
                    if data.endswith("d"):
                        total_seconds += int(data[:-1]) * 86400
                    if data.endswith("h"):
                        total_seconds += int(data[:-1]) * 3600
                    if data.endswith("m"):
                        total_seconds += int(data[:-1]) * 60
                    if data.endswith("s"):
                        total_seconds += int(data[:-1])

            return total_seconds

        return False

# -------------------- LISTENERS --------------------

    @commands.Cog.listener()
    async def on_ready(self):
        # ---------- LOAD COGSET ----------
        self.cogset = await cogset.LOAD(cogname=self.qualified_name)
        if not self.cogset:
            self.cogset = dict(NMlastmsgid=0,
                               NMlastchid=0,
                               guildclosed=False,
                               agreeoff=False)

            await cogset.SAVE(self.cogset, cogname=self.qualified_name)

    # ---------- WAIT FOR BOT TO RUN ON_READY ----------
        await asyncio.sleep(5)

        # ---------- LOG INVITES ----------
        inviteLog = await self.__get_invite_info()

        if inviteLog is not None:
            await self.bot.db.execute(pgCmds.ADD_INVITES,
                                      json.dumps(inviteLog))
            self.bot.safe_print("[Log] Invite information has been logged.")

        else:
            self.bot.safe_print("[Log] No invite information to log.")

    # ---------- GET IMPORTANT ROLES READY ----------
        self.tguild = self.bot.get_guild(self.bot.config.target_guild_id)

        self.roles['member'] = discord.utils.get(
            self.tguild.roles, id=self.bot.config.roles['member'])
        self.roles['newmember'] = discord.utils.get(
            self.tguild.roles, id=self.bot.config.roles['newmember'])
        self.roles['gated'] = discord.utils.get(
            self.tguild.roles, id=self.bot.config.roles['gated'])
        self.roles['name_colour'] = discord.utils.get(
            self.tguild.roles, id=self.bot.config.name_colors[0])

        # ---------- SCHEDULER ----------
        self.scheduler.start()
        self.scheduler.print_jobs()

        # ---------- CHECK NEW MEMBERS ----------
        await self.check_new_members()

    # ---------- START TASK LOOPS ----------
    #self.updateNewMembers.start() not this

    @commands.Cog.listener()
    async def on_resume(self):

        # ===== WAIT FOR BOT TO FINISH SETTING UP
        await self.bot.wait_until_ready()

        # ===== LOG INVITES
        inviteLog = await self.__get_invite_info()

        if inviteLog is not None:
            await self.bot.db.execute(pgCmds.ADD_INVITES,
                                      json.dumps(inviteLog))
            self.bot.safe_print("[Log] Invite information has been logged.")

        else:
            self.bot.safe_print("[Log] No invite information to log.")

    @commands.Cog.listener()
    async def on_member_join(self, m):
        ###===== WAIT FOR THE BOT TO BE FINISHED SETTING UP
        await self.bot.wait_until_ready()

        # ---------- GET INVITE INFO ----------
        invite = await self.__get_invite_used()

        # ---------- LOG NEW MEMBER ----------
        embed = await GenEmbed.getMemJoinStaff(member=m, invite=invite)
        await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                     embed=embed)

        # ---------- SEND WELCOME MESSAGE ----------
        fmt = random.choice([
            f'Oh {m.mention} steps up to my dinner plate, I mean to {m.guild.name}!',
            f"I'm so excited to have {m.mention} join us, that I think I'll tear up the couch!",
            f"Well dip me in batter and call me a nugget, {m.mention} has joined us at {m.guild.name}!",
            f"The gates of {m.guild.name} have opened to: {m.mention}.",
            f"Attention {m.mention}, all new members of {m.guild.name} must be approved by me and I approve of you *hugs*."
        ])

        #fmt += "\nPlease give the rules in <#" + self.bot.config.channels['public_rules_id'] + "> a read and when you're ready make a post in <#" + self.bot.config.channels['entrance_gate'] + "> saying that you agreed to the rules."

        await asyncio.sleep(0.5)
        welMSG = await self.bot.send_msg_chid(
            self.bot.config.channels['bot_log'],
            content=fmt,
            guild_id=m.guild.id)

        # ---------- Update Database ----------
        await self.bot.db.execute(pgCmds.ADD_WEL_MSG, welMSG.id,
                                  welMSG.channel.id, welMSG.guild.id, m.id)
        await self.bot.db.execute(pgCmds.ADD_MEMBER_FUNC, m.id, m.joined_at,
                                  m.created_at)

        # ---------- AUTO ROLES ----------
        if self.bot.config.roles["autoroles"]:
            for r_id in self.bot.config.roles['autoroles']:
                await asyncio.sleep(0.4)
                role = discord.utils.get(m.guild.roles, id=r_id)
                await m.add_roles(role, reason="Auto Roles")

        # ---------- Schedule a kick ----------
        await self.schedule_kick(m, daysUntilKick=Days.gated, days=Days.gated)

    @commands.Cog.listener()
    async def on_member_remove(self, m):
        # ===== WAIT FOR THE BOT TO BE FINISHED SETTING UP
        await self.bot.wait_until_ready()

        # ===== IGNORE NON-TARGET GUILDS
        if m.guild.id != self.bot.config.target_guild_id:
            return

        # ---------- CANCEL SCHEDULED KICK ----------
        await self.cancel_scheduled_kick(member=m)

        # ---------- IF MEMBER IS KICKED OR BANNED ----------
        # ===== WAIT A BIT TO MAKE SURE THE GUILD AUDIT LOGS ARE UPDATED BEFORE READING THEM
        await asyncio.sleep(0.2)

        banOrKick = list()
        past_id = discord.utils.time_snowflake(datetime.datetime.utcnow() -
                                               datetime.timedelta(seconds=10),
                                               high=True)

        try:
            for i in [discord.AuditLogAction.ban, discord.AuditLogAction.kick]:

                async for entry in m.guild.audit_logs(limit=30,
                                                      action=i,
                                                      oldest_first=False):
                    if entry.id >= past_id and entry.target.id == m.id:

                        if banOrKick:
                            if entry.id > banOrKick[4]:
                                banOrKick = [
                                    entry.action, entry.target, entry.user,
                                    entry.reason or "None", entry.id
                                ]

                        else:
                            banOrKick = [
                                entry.action, entry.target, entry.user,
                                entry.reason or "None", entry.id
                            ]

        except discord.errors.Forbidden:
            self.bot.safe_print("[Info]  Missing view_audit_log permission.")

        except discord.errors.HTTPException:
            self.bot.safe_print(
                "[Info]  HTTP error occurred, likely being rate limited or blocked by CloudFlare. Restart recommended."
            )

        # ---------- REMOVED MEMBER LOGGING ----------
        # ===== STAFF ONLY LOGGING
        embed = await GenEmbed.getMemLeaveStaff(m, banOrKick)
        await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                     embed=embed)

        # ===== PUBLIC VISABLE LOGGING, ONLY APPLICABLE IF EXMEMBER WAS GIVEN THE CORE ROLE
        if discord.utils.get(m.roles, id=self.bot.config.roles['member']):

            wel_ch = self.bot.get_channel(
                self.bot.config.channels['public_bot_log'])

            async with wel_ch.typing():
                # = GET THE USERS PFP AS BYTES
                avatar_bytes = await GET_AVATAR_BYTES(user=m, size=128)

                # = SAFELY RUN SOME SYNCRONOUS CODE TO GENERATE THE IMAGE
                final_buffer = await self.bot.loop.run_in_executor(
                    None,
                    partial(images.GenGoodbyeImg, avatar_bytes, m, banOrKick))

                # = SEND THE RETURN IMAGE
                await wel_ch.send(
                    file=discord.File(filename="goodbye.png", fp=final_buffer))

        # ---------- REMOVE WELCOME MESSAGES ----------
        await self.del_user_welcome(m)

        # ---------- UPDATE THE DATABASE ----------
        await self.bot.db.execute(pgCmds.REMOVE_MEMBER_FUNC, m.id)

        # ===== END
        return

    @commands.Cog.listener()
    async def on_member_update(self, before, after):
        """When there is an update to a users user data"""

        await self.bot.wait_until_ready()  # Wait for bot to finish setting up

        if before.guild.id != self.bot.config.target_guild_id:
            return  # Ignore non-target guilds

        # ===== Make sure cog is finished setting up
        while True:
            if "member" in self.roles: break
            await asyncio.sleep(1)

        # ===== HANDLING FOR STAFF ADDING THE MEMBER ROLE TO NEW USERS MANUALLY
        if {self.roles['member'], self.roles['gated']}.issubset(
                set(after.roles)) and self.roles['gated'] in before.roles:
            await self.handle_gated2member(after)

        return

    @commands.Cog.listener()
    async def on_message(self, msg):
        await self.bot.wait_until_ready()  # Wait for bot to finish setting up

        # ===== IF MESSAGE WAS NOT IN ENTRANCE GATE
        if msg.channel.id != self.bot.config.channels['entrance_gate']: return

        # ===== IF THE MESSAGE IS A BOT COMMAND
        if (msg.content[len(self.bot.command_prefix):].split(" ")
            )[0] in self.bot.all_cmds:
            return

        # ===== IF THE AUTHOR IS GATED, LOG THE MESSAGE. IGNORES STAFF SINCE THEY TEND TO MESS AROUND
        if (any(role.id == self.bot.config.roles['gated']
                for role in msg.author.roles)
                and not any(role.id in self.bot.config.roles['any_staff']
                            for role in msg.author.roles)):

            await self.bot.db.execute(pgCmds.ADD_WEL_MSG, msg.id,
                                      msg.channel.id, msg.guild.id,
                                      msg.author.id)
            return

        # ===== CYCLE THROUGH ALL THE MEMBER'S MENTIONED IN THE MESSAGE
        for member in msg.mentions:
            # === IF MENTIONED MEMBER HAS THE GATED ROLE AND IS NOT STAFF
            if (any(role.id == self.bot.config.roles['gated']
                    for role in member.roles)
                    and not any(role.id in self.bot.config.roles['any_staff']
                                for role in member.roles)):

                await self.bot.db.execute(pgCmds.ADD_WEL_MSG, msg.id,
                                          msg.channel.id, msg.guild.id,
                                          member.id)
                break

        return

    @commands.Cog.listener()
    async def on_guild_role_update(self, before, after):
        # ===== WAIT FOR THE BOT TO BE FINISHED SETTING UP
        await self.bot.wait_until_ready()

        # ===== IF STORED MEMBER ROLE WAS UPDATED
        if self.roles['member'] == before:
            self.roles['member'] = after

        # ===== IF STORED NEWMEMBER ROLE WAS UPDATED
        elif self.roles['newmember'] == before:
            self.roles['newmember'] = after

        # ===== IF STORED GATED ROLE WAS UPDATED
        elif self.roles['gated'] == before:
            self.roles['gated'] = after

        return

# -------------------- COMMANDS --------------------

    @checks.HIGHEST_STAFF()
    @commands.command(pass_context=True,
                      hidden=False,
                      name='clearEntranceGate',
                      aliases=['clearentrancegate'])
    async def cmd_clearentrancegate(self, ctx):
        """
        [Minister] Kick members who have sat in the entrance gate for 14 days or more.
        """

        currDateTime = datetime.datetime.utcnow()

        oldFreshUsers = [
            member for member in ctx.guild.members
            if (self.roles['gated'] in member.roles) and (
                self.roles['member'] not in member.roles) and (
                    (currDateTime - member.joined_at).days > Days.gated)
        ]

        if len(oldFreshUsers) == 0:
            await ctx.send(
                content="No members need to be kicked at this time.",
                delete_after=10)
            return

        react = await self.bot.ask_yn(
            ctx,
            "{} gated users will be kicked.\nAre you sure you want to continue?"
            .format(len(oldFreshUsers)),
            timeout=120,
            expire_in=2)

        #===== if user says yes
        if react:
            try:
                for member in oldFreshUsers:
                    await member.kick(
                        reason=
                        f"Manual clearing of the entrance gate by {ctx.author.id}"
                    )
                    await asyncio.sleep(0.5)

                await ctx.send(
                    content=f"Done, {len(oldFreshUsers)} members kicked",
                    delete_after=30)

            except discord.errors.Forbidden:
                await ctx.send(
                    content="Can't kick members due to lack of permissions.",
                    delete_after=30)

            except discord.errors.HTTPException:
                await ctx.send(
                    content=
                    "Some error occurred. Go blame discord and try again later.",
                    delete_after=30)

            return

        #===== Time out handing
        elif react == None:
            await ctx.send(
                content="You took too long respond. Canceling action.",
                delete_after=30)

        #===== if user says no
        else:
            await ctx.send(content="Alright then, no members kicked.",
                           delete_after=30)

        return

    @checks.HIGHEST_STAFF()
    @commands.command(pass_context=True,
                      hidden=False,
                      name='closeGuild',
                      aliases=['closeguild'])
    async def cmd_closeguild(self, ctx, timer=None):
        """
        [Admins] Closes the guild either for a certain amount of time in seconds or until manually reopened.

        Useage:
            [p]closeguild <xDxHxMxS>/<S> (d for days, h for hours, M for minutes, S for seconds)
            eg: [p]closeguild 4D3H
        """

        t = None

        if timer:
            t = NewMembers.time_pat_to_secs(timer)
            if not t:
                ctx.send_help('closeGuild')

        # ===== EDIT COGSET
        self.cogset['guildclosed'] = True
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        if t:
            await self.schedule_reopen_guild(t)

        else:
            embed = await GenEmbed.genCloseGuild()
            await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                         embed=embed)

        return

    @checks.GATED()
    @commands.command(pass_context=False,
                      hidden=False,
                      name='agree',
                      aliases=['iagree', 'letmein'])
    async def cmd_agree(self, ctx):
        """
        [Gated] Lets a new member sitting in the gate into the rest of the guild.
        """

        if self.cogset['agreeoff'] or self.cogset['guildclosed']:
            await ctx.send(
                f"```\nSorry <@{ctx.author.id}>, but the guild is not accepting new members at this time. This is most likely due to a raid.\nPlease ask staff for help or check back later.\n```"
            )

        await ctx.author.add_roles(self.roles['member'])

        return

# -------------------- FUNCTIONS --------------------

    @asyncio.coroutine
    async def handle_gated2member(self, member):
        # ===== ADD NEW MEMBER AND REMOVE GATED ROLES
        await member.remove_roles(self.roles['gated'],
                                  reason="Removed Gated Role")
        await asyncio.sleep(0.2)
        await member.add_roles(self.roles['newmember'],
                               reason="Added new member role")
        await asyncio.sleep(0.2)
        await member.add_roles(self.roles['name_colour'],
                               reason="Added name colour")

        # ===== SCHEDULE REMOVAL OF NEW MEMBER ROLE
        await self.schedule_rem_newuser_role(member, daysUntilRemove=7, days=7)

        # ===== CANCEL EXISTING MEMBER KICK
        await self.cancel_scheduled_kick(member)

        # ===== TELL THE USERS A NEW MEMBER HAS JOINED
        wel_ch = self.bot.get_channel(
            self.bot.config.channels['public_bot_log'])

        async with wel_ch.typing():
            # === GET THE USERS PFP AS BYTES
            avatar_bytes = await GET_AVATAR_BYTES(user=member, size=128)

            # === SAFELY RUN SOME SYNCRONOUS CODE TO GENERATE THE IMAGE
            final_buffer = await self.bot.loop.run_in_executor(
                None, partial(images.GenWelcomeImg, avatar_bytes, member))

            # === SEND THE RETURN IMAGE
            await wel_ch.send(
                file=discord.File(filename="welcome.png", fp=final_buffer))

        # ===== DELETE USER MESSAGES IN THE GATE
        await self.del_user_welcome(member)

        return

    @asyncio.coroutine
    async def del_user_welcome(self, user):
        """
        Custom func to delete a users welcome message
        """

        # ===== GRAB ALL THE WELCOME MESSAGES FROM THE DATABASE RELATED TO THE USER IN QUESTION.
        welcomeMessages = await self.bot.db.fetch(pgCmds.GET_MEM_WEL_MSG,
                                                  user.id)
        bulkDelete = {}
        now = datetime.datetime.utcnow()

        # ===== DO NOTHING IF NOT DATA
        if not welcomeMessages:
            return

        # ===== CYCLE THROUGH OUR DATABASE DATA
        for MYDM in welcomeMessages:

            # === LOG MESSAGES INTO OUR DICT IF THEY CAN BE DELETED IN BULK
            if (now - MYDM["timestamp"]).days < 13:

                # = IF CHANNEL ID DOES NOT EXIST AS A KEY
                if MYDM['ch_id'] not in bulkDelete.keys():
                    bulkDelete[MYDM['ch_id']] = list()

                bulkDelete[MYDM['ch_id']].append(MYDM["msg_id"])

            else:
                # = IF MESSAGE IS TOO OLD, DELETE ONE BY ONE.
                await self.bot.delete_msg_id(MYDM["msg_id"],
                                             MYDM["ch_id"],
                                             reason="Welcome message cleanup.")
                await asyncio.sleep(0.2)

        # ===== IF THERE ARE MESSAGES TO BE BULK DELETED.
        if bulkDelete:

            # === EVEN THOUGH ALL MESSAGES WILL MOST LIKELY BE FROM THE SAME CHANNEL, THIS ENSURES COMPATIBILTY WITH WELCOME MESSAGES FROM MULTIPLE CHANNELS
            for i in bulkDelete.keys():
                await self.bot.delete_msgs_id(
                    messages=bulkDelete[i],
                    channel=i,
                    reason="Welcome message cleanup.")

        # ===== DELETE WELCOME MESSAGES FROM THE DATABASE
        await self.bot.db.execute(pgCmds.REM_MEM_WEL_MSG, user.id)

    @asyncio.coroutine
    async def __get_invite_used(self):
        """
        When called it tries to find the invite used by calling the equivalent handler.
        Will return none
            if
                previous history file is not found
                if history file could not be read
                if new invite info cannot be found

        It will try to update the the invite info file as long as the current info can be found.
        """

        #===== Get current invite info
        inviteLog = await self.__get_invite_info()

        #=== if info cannot be gotten
        if inviteLog == None:
            invite = None

        #=== if info received
        else:
            invite = await self.__get_invite_used_handler(inviteLog)
            await self.bot.db.execute(pgCmds.ADD_INVITES,
                                      json.dumps(inviteLog))

        return invite

    @asyncio.coroutine
    async def __get_invite_info(self, quiet=False):
        """Returns a dict with the information on the invites of selected guild"""

        try:
            invites = await self.bot.get_guild(self.bot.config.target_guild_id
                                               ).invites()

        except discord.Forbidden:
            if not quiet:
                await self.bot.send_msg_chid(
                    self.bot.config.channels['bot_log'],
                    content=
                    "```css\nAn error has occurred```I do not have proper permissions to get the invite information."
                )

            return None

        except discord.HTTPException:
            if not quiet:
                await self.bot.send_msg_chid(
                    self.bot.config.channels['bot_log'],
                    content=
                    "```css\nAn error has occurred```An error occurred when getting the invite information."
                )

            return None

        inviteLog = list()
        for invite in invites:
            inviteLog.append(
                dict(
                    max_age=invite.max_age,
                    created_at=invite.created_at.__str__(),
                    uses=invite.uses,
                    max_uses=invite.max_uses,
                    code=invite.id,
                    inviter=dict(
                        name=invite.inviter.name,
                        id=invite.inviter.id,
                        discriminator=invite.inviter.discriminator,
                        avatar_url=invite.inviter.avatar_url.__str__(),
                        mention=invite.inviter.mention)
                    if invite.inviter != None else dict(
                        name="N/A",
                        id="N/A",
                        discriminator="N/A",
                        avatar_url=
                        "https://discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png?size=128",
                        mention="N/A"),
                    channel=dict(name=invite.channel.name,
                                 id=invite.channel.id,
                                 mention=invite.channel.mention)))

        if len(inviteLog) == 0:
            return None

        else:
            return inviteLog

    @asyncio.coroutine
    async def __get_invite_used_handler(self, current_invite_info):
        """
        Tries to find which invite was used by a user joining.
        """

        #===== Read old invite info
        past_invite_info = json.loads(await self.bot.db.fetchval(
            pgCmds.GET_INVITE_DATA))

        #===== Testing the existing invites.
        for past_invite in past_invite_info:
            for curr_invite in current_invite_info:
                if past_invite["code"] == curr_invite["code"]:
                    if past_invite["uses"] < curr_invite["uses"]:
                        return curr_invite

        #===== testing the new invites. should work if new invite is made and a user joins with that invite.
        for curr_invite in [
                curr_invite for curr_invite in current_invite_info
                if curr_invite not in past_invite_info
        ]:
            if curr_invite["uses"] == 1:
                return curr_invite

        #===== CHECKING THE AUDIT LOG FOR INVITE CREATIONS
        guild = self.bot.get_guild(self.bot.config.target_guild_id)

        try:
            logs = await guild.audit_logs(
                action=discord.AuditLogAction.invite_create,
                before=(datetime.datetime.utcnow() -
                        datetime.timedelta(days=1))).flatten()

            if len(logs) == 1:
                log = logs[0]

                invite = {
                    "inviter": {
                        'mention': "<@{}>".format(log.user.id),
                        'name': log.user.name,
                        'discriminator': log.user.discriminator
                    },
                    'code': "N/A",
                    'uses': "N/A",
                    'max_uses': "N/A"
                }

                return invite

        except discord.Forbidden:
            return None

        return None

# -------------------- SCHEDULING STUFF --------------------

# -------------------- Task loops --------------------

    @tasks.loop(hours=24.0)
    async def updateNewMembers(self):
        if self.cogset['NMlastmsgid']:
            await self.bot.delete_msg_id(self.cogset['NMlastmsgid'],
                                         self.cogset['NMlastchid'])

        newmems = await self.bot.fetchval(pgCmds.GET_ADDED_MEMBERS)

    @updateNewMembers.before_loop
    async def before_updateNewMembers(self):
        await self.bot.wait_until_ready()

# -------------------- Auto Kick Members --------------------

    async def check_new_members(self):
        """
        [Called on_ready]
        
        Adds members with the fresh role and not the core role to the scheduler via self.schedule_kick with the warning for member already in the scheduler turned off.
        Really only useful if the scheduled data in the SQL file has been lost.
        """

        # ===== WAIT FOR THE BOT TO BE FINISHED SETTING UP
        await self.bot.wait_until_ready()

        # ===== VARIABLE SETUP
        guild = self.bot.get_guild(self.bot.config.target_guild_id)
        now = datetime.datetime.utcnow()

        for member in guild.members:

            # === IF MEMBER HAS ONLY THE EVERYONE ROLE
            if len(member.roles) == 1:

                # = APPLY THE AUTO ROLES
                if self.bot.config.roles["autoroles"]:
                    for r_id in self.bot.config.roles['autoroles']:
                        role = discord.utils.get(guild.roles, id=r_id)
                        await member.add_roles(role, reason="Auto Roles")
                        await asyncio.sleep(0.4)

                # = WORK OUT THE TIME THE USER HAS LEFT TO REGISTER
                diff = Days.gated - int((now - member.joined_at).days)

                # = IF MEMBER HAS BEEN ON THE GUILD FOR GREATER THEN 14 DAYS
                if diff < 1:
                    diff = 1

                await self.schedule_kick(member,
                                         daysUntilKick=diff,
                                         quiet=True,
                                         days=diff)

            # === ELSE IF MEMBER HAS THE GATED ROLE BUT NOT THE MEMBER ROLE
            elif (self.roles['gated']
                  in member.roles) and (self.roles['member']
                                        not in member.roles):

                # = WORK OUT THE TIME THE USER HAS LEFT TO REGISTER
                diff = Days.gated - int((now - member.joined_at).days)

                # = IF MEMBER HAS BEEN ON THE GUILD FOR GREATER THEN 14 DAYS
                if diff < 1:
                    diff = 1

                await self.schedule_kick(member,
                                         daysUntilKick=diff,
                                         quiet=True,
                                         days=diff)

            # === IF MEMBER HAS THE MEMBER ROLE BUT NOT THE NEW MEMBER ROLE
            elif (self.roles['member']
                  in member.roles) and (self.roles['newmember']
                                        not in member.roles):
                days = (Days.newmember + 1) - int(
                    (now - member.joined_at).days)

                # = IF MEMBER HAS BEEN ON GUILD FOR LONGER THAN THE TIME REQUIRED FOR NEW MEMBER ROLE TO EXPIRE
                if days < 1:
                    continue

                # = GIVE THE MEMBER THE NEWMEMBER ROLE
                await member.add_roles(self.roles['newmember'],
                                       reason="Added new member role")

                # = SCHEDULE THE NEW MEMBER ROLE FOR REMOVAL
                await self.schedule_rem_newuser_role(member, days)

#-------------------- Remove New User Role --------------------

    @asyncio.coroutine
    async def schedule_rem_newuser_role(self,
                                        member: Union[discord.User,
                                                      discord.Member],
                                        daysUntilRemove=Days.newmember,
                                        **kwargs):
        """
        [Called on_member_update]

        Adds the removal of a new member's fresh role to the scheduler.
        Handles:
            If member is already scheduled.

        It passes on the time allotted for an automatic kick to self._rem_newuser_role via the scheduler in the form of **kwargs
        """

        ###===== IF MEMBER IS ALREADY SCHEDULED TO HAVE NEW MEMBER ROLE REMOVED, QUIT
        for job in self.jobstore.get_all_jobs():
            if ["_rem_newuser_role", str(member.id)] == job.id.split(" "):
                return

        ###===== SEND REPORT MESSAGE TO STAFF
        embed = await GenEmbed.getScheduleRemNewRole(
            member=member, daysUntilRemove=daysUntilRemove)
        await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                     embed=embed)

        ###===== ADD EVENT TO THE SCHEDULER
        self.scheduler.add_job(call_schedule,
                               'date',
                               id=self.get_id_args(self._rem_newuser_role,
                                                   member.id),
                               run_date=get_next(**kwargs),
                               kwargs={
                                   "func": "_rem_newuser_role",
                                   "arg": str(member.id)
                               })

        return

    @asyncio.coroutine
    async def cancel_rem_newuser_role(self, member):
        """
        Cancels the scheduled kick of a member
        """

        for job in self.jobstore.get_all_jobs():
            if ["_rem_newuser_role", str(member.id)] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

    @asyncio.coroutine
    async def _rem_newuser_role(self, user_id):
        """
        [Assumed to be called by the scheduler]

        Takes a user id and removes their new member role.
        Handles:
            If member is not on the guild.
            if bot lacks permission to edit roles
        """

        ###===== WAIT FOR THE BOT TO BE FINISHED SETTING UP
        await self.bot.wait_until_ready()

        guild = self.bot.get_guild(self.bot.config.target_guild_id)
        member = guild.get_member(int(user_id))

        ###===== QUIT IF MEMBER HAS LEFT THE GUILD
        if member == None:
            return

        try:
            await member.remove_roles(self.roles['newmember'],
                                      reason="Auto remove New Member role")

            embed = await GenEmbed.genRemNewRole(member=member)
            await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                         embed=embed)

        except discord.Forbidden:
            self.bot.safe_print(
                f"I could not remove {member.mention}'s New Member role due to Permission error."
            )

        except discord.HTTPException:
            self.bot.safe_print(
                f"I could not remove {member.mention}'s New Member role due to generic error."
            )

        return

#-------------------- Kick new members --------------------

    @asyncio.coroutine
    async def cancel_scheduled_kick(self, member: Union[discord.User,
                                                        discord.Member]):
        """
        Cancels the scheduled kick of a member

        Args:
            (discord.User/discord.Member) Member you want to cancel kicking
        """

        for job in self.jobstore.get_all_jobs():
            if ["_kick_entrance", str(member.id)] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

    @asyncio.coroutine
    async def schedule_kick(self,
                            member,
                            daysUntilKick=Days.gated,
                            quiet=False,
                            **kwargs):
        """
        [Called on_member_join and check_new_members]

        Adds the automatic kick of a member from entrance gate after 14 days to the scheduler.
        Handles:
            If member is already scheduled to be kicked.

        It passes on the time allotted for an automatic kick to self._kick_entrance via the scheduler in the form of **kwargs
        """

        for job in self.jobstore.get_all_jobs():
            if ["_kick_entrance", str(member.id)] == job.id.split(" "):
                if not quiet:
                    await self.bot.send_msg_chid(
                        self.bot.config.channels['bot_log'],
                        content="{0.mention} already scheduled for a kick".
                        format(member))
                return

        embed = await GenEmbed.getScheduleKick(
            member=member,
            daysUntilKick=daysUntilKick,
            kickDate=(datetime.datetime.now() + datetime.timedelta(seconds=(
                (daysUntilKick * 24 * 60 * 60) + 3600))))

        await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                     embed=embed)

        #===== add the kicking of member to the scheduler
        self.scheduler.add_job(call_schedule,
                               'date',
                               id=self.get_id_args(self._kick_entrance,
                                                   member.id),
                               run_date=get_next(**kwargs),
                               kwargs={
                                   "func": "_kick_entrance",
                                   "arg": str(member.id)
                               })

    @asyncio.coroutine
    async def _kick_entrance(self, user_id):
        """
        [Assumed to be called by the scheduler]

        Takes a user id and kicks them from entrance gate.
        Handles:
            If member is not on the guild.
            if bot lacks permission to kick members
        """

        ###===== WAIT FOR THE BOT TO FINISH IT'S SETUP
        await self.bot.wait_until_ready()

        guild = self.bot.get_guild(self.bot.config.target_guild_id)
        member = guild.get_member(int(user_id))

        ###===== IF MEMBER IS NO LONGER ON THE GUILD
        if member == None:
            return

        gatedRole = discord.utils.get(guild.roles,
                                      id=self.bot.config.roles['gated'])
        memberRole = discord.utils.get(guild.roles,
                                       id=self.bot.config.roles['member'])

        try:
            #=== if member has fresh role and not core role
            if (gatedRole in member.roles) and (memberRole
                                                not in member.roles):
                #= kick member
                await member.kick(reason="Waited in entrance for too long.")

                #= report event
                embed = await GenEmbed.genKickEntrance(
                    member, self.bot.config.channels['entrance_gate'])
                await self.bot.send_msg_chid(
                    self.bot.config.channels['bot_log'], embed=embed)

        #===== Error if bot lacks permission
        except discord.errors.Forbidden:
            self.bot.safe_print(
                "[Error] (Scheduled event) I do not have permissions to kick members"
            )
            await self.bot.send_msg_chid(
                self.bot.config.channels['bot_log'],
                content=
                "I could not kick <@{0.id}> | {0.name}#{0.discriminator}, due to lack of permissions"
                .format(member))

        #===== Error for generic error, eg discord api gateway down
        except discord.errors.HTTPException:
            self.bot.safe_print(
                "[Error] (Scheduled event) I could not kick a member")
            await self.bot.send_msg_chid(
                self.bot.config.channels['bot_log'],
                content=
                "I could not kick <@{0.id}> | {0.name}#{0.discriminator}, due to an error"
                .format(member))

        return

#-------------------- Close Guild --------------------

    @asyncio.coroutine
    async def schedule_reopen_guild(self, secondsUntilReopen=3600, **kwargs):
        """
        [Called Close Guild Command]

        Adds re-open guild func to the scheduler.
        """

        # =====
        for job in self.jobstore.get_all_jobs():
            if ["_reopen_guild"] == job.id.split(" "):
                return

        # ===== SEND REPORT MESSAGE TO STAFF
        embed = await GenEmbed.genReopenGuild(secondsUntilReopen)
        await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                     embed=embed)

        # ===== ADD EVENT TO THE SCHEDULER
        self.scheduler.add_job(call_schedule,
                               'date',
                               id=self._reopen_guild.__name__,
                               run_date=get_next(**kwargs),
                               kwargs={"func": "_reopen_guild"})

        return

    @asyncio.coroutine
    async def _reopen_guild(self):
        # ===== WAIT FOR THE BOT TO FINISH IT'S SETUP
        await self.bot.wait_until_ready()

        # ===== EDIT COGSET
        self.cogset['guildclosed'] = False
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== REPORT TO STAFF
        embed = discord.Embed(
            title='Guild is now open.',
            description="Users will now be able to join the guild.",
            type="rich",
            timestamp=datetime.datetime.utcnow(),
            color=RANDOM_DISCORD_COLOR())

        await self.bot.send_msg_chid(self.bot.config.channels['bot_log'],
                                     embed=embed)

        return

#-------------------- TRINKETS --------------------

    def job_missed(self, event):
        """
        This exists too
        """

        asyncio.ensure_future(call_schedule(*event.job_id.split(" ")))

    @staticmethod
    def get_id_args(func, arg):
        """
        I have no damn idea what this does
        """

        return "{} {}".format(func.__name__, arg)
コード例 #2
0
class Gallery(commands.Cog):
    """Handle the Gallery channels."""


    delete_after = 15
    compare = lambda x, y: collections.Counter(x) == collections.Counter(y)

    def __init__(self, bot):
        Gallery.bot = self
        self.bot = bot
        self.db = None

        self.cogset = dict()

        self.jobstore = SQLAlchemyJobStore(url=fr'sqlite:///{pathlib.Path.cwd() / "data" / "jobs" / "gallery.sqlite"}')
        jobstores = {"default": self.jobstore}
        self.scheduler = AsyncIOScheduler(jobstores=jobstores)
        self.scheduler.add_listener(self.job_missed, events.EVENT_JOB_MISSED)


  # -------------------- LOCAL COG EVENTS --------------------
    async def cog_before_invoke(self, ctx):
        '''THIS IS CALLED BEFORE EVERY COG COMMAND, IT'S SOLE PURPOSE IS TO CONNECT TO THE DATABASE'''

        credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
        self.db = await asyncpg.create_pool(**credentials)

        return

    async def cog_after_invoke(self, ctx):
        '''THIS IS CALLED AFTER EVERY COG COMMAND, IT DISCONNECTS FROM THE DATABASE AND DELETES INVOKING MESSAGE IF SET TO.'''

        await self.db.close()
        await ctx.message.delete()

        return

    async def cog_command_error(self, ctx, error):
        if isinstance(error, discord.ext.commands.errors.NotOwner):
            try:
                owner = (self.bot.application_info()).owner
            except:
                owner = self.bot.get_guild(self.cogset['guild_id']).owner()

            await ctx.channel.send(content=f"```diff\n- {ctx.prefix}{ctx.invoked_with} is an owner only command, this will be reported to {owner.name}.")
            await owner.send(content=f"{ctx.author.mention} tried to use the owner only command{ctx.invoked_with}")
            return 


  # -------------------- STATIC METHODS --------------------
    @staticmethod
    def time_pat_to_hrs(t):
        '''
        Converts a string in format <xdxh> (d standing for days and h standing for hours) to amount of hours.
        eg: 3d5h would be 77 hours

        Args:
            (str) or (int)

        Returns:
            (int) or (None)
        '''
        try:
            timeinHours = int(t)
            return timeinHours

        except ValueError:
            valid = False 

            #===== if input doesn't match basic pattern
            if (re.match(r"(\d+[DHdh])+", t)):
                
                #=== if all acsii chars in the string are unique 
                letters = re.findall(r"[DHdh]", t)
                if len(letters) == len(set(letters)):
                    
                    #= if more then 1 letter side by side
                    #= ie. if t was 2dh30m then after the split you'd have ['', 'dh', 'm', '']
                    if not ([i for i in re.split(r"[0-9]", t) if len(i) > 1]):
                        
                        # if letters are in order.
                        if letters == sorted(letters, key=lambda letters: ["d", "h"].index(letters[0])):
                            valid = True

            if valid:
                total_hours = int() 

                for data in re.findall(r"(\d+[DHdh])", t):
                    if data.endswith("d"):
                        total_hours += int(data[:-1])*24
                    if data.endswith("h"):
                        total_hours += int(data[:-1])

            return total_hours 

        return False

    @staticmethod
    async def split_list(arr, size=100):
        """Custom function to break a list or string into an array of a certain size"""

        arrs = []

        while len(arr) > size:
            pice = arr[:size]
            arrs.append(pice)
            arr = arr[size:]

        arrs.append(arr)
        return arrs


  # -------------------- LISTENERS --------------------
    @commands.Cog.listener()
    async def on_ready(self):

        self.cogset = await cogset.LOAD(cogname=self.qualified_name)
        if not self.cogset:
            self.cogset= dict(
                guild_id=      0,
                enable=        False, 
                channel_ids=   [],
                text_expirein= None,
                rem_low=       False,
                user_wl=       [],
                allow_links=   False,
                link_wl=       []
            )

            await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        await asyncio.sleep(120)
        
        # ===== SCHEDULER
        self.scheduler.start()

    @commands.Cog.listener()
    async def on_message(self, msg):
        # ===== RETURN IF GALLERYS ARE DISABLED or MESSAGE IS NOT FROM A GUILD
        if  (  not self.cogset['enable'] 
            or not msg.guild
            or not msg.type == discord.MessageType.default
            or msg.author.id in self.cogset['user_wl']
            ):
            return 

        if msg.channel.id in self.cogset['channel_ids']:
            valid = False

            ###=== IF MESSAGE HAS ATTACHMENTS ASSUME THE MESSAGE IS OF ART.
            if msg.attachments:
                if not self.cogset['rem_low']:
                    valid = True 

                else:
                    toosmall = False 

                    for attch in msg.attachments:
                        if attch.height < 300 and attch.width < 300:
                            toosmall=True
                            break 
                    
                    valid = not toosmall
            
            ###=== IF LINKS ARE ALLOWED IN GALLERY CHANNELS
            if self.cogset['allow_links']:
                #- get the links from msg content
                links = re.findall(r"(?P<url>http[s]?://[^\s]+)", msg.content)

                ###= IF ONLY CERTAIN LINKS ARE ALLOWED
                if self.cogset['link_wl']:
                    
                    #= LOOP THROUGH THE LINKS FROM THE MESSAGE CONTENT AND THE WHITELISTED LINKS
                    #= ASSUME VALID IF ONE LINK MATCHES. 
                    for link in links:
                        for wl_link in self.cogset['link_wl']:

                            if link.startswith(wl_link):
                                valid = True  
                                break

                else:
                    valid = False

            ###=== IF THE MESSAGE IS NOT VALID.
            if not valid:
                credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
                self.db = await asyncpg.create_pool(**credentials)

                await self.db.execute(pgCmds.ADD_GALL_MSG, msg.id, msg.channel.id, msg.guild.id, msg.author.id, msg.created_at)
                await self.db.close()


  # -------------------- COMMANDS --------------------
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galenable', aliases=[])
    async def cmd_galenable(self, ctx):
        """
        [Bot Owner] Enables the gallery feature.

        Useage:
            [prefix]galenable
        """

        # ===== SET LOCAL COG VARIABLE
        self.cogset['enable']= True

        # ===== ADD THE FUNCTION TO THE SCHEDULER
        self.scheduler.add_job(call_schedule,
                                'date',
                                id="_delete_gallery_messages",
                                run_date=get_next(hours=self.cogset['text_expirein']),
                                kwargs={"func": "_delete_gallery_messages"}
                                )

        # ===== SAVE SETTINGS  
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        await ctx.channel.send(content="Galleries are **enabled**.")

        return
        
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galdisable', aliases=[])
    async def cmd_galdisable(self, ctx):
        """
        [Bot Owner] Disables the gallery feature.

        Useage:
            [prefix]galdisable
        """
        # ===== SET LOCAL COG VARIABLE
        self.cogset['enable']= False

        # ===== SAVE SETTINGS  
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== DELETE THE JOB IF IT EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

        await ctx.channel.send(content="Galleries are disabled.")

        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galtogglechannel', aliases=[])
    async def cmd_galtogglechannel(self, ctx, channel):
        """
        [Bot Owner] Add or remove a channel to the list of active gallery channels

        Useage:
            [prefix]galaddchannel <channelid/mention>
        """

        # ===== GET CHANNEL ID
        try:
            ch_id = int(channel.lower().replace('<').replace('>').replace('#').strip())

        except ValueError:
            ctx.send_help('galtogglechannel', delete_after=Gallery.delete_after)
        
        ret_msg=""

        # ===== REMOVE CHANNEL ID FROM LIST
        if ch_id in self.cogset['channel_ids']:
            self.cogset['channel_ids'].remove(ch_id)

            ret_msg = f"<#{ch_id}> is no longer a gallery channel."

            ###=== DELETE LOGGED MESSAGES FROM DATABASE
            await self.db.execute(pgCmds.DEL_GALL_MSGS_FROM_CH, ch_id, self.cogset['guild_id'])

        # ===== ADD CHANNEL ID TO LIST
        else:
            self.cogset['channel_ids'] = list(set(self.cogset['channel_ids']) + {ch_id})
            ret_msg = f"<#{ch_id}> has been made a gallery channel."

        # ===== SAVE SETTINGS  
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== END
        await ctx.channel.send(content=ret_msg, delete_after=Gallery.delete_after)
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galsetexpirehours', aliases=[])
    async def cmd_galsetexpirehours(self, ctx, timepat):
        """
        [Bot Owner] Sets how long the bot should wait to delete text only messages from gallery channels.
        Can be provided as <XdXh>

        Useage:
            [prefix]galsetexpirehours <hours>
        """
        # ===== CHECK IF VALID
        new_time = Gallery.time_pat_to_hrs(timepat)
        
        if not new_time:
            await ctx.send_help("galsetexpirehours", delete_after=15)
            return

        # ===== SAVE COG SETTINGS
        self.cogset['text_expirein'] = new_time
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        resetJob = False

        # ===== RESET THE JOB IF IT EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)
                resetJob = True

        if resetJob:
            # ===== ADD THE FUNCTION TO THE SCHEDULER
            self.scheduler.add_job(call_schedule,
                                    'date',
                                    id="_delete_gallery_messages",
                                    run_date=get_next(hours=self.cogset['text_expirein']),
                                    kwargs={"func": "_delete_gallery_messages"}
                                    )

            await ctx.channel.send(content=f"Text message expirey time has been set to {new_time} hours and the scheduler was reset.")
            return

        await ctx.channel.send(content=f"Text message expirey time has been set to {new_time} hours.")
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galtoguserwl', aliases=[])
    async def cmd_galtoguserwl(self, ctx, user_id):
        """
        [Bot Owner] Toggles a user in the gallery user whitelist. 
        If they are in the whitelist then their messages are persistent and not deleted retroactivly.

        Useage:
            [prefix]galtoguserwl <userid/mention>
        """ 

        # ===== CHECK IF INPUT IS VALID
        try:
            user_id = int(user_id.replace("<", '').replace("@", '').replace("!", '').replace(">", ''))
        except (IndexError, ValueError):
            await ctx.send_help('galtoguserwl', delete_after=Gallery.delete_after)
            return    

        # ===== REMOVE OR ADD USER TO THE WHITELIST
        ret_msg = ""

        if user_id in self.cogset['user_wl']:
            self.cogset['user_wl'].remove(user_id)
            ret_msg = f'<@{user_id} has been **removed** from the gallery whitelist.'
        
        else:
            self.cogset['user_wl'].append(user_id)
            ret_msg = f'<@{user_id} has been **added** to the gallery whitelist.'


        # ===== WRITE TO THE DATABASE
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== RETURN
        await ctx.channel.send(content=ret_msg, delete_after=Gallery.delete_after)
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galltogglelinks', aliases=[])
    async def cmd_galtogglelinks(self, ctx, tog=None):
        """
        [Bot Owner] Toggle links in the gallery channels or you can set if links are allowed with true or false.

        Useage:
            [prefix]galltogglelinks []
        """

        update = not self.cogset['allow_links']

        # ===== IF EXPLICITLY SETTING LINK STATUS
        if tog is not None:
            if tog.lower() in ['y', 'true', 'ture', 't']:
                update = True 

                if self.cogset['allow_links']:
                    await ctx.channel.send("Galleries are already **enabled**.", delete_after=Gallery.delete_after)

            elif tog.lower() in ['n', 'false', 'flase', 'f']:
                update = False 

                if not self.cogset['allow_links']:
                    await ctx.channel.send("Galleries are already **disabled**.", delete_after=Gallery.delete_after)
        
        self.cogset['allow_links']=update
            
        # ===== WRITE TO THE DATABASE
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== RETURN
        await ctx.channel.send(content=f"Links in the gallery channels is now set to{update}.", delete_after=Gallery.delete_after)
        return

    ### ADD LINK WHITELIST
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galaddlinkuwl', aliases=[])
    async def cmd_galaddlinkuwl(self, ctx):
        """
        [Bot Owner] Adds a link from gallery link whitelist.

        Useage:
            [prefix]galaddlinkuwl <startoflink>
        """

        links = re.findall(r"(?P<url>http[s]?://[^\s]+)", ctx.message.content)

        if not links:
            await ctx.channel.send('`Useage: [p]galaddlinkuwl <startoflink>, [Bot Owner] Adds a link from gallery link whitelist.`')
        
        # ===== ADD THE NEW LINKS TO THE WHITELIST
        new_gal_link_wl = list(set(self.cogset['link_wl']) + set(links))

        if Gallery.compare(new_gal_link_wl, self.cogset['link_wl']):
            await ctx.channel.send(content="{}\n are already in the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
            return  
        
        else:
            self.cogset['link_wl'] = new_gal_link_wl

        # ===== WRITE TO THE DATABASE
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== RETURN
        await ctx.channel.send(content="{}\n have been added to the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
        return
 
    ### REM LINK WHITELIST
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galremlinkuwl', aliases=[])
    async def cmd_galremlinkuwl(self, ctx):
        """
        [Bot Owner] Removes a link from gallery link whitelist.

        Useage:
            [prefix]galremlinkuwl <startoflink>
        """
        links = re.findall(r"(?P<url>http[s]?://[^\s]+)", ctx.message.content)

        if not links:
            await ctx.channel.send('Useage: [p]galremlinkuwl <startoflink>, [Bot Owner] Removes a link from gallery link whitelist.')

        # ===== REMOVE THE LINKS FROM THE LIST
        new_gal_link_wl = list(set(self.cogset['link_wl']) - set(links))

        if Gallery.compare(new_gal_link_wl, self.cogset['link_wl']):
            await ctx.channel.send(content="{}\n are not in the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
            return  
        
        else:
            self.cogset['link_wl'] = new_gal_link_wl

        # ===== WRITE TO THE DATABASE
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)

        # ===== RETURN
        await ctx.channel.send(content="{}\n have been removed from the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
        return

    ### SPECIAL
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galloadsettings', aliases=[])
    async def cmd_galloadsettings(self, ctx):
        """
        [Bot Owner] Loads gallery settings from the setup.ini file

        Useage:
            [prefix]galloadsettings
        """
        config = Config()

        # ===== UPDATE THE SETTINGS IN THE LOCAL COG
        self.cogset['guild_id'] =       config.target_guild_id
        self.cogset['enable']=          config.galEnable
        self.cogset['channel_ids'] =    config.gallerys["chls"]
        self.cogset['text_expirein']=   config.gallerys['expire_in']
        self.cogset['rem_low']=         config.gallerys['rem_low']
        self.cogset['user_wl']=         config.gallerys["user_wl"]
        self.cogset['allow_links']=     config.gallerys["links"]
        self.cogset['link_wl']=         config.gallerys['link_wl']

        # ===== SAVE COG SETTING
        await cogset.SAVE(self.cogset, cogname=self.qualified_name)
        
        # ===== RETURN
        await ctx.channel.send(content="Gallery information has been updated from the setup.ini file", delete_after=15)
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galsettings', aliases=[])
    async def cmd_galsettings(self, ctx, showlinks=False):
        sched = None
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                sched = job

        embed=discord.Embed(    
            title=      "Gallery Channel Settings.",
            description=f"**Enabled:** {self.cogset['enable']}\n"
                        f"**Expire time:** {self.cogset['text_expirein']} hours",
            colour=     RANDOM_DISCORD_COLOR(),
            type=       "rich",
            timestamp=  datetime.datetime.utcnow()
            )

        embed.add_field(
            name=       "Gallery Channels",
            value=      "\n".join([f"<#{ch_id}>" for ch_id in self.cogset['channel_ids']]) or "None",
            inline=     False
            )    

        embed.add_field(
            name=       "Whitelisted Members",
            value=      "\n".join([f"<#{user_id}>" for user_id in self.cogset['user_wl']]) or "None",
            inline=     False
            )

        if showlinks:
            links = "\n".join(self.cogset["link_wl"])

            embed.add_field(
                name=       "Gallery Links",
                value=      f'**Allowed:** {self.cogset["allow_links"]}\n'
                            f'**Links:** {links}',
                inline=     False
                )   

        if sched:
            run_time = sched.next_run_time.__str__()
        else:
            run_time = "never"

        embed.add_field(
            name=       "Scheduler",
            value=      f"**Next run time:** {run_time}",
            inline=     False
        )   

        embed.set_footer(       
            icon_url=   GUILD_URL_AS(ctx.guild) if ctx.guild else AVATAR_URL_AS(self.bot.user), 
            text=       "Gallery Settings"
                    )

        await ctx.channel.send(embed=embed)
        return


  # -------------------- SCHEDULING --------------------
    def job_missed(self, event):
        """
        This exists too
        """
        asyncio.ensure_future(call_schedule(*event.job_id.split(" ")))

    @staticmethod
    def get_id_args(func, arg):
        """
        I have no damn idea what this does
        """

        return "{} {}".format(func.__name__, arg)

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=False, name='galinitiateschedule', aliases=[])
    async def cmd_galinitiateschedule(self, ctx):
        
        # ===== DELETE THE JOB IF IT ALREADY EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

        # ===== ADD THE FUNCTION TO THE SCHEDULER
        self.scheduler.add_job(call_schedule,
                               'date',
                               id="_delete_gallery_messages",
                               run_date=get_next(hours=self.cogset['text_expirein']),
                               kwargs={"func": "_delete_gallery_messages"}
                               )

        # ===== RETURN
        await ctx.channel.send(content=f"Gallery schedule has been set for {get_next(hours=self.cogset['text_expirein'])}")

        return

    async def _delete_gallery_messages(self, *args):
        # ===== QUIT ID GALLERIES ARE DISABLED.
        if not self.cogset['enable']:
            return 

        # ===== CONNECT TO THE DATABASE
        credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
        self.db = await asyncpg.create_pool(**credentials)

        after = (datetime.datetime.utcnow() - datetime.timedelta(hours=self.cogset['text_expirein']))

        t = await self.db.fetch(pgCmds.GET_GALL_MSG_BEFORE, after)
        ch_ids = await self.db.fetch(pgCmds.GET_GALL_CHIDS_BEFORE, after)
        await self.db.execute(pgCmds.DEL_GALL_MSGS_BEFORE, self.cogset['guild_id'], after)

        await self.db.close()

        # ===== TURNING THE DATA INTO SOMETHING MORE USEFUL
        ch_ids = [ch_id['ch_id'] for ch_id in ch_ids]
        fast_delete = dict()
        slow_delete = []

        for ch_id in ch_ids:
            fast_delete[ch_id] = []

        now = datetime.datetime.utcnow()
        delta = datetime.timedelta(days=13, hours=12)

        for record in t:
            ###=== IF MESSAGE IS OLDER THAN 13 DAYS AND 12 HOURS
            if bool((now - record['timestamp']) > delta):
                slow_delete.append(record)

            ###=== IF MESSAHE IS YOUNGER THAN 13 DAYS AND 12 HOURS
            else:
                fast_delete[record['ch_id']].append(record['msg_id'])

        # ===== IF THERE IS FAST DELETE DATA   
        # WITH FAST DELETE MESSAGES WE CAN DELETE MESSAGES IN BULK OF 100
        if fast_delete:
            for ch_id in fast_delete.keys():
                msgs_ids = await Gallery.split_list(fast_delete[ch_id], 100)

                for msg_ids in msgs_ids:
                    if len(msg_ids) > 1:

                        await self.bot.http.delete_messages(ch_id, msg_ids, reason="Deleting Gallery Messages")
                        await asyncio.sleep(0.5)

                    else:
                        # SOMETIMES AN EMPTY LIST MAKES IT HERE
                        if msg_ids:
                            msg_id = msg_ids[0]
                            await self.bot.http.delete_message(ch_id, msg_id, reason="Deleting Gallery Messages")
                            await asyncio.sleep(0.5)

        # ===== IF THERE IS SLOW DELETE DATA
        # WE CANNOT DELETE THESE MESSAGES IN BULK, ONLY ONE BY ONE.
        if slow_delete:
            for record in slow_delete:

                await self.bot.http.delete_message(record['ch_id'], record['msg_id'], reason="Deleting Gallery Messages")
                await asyncio.sleep(0.5)


        ###==== LOOP THE SCHEDULER
        self.scheduler.add_job( call_schedule,
                                'date',
                                id="_delete_gallery_messages",
                                run_date=get_next(hours=self.cogset['text_expirein']),
                                kwargs={"func": "_delete_gallery_messages"}
                                )
        return
コード例 #3
0
class Gallery(commands.Cog):
    """Handle the Gallery channels."""

    config = None 
    delete_after = 15
    compare = lambda x, y: collections.Counter(x) == collections.Counter(y)

    def __init__(self, bot):
        Gallery.bot = self
        self.bot = bot
        self.db = None
        #Gallery.config = Config()
        
        self.gal_guild_id=      0
        self.gal_enable=        False 
        self.gal_channel_ids=   []
        self.gal_channels=      []
        self.gal_text_expirein= None
        self.gal_user_wl=       []
        self.gal_allow_links=   False
        self.gal_link_wl=       []

        self.jobstore = SQLAlchemyJobStore(url='sqlite:///gallery.sqlite')
        jobstores = {"default": self.jobstore}
        self.scheduler = AsyncIOScheduler(jobstores=jobstores)
        self.scheduler.add_listener(self.job_missed, events.EVENT_JOB_MISSED)


  #-------------------- LOCAL COG EVENTS --------------------
    async def cog_before_invoke(self, ctx):
        '''THIS IS CALLED BEFORE EVERY COG COMMAND, IT'S SOLE PURPOSE IS TO CONNECT TO THE DATABASE'''

        credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
        self.db = await asyncpg.create_pool(**credentials)

        return

    async def cog_after_invoke(self, ctx):
        '''THIS IS CALLED AFTER EVERY COG COMMAND, IT DISCONNECTS FROM THE DATABASE AND DELETES INVOKING MESSAGE IF SET TO.'''

        await self.db.close()

        if Gallery.config.delete_invoking:
            await ctx.message.delete()

        return

    async def on_cog_command_error(self, ctx, error):
        if isinstance(error, discord.ext.commands.errors.NotOwner):
            try:
                owner = (self.bot.application_info()).owner
            except:
                owner = self.bot.get_guild(self.gal_guild_id).owner()

            await ctx.channel.send(content=f"```diff\n- {ctx.prefix}{ctx.invoked_with} is an owner only command, this will be reported to {owner.name}.")
            await owner.send(content=f"{ctx.author.mention} tried to use the owner only command{ctx.invoked_with}")
            return 


  #-------------------- STATIC METHODS --------------------
    @staticmethod
    async def get_channel_id(content):
        try:
            args= content.split(" ")
            if len(args) > 2:
                return False 

            #=== SPLIT, REMOVE MENTION WRAPPER AND CONVERT TO INT
            ch_id = args[1]
            ch_id = ch_id.replace("<", "").replace("#", "").replace(">", "")
            ch_id = int(ch_id)
            return ch_id

        except (IndexError, ValueError):
            return False

    @staticmethod
    async def get_user_id(content):
        try:
            args= content.split(" ")
            if len(args) > 2:
                return False 

            #=== SPLIT, REMOVE MENTION WRAPPER AND CONVERT TO INT
            user_id = args[1]
            user_id = user_id.replace("<", "").replace("@", "").replace("!", "").replace(">", "")
            user_id = int(user_id)
            return user_id

        except (IndexError, ValueError):
            return False
    
    @staticmethod
    def time_pat_to_hrs(content):
        '''
        Converts a string in format <xdxh> (d standing for days and h standing for hours) to amount of hours.
        eg: 3d5h would be 77 hours

        Args:
            (str) or (int)

        Returns:
            (int) or (None)
        '''
        args= content.split(" ")

        if len(args) > 2:
            return False 
        
        t = args[1]

        timeinHours = int() 

        try:
            timeinHours = int(t)
            return timeinHours

        except ValueError:
            valid = False 

            #===== if input doesn't match basic pattern
            if (re.match(r"(\d+[DHdh])+", t)):
                
                #=== if all acsii chars in the string are unique 
                letters = re.findall(r"[DHdh]", t)
                if len(letters) == len(set(letters)):
                    
                    #= if more then 1 letter side by side
                    #= ie. if t was 2dh30m then after the split you'd have ['', 'dh', 'm', '']
                    if not ([i for i in re.split(r"[0-9]", t) if len(i) > 1]):
                        
                        # if letters are in order.
                        if letters == sorted(letters, key=lambda letters: ["d", "h"].index(letters[0])):
                            valid = True

            if valid:
                total_hours = int() 

                for data in re.findall(r"(\d+[DHdh])", t):
                    if data.endswith("d"):
                        total_hours += int(data[:-1])*24
                    if data.endswith("h"):
                        total_hours += int(data[:-1])

            return total_hours 

        return False

    @staticmethod
    async def split_list(arr, size=100):
        """Custom function to break a list or string into an array of a certain size"""

        arrs = []

        while len(arr) > size:
            pice = arr[:size]
            arrs.append(pice)
            arr = arr[size:]

        arrs.append(arr)
        return arrs

    @staticmethod
    async def oneline_valid(content):
        try:
            args = content.split(" ")
            if len(args) > 1:
                return False 

            return True

        except (IndexError, ValueError):
            return False


  #-------------------- LISTENERS --------------------
    @commands.Cog.listener()
    async def on_ready(self):
        self.cogset = await cogset.LOAD(cogname=self.qualified_name)
        if not self.cogset:
            self.cogset= dict(
                enablelogging=False
            )

            await cogset.SAVE(self.cogset, cogname=self.qualified_name)

    #@commands.Cog.listener()
    async def on_ready_old(self):
        credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
        self.db = await asyncpg.create_pool(**credentials)
        dbconfig = await self.db.fetchrow(pgCmds.GET_GUILD_GALL_CONFIG)
        await self.db.close()

        self.gal_guild_id=      dbconfig['guild_id']
        self.gal_enable=        dbconfig['gall_nbl']

        self.gal_channel_ids=   dbconfig['gall_ch']
        guild = self.bot.get_guild(self.gal_guild_id)
        self.gal_channels=      [channel for channel in guild.channels if channel.id in dbconfig['gall_ch']]

        self.gal_text_expirein= dbconfig['gall_text_exp']
        self.gal_user_wl=       dbconfig['gall_user_wl']
        self.gal_allow_links=   dbconfig['gall_nbl_links']
        self.gal_link_wl=       dbconfig['gall_links']

        ###===== SCHEDULER
        self.scheduler.start()

    @commands.Cog.listener()
    async def on_message(self, msg):
        ###===== RETURN IF GALLERYS ARE DISABLED
        if not self.gal_enable:
            return 
        
        ###===== RETURN IF MESSAGE IS NOT FROM A GUILD
        if not msg.guild:
            return
        
        ###===== RETURN IF MESSAGE TYPE IS ANYTHING OTHER THAN A NORMAL MESSAGE.
        if not bool(msg.type == discord.MessageType.default):
            return

        if msg.channel in self.gal_channels:
            
            ###=== IF AUTHOR IS ALLOWED TO POST MESSAGES FREELY IN GALLERY CHANNELS
            if msg.author.id in self.gal_user_wl:
                return 

            valid = False

            ###=== IF MESSAGE HAS ATTACHMENTS ASSUME THE MESSAGE IS OF ART.
            if msg.attachments:
                valid = True 
            
            ###=== IF LINKS ARE ALLOWED IN GALLERY CHANNELS
            if self.gal_allow_links:
                #- get the links from msg content
                links = re.findall(r"(?P<url>http[s]?://[^\s]+)", msg.content)

                ###= IF ONLY CERTAIN LINKS ARE ALLOWED
                if self.gal_link_wl:
                    
                    #= LOOP THROUGH THE LINKS FROM THE MESSAGE CONTENT AND THE WHITELISTED LINKS
                    #= ASSUME VALID IF ONE LINK MATCHES. 
                    for link in links:
                        for wl_link in self.gal_link_wl:

                            if link.startswith(wl_link):
                                valid = True  
                                break

                else:
                    valid = True

            ###=== IF THE MESSAGE IS NOT VALID.
            if not valid:
                credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
                self.db = await asyncpg.create_pool(**credentials)

                self.db.execute(pgCmds.ADD_GALL_MSG, msg.id, msg.channel.id, msg.guild.id, msg.author.id, msg.created_at)
                await self.db.close()



            #regex = r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"
            #urls = re.findall( regex, text )

            #re.findall("(?P<url>http[s]?://[^\s]+)", t)
            #re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\), ]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', t)

            #credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
            #self.db = await asyncpg.create_pool(**credentials)


  #-------------------- COMMANDS --------------------
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galenable', aliases=[])
    async def cmd_galenable(self, ctx):
        """
        [Bot Owner] Enables the gallery feature.

        Useage:
            [prefix]galenable
        """

        ###===== Write to database
        await self.db.execute(pgCmds.SET_GUILD_GALL_ENABLE, self.gal_guild_id)

        ###===== SET LOCAL COG VARIABLE
        self.gal_enable= True

        ###===== DELETE THE JOB IF IT EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

        ###===== ADD THE FUNCTION TO THE SCHEDULER
        self.scheduler.add_job(call_schedule,
                               'date',
                               id="_delete_gallery_messages",
                               run_date=get_next(hours=self.gal_text_expirein),
                               kwargs={"func": "_delete_gallery_messages"}
                               )

        await ctx.channel.send(content="Galleries are disabled.")

        return
        
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galdisable', aliases=[])
    async def cmd_galdisable(self, ctx):
        """
        [Bot Owner] Disables the gallery feature.

        Useage:
            [prefix]galdisable
        """
        ###===== Write to database
        await self.db.execute(pgCmds.SET_GUILD_GALL_DISABLE, self.gal_guild_id)

        ###===== SET LOCAL COG VARIABLE
        self.gal_enable= False

        ###===== DELETE THE JOB IF IT EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

        await ctx.channel.send(content="Galleries are disabled.")

        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='enablegalleries', aliases=[])
    async def cmd_galaddchannel(self, ctx):
        """
        [Bot Owner] Add a channel to the list of active gallery channels

        Useage:
            [prefix]galaddchannel <channelid/mention>
        """

        ###===== VALIDATE INPUT
        ch_id = await Gallery.get_channel_id(ctx.message.content)

        if not ch_id:
            ctx.channel.send(content="`Useage: [p]galaddchannel <channelid/mention>, [Bot Owner] Add a channel to the list of active gallery channels.`", delete_after=Gallery.delete_after)

        ###===== ADD NEW CHANNEL ID TO LIST
        new_channel_ids = list(set(self.gal_channel_ids) + {ch_id})

        if Gallery.compare(self.gal_channel_ids, new_channel_ids):
            await ctx.channel.send(content=f"<#{ch_id}> is already a gallery channel.")
            return

        else:
            self.gal_channel_ids = new_channel_ids

        ###===== GET THE ACTUAL CHANNEL FROM THE GUILD
        if ctx.guild:
            guild = ctx.guild 
        else:
            guild = self.bot.get_guild(self.gal_guild_id)

        self.gal_channels = [channel for channel in guild.channels if channel.id in self.gal_channel_ids]

        ###===== WRITE DATA TO DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_CHLS, self.gal_channel_ids)

        ###===== END
        await ctx.channel.send(content=f"<#{ch_id}> has been made a gallery channel.")
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galremchannel', aliases=[])
    async def cmd_galremchannel(self, ctx):
        """
        [Bot Owner] Removes a channel to the list of active gallery channels

        Useage:
            [prefix]galremchannel <channelid/mention>
        """

        ch_id = await Gallery.get_channel_id(ctx.message.content)

        if not ch_id:
            ctx.channel.send(content="`Useage: [p]galremchannel <channelid/mention>, [Bot Owner] Removes a channel to the list of active gallery channels.`", delete_after=Gallery.delete_after)

        ###===== REMOVE CHANNEL ID FROM LIST
        try:
            self.gal_channel_ids.remove(ch_id)

        except ValueError:
            await ctx.channel.send(content=f"<#{ch_id}> isn't a gallery channel.")
            return  

        ###===== GET THE ACTUAL CHANNEL FROM THE GUILD
        if ctx.guild:
            guild = ctx.guild 
        else:
            guild = self.bot.get_guild(self.gal_guild_id)

        self.gal_channels = [channel for channel in guild.channels if channel.id in self.gal_channel_ids]

        ###===== WRITE DATA TO DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_CHLS, self.gal_channel_ids)
        await self.db.execute(pgCmds.DEL_GALL_MSGS_FROM_CH, ch_id, self.gal_guild_id)

        ###===== END
        await ctx.channel.send(content=f"<#{ch_id}> is no longer a gallery channel.")
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galsetexpirehours', aliases=[])
    async def cmd_galsetexpirehours(self, ctx):
        """
        [Bot Owner] Sets how long the bot should wait to delete text only messages from gallery channels

        Useage:
            [prefix]galsetexpirehours <hours>
        """
        new_time = Gallery.time_pat_to_hrs(ctx.message.content)

        await self.db.execute(pgCmds.SET_GUILD_GALL_EXP, new_time)

        resetJob = False

        ###===== RESET THE JOB IF IT EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)
                resetJob = True

        if resetJob:
            ###===== ADD THE FUNCTION TO THE SCHEDULER
            self.scheduler.add_job(call_schedule,
                                    'date',
                                    id="_delete_gallery_messages",
                                    run_date=get_next(hours=self.gal_text_expirein),
                                    kwargs={"func": "_delete_gallery_messages"}
                                    )

            await ctx.channel.send(content=f"Text message expirey time has been set to {new_time} hours and the scheduler was reset.")
            return

        await ctx.channel.send(content=f"Text message expirey time has been set to {new_time} hours.")
        return
    
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galadduserwl', aliases=[])
    async def cmd_galadduserwl(self, ctx):
        """
        [Bot Owner] Adds a user to the gallery user whitelist. Allowing them to post in Gallery channels.

        Useage:
            [prefix]galadduserwl <userid/mention>
        """ 

        ###===== CHECK IF INPUT IS VALID
        user_id = Gallery.get_user_id(ctx.message.content)

        if not user_id:
            return

        ###===== ADD USER ID TO THE WHITELIST
        new_user_whitelist = list(set(self.gal_user_wl) + {user_id})

        if Gallery.compare(self.gal_user_wl, new_user_whitelist):
            await ctx.channel.send(content=f"<@{user_id}> is alreadt in the gallery whitelist.", delete_after=Gallery.delete_after)
            return 

        else:
            self.gal_user_wl = new_user_whitelist

        ###===== WRITE TO THE DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_USER_WL, self.gal_user_wl, self.gal_guild_id)

        ###===== RETURN
        await ctx.channel.send(content=f"<@{user_id}> has been added to the gallery whitelist.", delete_after=Gallery.delete_after)
        return

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galremuserwl', aliases=[])
    async def cmd_galremuserwl(self, ctx):
        """
        [Bot Owner] Remomes a user to the gallery user whitelist.

        Useage:
            [prefix]galremuserwl <userid/mention>
        """

        ###===== CHECK IF INPUT IS VALID
        user_id = Gallery.get_user_id(ctx.message.content)
        
        if not user_id:
            return

        ###===== REMOVE USER FROM WHITELIST
        try:
            self.gal_user_wl.remove(user_id)

        except ValueError:

            #=== IF USER IS NOT ON THE WHITELIST
            await ctx.channel.send(content=f"<@{user_id}> was not on the gallery whitelist.", delete_after=Gallery.delete_after)
            return

        ###===== WRITE TO DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_USER_WL, self.gal_user_wl, self.gal_guild_id)

        ###===== RETURN 
        await ctx.channel.send(content=f"<@{user_id}> has been removed from the gallery whitelist.", delete_after=Gallery.delete_after)
        return

    ### ENABLE LINKS
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galenablelinks', aliases=[])
    async def cmd_galenablelinks(self, ctx):
        """
        [Bot Owner] Allow links in the gallery channels.

        Useage:
            [prefix]galenablelinks <channelid/mention>
        """

        valid = Gallery.oneline_valid(ctx.message.content)

        if not valid:
            return

        self.gal_allow_links=True
        
        ###===== WRITE TO THE DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_LINK_ENABLE)

        ###===== RETURN
        await ctx.channel.send(content="Links are now allowed in the gallery channels.", delete_after=Gallery.delete_after)
        return

    ### BLOCK LINKS
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galdisablelinks', aliases=[])
    async def cmd_galdisablelinks(self, ctx):
        """
        [Bot Owner] Block links in the gallery channels.

        Useage:
            [prefix]galdisablelinks <channelid/mention>
        """

        valid = Gallery.oneline_valid(ctx.message.content)

        if not valid:
            return

        self.gal_allow_links=False

        ###===== WRITE TO THE DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_LINK_DISABLE)

        ###===== RETURN
        await ctx.channel.send(content="Links are no longer allowed in the gallery channels.", delete_after=Gallery.delete_after)
        return

    ### ADD LINK WHITELIST
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galaddlinkuwl', aliases=[])
    async def cmd_galaddlinkuwl(self, ctx):
        """
        [Bot Owner] Adds a link from gallery link whitelist.

        Useage:
            [prefix]galaddlinkuwl <startoflink>
        """

        links = re.findall(r"(?P<url>http[s]?://[^\s]+)", ctx.message.content)

        if not links:
            await ctx.channel.send('`Useage: [p]galaddlinkuwl <startoflink>, [Bot Owner] Adds a link from gallery link whitelist.`')
        
        ###===== ADD THE NEW LINKS TO THE WHITELIST
        new_gal_link_wl = list(set(self.gal_link_wl) + set(links))

        if Gallery.compare(new_gal_link_wl, self.gal_link_wl):
            await ctx.channel.send(content="{}\n are already in the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
            return  
        
        else:
            self.gal_link_wl = new_gal_link_wl

        ###===== WRITE TO THE DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_LINKS, self.gal_link_wl, self.gal_guild_id)

        ###===== RETURN
        await ctx.channel.send(content="{}\n have been added to the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
        return
 
    ### REM LINK WHITELIST
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galremlinkuwl', aliases=[])
    async def cmd_galremlinkuwl(self, ctx):
        """
        [Bot Owner] Removes a link from gallery link whitelist.

        Useage:
            [prefix]galremlinkuwl <startoflink>
        """

        links = re.findall(r"(?P<url>http[s]?://[^\s]+)", ctx.message.content)

        if not links:
            await ctx.channel.send('Useage: [p]galremlinkuwl <startoflink>, [Bot Owner] Removes a link from gallery link whitelist.')

        ###===== REMOVE THE LINKS FROM THE LIST
        new_gal_link_wl = list(set(self.gal_link_wl) - set(links))

        if Gallery.compare(new_gal_link_wl, self.gal_link_wl):
            await ctx.channel.send(content="{}\n are not in the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
            return  
        
        else:
            self.gal_link_wl = new_gal_link_wl

        ###===== WRITE TO THE DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_LINKS, self.gal_link_wl, self.gal_guild_id)

        ###===== RETURN
        await ctx.channel.send(content="{}\n have been removed from the gallery link whitelist.".format('\n'.join(links)), delete_after=Gallery.delete_after)
        return

    ### SPECIAL
    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galloadsettings', aliases=[])
    async def cmd_galloadsettings(self, ctx):
        ###===== OPEN THE SETUP.INI FILE
        config = Config()

        ###===== WRITE DATA FROM THE SETUP.INI FILE TO THE DATABASE
        await self.db.execute(pgCmds.SET_GUILD_GALL_CONFIG, config.galEnable, config.gallerys["chls"], config.gallerys['expire_in'], config.gallerys["user_wl"], config.gallerys["links"], config.gallerys['link_wl'])

        ###===== UPDATE THE SETTINGS IN THE LOCAL COG
        self.gal_enable=        config.galEnable

        guild = self.bot.get_guild(self.gal_guild_id)
        self.gal_channels=      [channel for channel in guild.channels if channel.id in config.gallerys["chls"]]

        self.gal_text_expirein= config.gallerys['expire_in']
        self.gal_user_wl=       config.gallerys["user_wl"]
        self.gal_allow_links=   config.gallerys["links"]
        self.gal_link_wl=       config.gallerys['link_wl']

        ###===== RETURN
        await ctx.channel.send(content="Gallery information has been updated from the setup.ini file", delete_after=15)
        return


  #-------------------- SCHEDULING --------------------
    def job_missed(self, event):
        """
        This exists too
        """

        asyncio.ensure_future(call_schedule(*event.job_id.split(" ")))

    @staticmethod
    def get_id_args(func, arg):
        """
        I have no damn idea what this does
        """

        return "{} {}".format(func.__name__, arg)

    @commands.is_owner()
    @commands.command(pass_context=True, hidden=True, name='galinitiateschedule', aliases=[])
    async def cmd_galinitiateschedule(self, ctx):
        
        ###===== DELETE THE JOB IF IT ALREADY EXISTS
        for job in self.jobstore.get_all_jobs():
            if ["_delete_gallery_messages"] == job.id.split(" "):
                self.scheduler.remove_job(job.id)

        ###===== ADD THE FUNCTION TO THE SCHEDULER
        self.scheduler.add_job(call_schedule,
                               'date',
                               id="_delete_gallery_messages",
                               run_date=get_next(hours=self.gal_text_expirein),
                               kwargs={"func": "_delete_gallery_messages"}
                               )

        ###===== RETURN
        ctx.channel.send(content=f"Gallery schedule has been set for {get_next(hours=self.gal_text_expirein)}")

        return


    async def _delete_gallery_messages(self):
        ###===== QUIT ID GALLERIES ARE DISABLED.
        if not self.gal_enable:
            return 

        ###===== CONNECT TO THE DATABASE
        credentials = {"user": dblogin.user, "password": dblogin.pwrd, "database": dblogin.name, "host": dblogin.host}
        self.db = await asyncpg.create_pool(**credentials)

        after = datetime.datetime.utcnow() - datetime.timedelta(hours=self.gal_text_expirein)

        t = await self.db.fetch(pgCmds.GET_GALL_MSG_AFTER, after)
        ch_ids = await self.db.fetch(pgCmds.GET_GALL_CHIDS_AFTER, after)

        await self.db.close()

        ###===== TURNING THE DATA INTO SOMETHING MORE USEFUL
        ch_ids = [ch_id['ch_id'] for ch_id in ch_ids]
        fast_delete = dict()
        slow_delete = []

        for ch_id in ch_ids:
            fast_delete[ch_id] = []

        now = datetime.datetime.utcnow()
        delta = datetime.timedelta(days=13, hours=12)

        for record in t:
            ###=== IF MESSAGE IS OLDER THAN 13 DAYS AND 12 HOURS
            if bool((now - record['timestamp']) > delta):
                slow_delete.append(record)

            ###=== IF MESSAHE IS YOUNGER THAN 13 DAYS AND 12 HOURS
            else:
                fast_delete[record['ch_id']].append(record['msg_id'])

        ###===== IF THERE IS FAST DELETE DATA   
        # WITH FAST DELETE MESSAGES WE CAN DELETE MESSAGES IN BULK OF 100
        if fast_delete:
            for ch_id in fast_delete.keys():
                msgs_ids = Gallery.split_list(fast_delete[ch_id], 100)

                for msg_ids in msgs_ids:
                    if len(msg_ids) > 1:

                        await self.bot.http.delete_messages(ch_id, msg_ids, reason="Deleting Gallery Messages")
                        await asyncio.sleep(0.5)

                    else:
                        msg_id = msg_ids[0]
                        await self.bot.http.delete_message(ch_id, msg_id, reason="Deleting Gallery Messages")
                        await asyncio.sleep(0.5)

        ###===== IF THERE IS SLOW DELETE DATA
        # WE CANNOT DELETE THESE MESSAGES IN BULK, ONLY ONE BY ONE.
        if slow_delete:
            for record in slow_delete:

                await self.bot.http.delete_message(record['ch_id'], record['msg_id'], reason="Deleting Gallery Messages")
                await asyncio.sleep(0.5)


        ###==== LOOP THE SCHEDULER
        self.scheduler.add_job( call_schedule,
                                'date',
                                id="_delete_gallery_messages",
                                run_date=get_next(hours=self.gal_text_expirein),
                                kwargs={"func": "_delete_gallery_messages"}
                                )
        return