Beispiel #1
0
    async def _unmuting(
        self,
        ctx,
        member: discord.Member,
        *,
        reason='-No reason specified-'
    ):  # TODO: Allow IDs to be unmuted (in the case of not being in the guild)
        if len(reason) > 990:
            return await ctx.send(
                f'{config.redTick} Unmute reason is too long, reduce it by at least {len(reason) - 990} characters'
            )
        db = mclient.bowser.puns
        muteRole = ctx.guild.get_role(config.mute)
        action = db.find_one_and_update(
            {
                'user': member.id,
                'type': 'mute',
                'active': True
            }, {'$set': {
                'active': False
            }})
        if not action:
            return await ctx.send(
                f'{config.redTick} Cannot unmute {member} ({member.id}), they are not currently muted'
            )

        docID = await tools.issue_pun(member.id,
                                      ctx.author.id,
                                      'unmute',
                                      reason,
                                      context=action['_id'],
                                      active=False)
        await member.remove_roles(
            muteRole, reason='Unmute action performed by moderator')
        await tools.send_modlog(self.bot,
                                self.modLogs,
                                'unmute',
                                docID,
                                reason,
                                user=member,
                                moderator=ctx.author,
                                public=True)

        try:
            await member.send(tools.format_pundm('unmute', reason, ctx.author))

        except (discord.Forbidden, AttributeError):
            if not tools.mod_cmd_invoke_delete(ctx.channel):
                await ctx.send(
                    f'{config.greenTick} {member} ({member.id}) has been successfully unmuted. I was not able to DM them about this action'
                )

            return

        if tools.mod_cmd_invoke_delete(ctx.channel):
            return await ctx.message.delete()

        await ctx.send(
            f'{config.greenTick} {member} ({member.id}) has been successfully unmuted'
        )
Beispiel #2
0
    async def _kicking(self,
                       ctx,
                       member: discord.Member,
                       *,
                       reason='-No reason specified-'):
        if len(reason) > 990:
            return await ctx.send(
                f'{config.redTick} Kick reason is too long, reduce it by at least {len(reason) - 990} characters'
            )
        docID = await tools.issue_pun(member.id,
                                      ctx.author.id,
                                      'kick',
                                      reason,
                                      active=False)
        await tools.send_modlog(self.bot,
                                self.modLogs,
                                'kick',
                                docID,
                                reason,
                                user=member,
                                moderator=ctx.author,
                                public=True)
        try:
            await member.send(tools.format_pundm('kick', reason, ctx.author))

        except (discord.Forbidden, AttributeError):
            if not tools.mod_cmd_invoke_delete(ctx.channel):
                await ctx.send(
                    f'{config.greenTick} {member} ({member.id}) has been successfully kicked. I was not able to DM them about this action'
                )

            await member.kick(reason='Kick action performed by moderator')
            return

        if tools.mod_cmd_invoke_delete(ctx.channel):
            return await ctx.message.delete()

        await ctx.send(
            f'{config.greenTick} {member} ({member.id}) has been successfully kicked'
        )
Beispiel #3
0
    async def expire_actions(self, _id, guild):
        db = mclient.bowser.puns
        doc = db.find_one({'_id': _id})
        if not doc:
            logging.error(
                f'[Moderation] Expiry failed. Doc {_id} does not exist!')
            return

        # Lets do a sanity check.
        if not doc['active']:
            logging.debug(
                f'[Moderation] Expiry failed. Doc {_id} is not active but was scheduled to expire!'
            )
            return

        twelveHr = 60 * 60 * 12
        if doc['type'] == 'strike':
            userDB = mclient.bowser.users
            user = userDB.find_one({'_id': doc['user']})
            try:
                if user['strike_check'] > time.time():
                    # To prevent drift we recall every 12 hours. Schedule for 12hr or expiry time, whichever is sooner
                    retryTime = (twelveHr if
                                 user['strike_check'] - time.time() > twelveHr
                                 else user['strike_check'] - time.time())
                    self.taskHandles.append(
                        self.bot.loop.call_later(
                            retryTime, asyncio.create_task,
                            self.expire_actions(_id, guild)))
                    return

            except KeyError:  # This is a rare edge case, but if a pun is manually created the user may not have the flag yet. More a dev handler than not
                logging.error(
                    f'[Moderation] Expiry failed. Could not get strike_check from db.users resolving for pun {_id}, was it manually added?'
                )

            # Start logic
            if doc['active_strike_count'] - 1 == 0:
                db.update_one({'_id': doc['_id']}, {
                    '$set': {
                        'active': False
                    },
                    '$inc': {
                        'active_strike_count': -1
                    }
                })
                strikes = [
                    x for x in db.find({
                        'user': doc['user'],
                        'type': 'strike',
                        'active': True
                    }).sort('timestamp', 1)
                ]
                if not strikes:  # Last active strike expired, no additional
                    return

                self.taskHandles.append(
                    self.bot.loop.call_later(
                        60 * 60 * 12, asyncio.create_task,
                        self.expire_actions(strikes[0]['_id'], guild)))

            elif doc['active_strike_count'] > 0:
                db.update_one({'_id': doc['_id']},
                              {'$inc': {
                                  'active_strike_count': -1
                              }})
                self.taskHandles.append(
                    self.bot.loop.call_later(
                        60 * 60 * 12, asyncio.create_task,
                        self.expire_actions(doc['_id'], guild)))

            else:
                logging.warning(
                    f'[Moderation] Expiry failed. Doc {_id} had a negative active strike count and was skipped'
                )
                return

            userDB.update_one(
                {'_id': doc['user']},
                {'$set': {
                    'strike_check': time.time() + 60 * 60 * 24 * 7
                }})

        elif doc['type'] == 'mute' and doc[
                'expiry']:  # A mute that has an expiry
            # To prevent drift we recall every 12 hours. Schedule for 12hr or expiry time, whichever is sooner
            if doc['expiry'] > time.time():
                retryTime = twelveHr if doc['expiry'] - time.time(
                ) > twelveHr else doc['expiry'] - time.time()
                self.taskHandles.append(
                    self.bot.loop.call_later(retryTime, asyncio.create_task,
                                             self.expire_actions(_id, guild)))
                return

            punGuild = self.bot.get_guild(guild)
            try:
                member = await punGuild.fetch_member(doc['user'])

            except discord.NotFound:
                # User has left the server after the mute was issued. Lets just move on and let on_member_join handle on return
                return

            except discord.HTTPException:
                # Issue with API, lets just try again later in 30 seconds
                self.taskHandles.append(
                    self.bot.loop.call_later(30, asyncio.create_task,
                                             self.expire_actions(_id, guild)))
                return

            newPun = db.find_one_and_update({'_id': doc['_id']},
                                            {'$set': {
                                                'active': False
                                            }})
            docID = await tools.issue_pun(doc['user'],
                                          self.bot.user.id,
                                          'unmute',
                                          'Mute expired',
                                          active=False,
                                          context=doc['_id'])

            if not newPun:  # There is near zero reason this would ever hit, but in case...
                logging.error(
                    f'[Moderation] Expiry failed. Database failed to update user on pun expiration of {doc["_id"]}'
                )

            await member.remove_roles(self.roles[doc['type']])
            try:
                await member.send(
                    tools.format_pundm('unmute',
                                       'Mute expired',
                                       None,
                                       auto=True))

            except discord.Forbidden:  # User has DMs off
                pass

            await tools.send_modlog(
                self.bot,
                self.modLogs,
                'unmute',
                docID,
                'Mute expired',
                user=member,
                moderator=self.bot.user,
                public=True,
            )
Beispiel #4
0
    async def _strike_set(self, ctx, member: discord.Member,
                          count: StrikeRange, *, reason):
        punDB = mclient.bowser.puns
        activeStrikes = 0
        puns = punDB.find({
            'user': member.id,
            'type': 'strike',
            'active': True
        })
        for pun in puns:
            activeStrikes += pun['active_strike_count']

        if activeStrikes == count:
            return await ctx.send(
                f'{config.redTick} That user already has {activeStrikes} active strikes'
            )

        elif (
                count > activeStrikes
        ):  # This is going to be a positive diff, lets just do the math and defer work to _strike()
            return await self._strike(ctx,
                                      member,
                                      count - activeStrikes,
                                      reason=reason)

        else:  # Negative diff, we will need to reduce our strikes
            diff = activeStrikes - count

            puns = punDB.find({
                'user': member.id,
                'type': 'strike',
                'active': True
            }).sort('timestamp', 1)
            for pun in puns:
                if pun['active_strike_count'] - diff >= 0:
                    userDB = mclient.bowser.users
                    punDB.update_one(
                        {'_id': pun['_id']},
                        {
                            '$set': {
                                'active_strike_count':
                                pun['active_strike_count'] - diff,
                                'active':
                                pun['active_strike_count'] - diff > 0,
                            }
                        },
                    )
                    userDB.update_one({'_id': member.id}, {
                        '$set': {
                            'strike_check': time.time() + (60 * 60 * 24 * 7)
                        }
                    })
                    self.taskHandles.append(
                        self.bot.loop.call_later(
                            60 * 60 * 12, asyncio.create_task,
                            self.expire_actions(pun['_id'], ctx.guild.id))
                    )  # Check in 12 hours, prevents time drifting

                    # Logic to calculate the remaining (diff) strikes will simplify to 0
                    # new_diff = diff - removed_strikes
                    #          = diff - (old_strike_amount - new_strike_amount)
                    #          = diff - (old_strike_amount - (old_strike_amount - diff))
                    #          = diff - old_strike_amount + old_strike_amount - diff
                    #          = 0
                    diff = 0
                    break

                elif pun['active_strike_count'] - diff < 0:
                    punDB.update_one(
                        {'_id': pun['_id']},
                        {'$set': {
                            'active_strike_count': 0,
                            'active': False
                        }})
                    diff -= pun['active_strike_count']

            if diff != 0:  # Something has gone horribly wrong
                raise ValueError('Diff != 0 after full iteration')

            docID = await tools.issue_pun(member.id,
                                          ctx.author.id,
                                          'destrike',
                                          reason=reason,
                                          active=False,
                                          strike_count=activeStrikes - count)
            await tools.send_modlog(
                self.bot,
                self.modLogs,
                'destrike',
                docID,
                reason,
                user=member,
                moderator=ctx.author,
                extra_author=(activeStrikes - count),
                public=True,
            )
            error = ""
            try:
                await member.send(
                    tools.format_pundm('destrike',
                                       reason,
                                       ctx.author,
                                       details=activeStrikes - count))
            except discord.Forbidden:
                error = 'I was not able to DM them about this action'

            if tools.mod_cmd_invoke_delete(ctx.channel):
                return await ctx.message.delete()

            await ctx.send(
                f'{member} ({member.id}) has had {activeStrikes - count} strikes removed, '
                f'they now have {activeStrikes} strike{"s" if activeStrikes > 1 else ""} '
                f'({activeStrikes+count} - {count}) {error}')
Beispiel #5
0
    async def _strike(self,
                      ctx,
                      member: discord.Member,
                      count: typing.Optional[StrikeRange] = 1,
                      *,
                      reason):
        if count == 0:
            return await ctx.send(
                f'{config.redTick} You cannot issue less than one strike. If you need to reset this user\'s strikes to zero instead use `{ctx.prefix}strike set`'
            )

        if len(reason) > 990:
            return await ctx.send(
                f'{config.redTick} Strike reason is too long, reduce it by at least {len(reason) - 990} characters'
            )
        punDB = mclient.bowser.puns
        userDB = mclient.bowser.users

        activeStrikes = 0
        for pun in punDB.find({
                'user': member.id,
                'type': 'strike',
                'active': True
        }):
            activeStrikes += pun['active_strike_count']

        activeStrikes += count
        if activeStrikes > 16:  # Max of 16 active strikes
            return await ctx.send(
                f'{config.redTick} Striking {count} time{"s" if count > 1 else ""} would exceed the maximum of 16 strikes. The amount being issued must be lowered by at least {activeStrikes - 16} or consider banning the user instead'
            )

        docID = await tools.issue_pun(member.id,
                                      ctx.author.id,
                                      'strike',
                                      reason,
                                      strike_count=count,
                                      public=True)
        userDB.update_one(
            {'_id': member.id},
            {'$set': {
                'strike_check': time.time() + (60 * 60 * 24 * 7)
            }})  # 7 days

        self.taskHandles.append(
            self.bot.loop.call_later(60 * 60 * 12, asyncio.create_task,
                                     self.expire_actions(docID, ctx.guild.id))
        )  # Check in 12 hours, prevents time drifting
        await tools.send_modlog(
            self.bot,
            self.modLogs,
            'strike',
            docID,
            reason,
            user=member,
            moderator=ctx.author,
            extra_author=count,
            public=True,
        )
        content = (
            f'{config.greenTick} {member} ({member.id}) has been successfully struck, '
            f'they now have {activeStrikes} strike{"s" if activeStrikes > 1 else ""} ({activeStrikes-count} + {count})'
        )
        try:
            await member.send(
                tools.format_pundm('strike', reason, ctx.author,
                                   details=count))

        except discord.Forbidden:
            if not tools.mod_cmd_invoke_delete(ctx.channel):
                content += '. I was not able to DM them about this action'
                if activeStrikes == 16:
                    content += '.\n:exclamation: You may want to consider a ban'

                await ctx.send(content)

            return

        if tools.mod_cmd_invoke_delete(ctx.channel):
            return await ctx.message.delete()

        if activeStrikes == 16:
            content += '.\n:exclamation: You may want to consider a ban'

        await ctx.send(content)
Beispiel #6
0
    async def _muting(self,
                      ctx,
                      member: discord.Member,
                      duration,
                      *,
                      reason='-No reason specified-'):
        if len(reason) > 990:
            return await ctx.send(
                f'{config.redTick} Mute reason is too long, reduce it by at least {len(reason) - 990} characters'
            )
        db = mclient.bowser.puns
        if db.find_one({'user': member.id, 'type': 'mute', 'active': True}):
            return await ctx.send(
                f'{config.redTick} {member} ({member.id}) is already muted')

        muteRole = ctx.guild.get_role(config.mute)
        try:
            _duration = tools.resolve_duration(duration)
            try:
                if int(duration):
                    raise TypeError

            except ValueError:
                pass

        except (KeyError, TypeError):
            return await ctx.send(f'{config.redTick} Invalid duration passed')

        docID = await tools.issue_pun(member.id, ctx.author.id, 'mute', reason,
                                      int(_duration.timestamp()))
        await member.add_roles(muteRole,
                               reason='Mute action performed by moderator')
        await tools.send_modlog(
            self.bot,
            self.modLogs,
            'mute',
            docID,
            reason,
            user=member,
            moderator=ctx.author,
            expires=
            f'{_duration.strftime("%B %d, %Y %H:%M:%S UTC")} ({tools.humanize_duration(_duration)})',
            public=True,
        )
        error = ""
        try:
            await member.send(
                tools.format_pundm('mute', reason, ctx.author,
                                   tools.humanize_duration(_duration)))

        except (discord.Forbidden, AttributeError):
            error = '. I was not able to DM them about this action'

        if not tools.mod_cmd_invoke_delete(ctx.channel):
            await ctx.send(
                f'{config.greenTick} {member} ({member.id}) has been successfully muted{error}'
            )

        twelveHr = 60 * 60 * 12
        expireTime = time.mktime(_duration.timetuple())
        logging.info(f'using {expireTime}')
        tryTime = twelveHr if expireTime - time.time(
        ) > twelveHr else expireTime - time.time()
        self.taskHandles.append(
            self.bot.loop.call_later(tryTime, asyncio.create_task,
                                     self.expire_actions(docID, ctx.guild.id)))
        if tools.mod_cmd_invoke_delete(ctx.channel):
            return await ctx.message.delete()
Beispiel #7
0
    async def _banning(self,
                       ctx,
                       users: commands.Greedy[ResolveUser],
                       *,
                       reason='-No reason specified-'):
        if len(reason) > 990:
            return await ctx.send(
                f'{config.redTick} Ban reason is too long, reduce it by at least {len(reason) - 990} characters'
            )
        if not users:
            return await ctx.send(
                f'{config.redTick} An invalid user was provided')
        banCount = 0
        failedBans = 0
        for user in users:
            userid = user if (type(user) is int) else user.id

            username = userid if (type(user) is int) else f'{str(user)}'
            user = (discord.Object(id=userid) if (type(user) is int) else user
                    )  # If not a user, manually contruct a user object
            try:
                await ctx.guild.fetch_ban(user)
                if len(users) == 1:
                    return await ctx.send(
                        f'{config.redTick} {username} is already banned')

                else:
                    # If a many-user ban, don't exit if a user is already banned
                    failedBans += 1
                    continue

            except discord.NotFound:
                pass

            try:
                await user.send(tools.format_pundm('ban', reason, ctx.author))

            except (discord.Forbidden, AttributeError):
                pass

            try:
                await ctx.guild.ban(
                    user,
                    reason=f'Ban action performed by moderator',
                    delete_message_days=3)

            except discord.NotFound:
                # User does not exist
                if len(users) == 1:
                    return await ctx.send(
                        f'{config.redTick} User {userid} does not exist')

                failedBans += 1
                continue

            docID = await tools.issue_pun(userid,
                                          ctx.author.id,
                                          'ban',
                                          reason=reason)
            await tools.send_modlog(
                self.bot,
                self.modLogs,
                'ban',
                docID,
                reason,
                username=username,
                userid=userid,
                moderator=ctx.author,
                public=True,
            )
            banCount += 1

        if tools.mod_cmd_invoke_delete(ctx.channel):
            return await ctx.message.delete()

        if len(users) == 1:
            await ctx.send(
                f'{config.greenTick} {users[0]} has been successfully banned')

        else:
            resp = f'{config.greenTick} **{banCount}** users have been successfully banned'
            if failedBans:
                resp += f'. Failed to ban **{failedBans}** from the provided list'
            return await ctx.send(resp)
Beispiel #8
0
    async def _kicking(self,
                       ctx,
                       users: commands.Greedy[ResolveUser],
                       *,
                       reason='-No reason specified-'):
        if len(reason) > 990:
            return await ctx.send(
                f'{config.redTick} Kick reason is too long, reduce it by at least {len(reason) - 990} characters'
            )
        if not users:
            return await ctx.send(
                f'{config.redTick} An invalid user was provided')

        kickCount = 0
        failedKicks = 0
        couldNotDM = False

        for user in users:
            userid = user if (type(user) is int) else user.id
            username = userid if (type(user) is int) else f'{str(user)}'

            user = (discord.Object(id=userid) if (type(user) is int) else user
                    )  # If not a user, manually contruct a user object

            try:
                member = await ctx.guild.fetch_member(userid)
            except discord.HTTPException:  # Member not in guild
                if len(users) == 1:
                    return await ctx.send(
                        f'{config.redTick} {username} is not the server!')

                else:
                    # If a many-user kick, don't exit if a user is already gone
                    failedKicks += 1
                    continue

            usr_role_pos = member.top_role.position

            if (usr_role_pos >= ctx.guild.me.top_role.position) or (
                    usr_role_pos >= ctx.author.top_role.position):
                if len(users) == 1:
                    return await ctx.send(
                        f'{config.redTick} Insufficent permissions to kick {username}'
                    )
                else:
                    failedKicks += 1
                    continue

            try:
                await user.send(tools.format_pundm('kick', reason, ctx.author))
            except (discord.Forbidden, AttributeError):
                couldNotDM = True
                pass

            try:
                await member.kick(reason='Kick action performed by moderator')
            except (discord.Forbidden):
                failedKicks += 1
                continue

            docID = await tools.issue_pun(member.id,
                                          ctx.author.id,
                                          'kick',
                                          reason,
                                          active=False)
            await tools.send_modlog(self.bot,
                                    self.modLogs,
                                    'kick',
                                    docID,
                                    reason,
                                    user=member,
                                    moderator=ctx.author,
                                    public=True)
            kickCount += 1

        if tools.mod_cmd_invoke_delete(ctx.channel):
            return await ctx.message.delete()

        if ctx.author.id != self.bot.user.id:  # Non-command invoke, such as automod
            if len(users) == 1:
                resp = f'{config.greenTick} {users[0]} has been successfully kicked'
                if couldNotDM:
                    resp += '. I was not able to DM them about this action'

            else:
                resp = f'{config.greenTick} **{kickCount}** users have been successfully kicked'
                if failedKicks:
                    resp += f'. Failed to kick **{failedKicks}** from the provided list'

            return await ctx.send(resp)
Beispiel #9
0
    async def _infraction_editing(self,
                                  ctx,
                                  infraction,
                                  reason,
                                  duration=None):
        db = mclient.bowser.puns
        doc = db.find_one({'_id': infraction})
        if not doc:
            return await ctx.send(
                f'{config.redTick} An invalid infraction id was provided')

        if not doc['active'] and duration:
            return await ctx.send(
                f'{config.redTick} That infraction has already expired and the duration cannot be edited'
            )

        if duration and doc[
                'type'] != 'mute':  # TODO: Should we support strikes in the future?
            return ctx.send(
                f'{config.redTick} Setting durations is not supported for {doc["type"]}'
            )

        user = await self.bot.fetch_user(doc['user'])
        if duration:
            try:
                _duration = tools.resolve_duration(duration)
                humanized = tools.humanize_duration(_duration)
                expireStr = f'{_duration.strftime("%B %d, %Y %H:%M:%S UTC")} ({humanized})'
                stamp = _duration.timestamp()
                try:
                    if int(duration):
                        raise TypeError

                except ValueError:
                    pass

            except (KeyError, TypeError):
                return await ctx.send(
                    f'{config.redTick} Invalid duration passed')

            if stamp - time.time() < 60:  # Less than a minute
                return await ctx.send(
                    f'{config.redTick} Cannot set the new duration to be less than one minute'
                )

            db.update_one({'_id': infraction},
                          {'$set': {
                              'expiry': int(stamp)
                          }})
            await tools.send_modlog(
                self.bot,
                self.modLogs,
                'duration-update',
                doc['_id'],
                reason,
                user=user,
                moderator=ctx.author,
                expires=expireStr,
                extra_author=doc['type'].capitalize(),
            )

        else:
            db.update_one({'_id': infraction}, {'$set': {'reason': reason}})
            await tools.send_modlog(
                self.bot,
                self.modLogs,
                'reason-update',
                doc['_id'],
                reason,
                user=user,
                moderator=ctx.author,
                extra_author=doc['type'].capitalize(),
                updated=doc['reason'],
            )

        try:
            pubChannel = self.bot.get_channel(doc['public_log_channel'])
            pubMessage = await pubChannel.fetch_message(
                doc['public_log_message'])
            embed = pubMessage.embeds[0]
            embedDict = embed.to_dict()
            newEmbedDict = copy.deepcopy(embedDict)
            listIndex = 0
            for field in embedDict['fields']:
                # We are working with the dict because some logs can have `reason` at different indexes and we should not assume index position
                if duration and field['name'] == 'Expires':
                    # This is subject to a breaking change if `name` updated, but I'll take the risk
                    newEmbedDict['fields'][listIndex]['value'] = expireStr
                    break

                elif not duration and field['name'] == 'Reason':
                    newEmbedDict['fields'][listIndex]['value'] = reason
                    break

                listIndex += 1

            assert (
                embedDict['fields'] != newEmbedDict['fields']
            )  # Will fail if message was unchanged, this is likely because of a breaking change upstream in the pun flow
            newEmbed = discord.Embed.from_dict(newEmbedDict)
            await pubMessage.edit(embed=newEmbed)

        except Exception as e:
            logging.error(f'[Moderation] _infraction_duration: {e}')

        error = ''
        try:
            member = await ctx.guild.fetch_member(doc['user'])
            if duration:
                await member.send(
                    tools.format_pundm('duration-update',
                                       reason,
                                       details=(doc['type'], expireStr)))

            else:
                await member.send(
                    tools.format_pundm(
                        'reason-update',
                        reason,
                        details=(
                            doc['type'],
                            datetime.datetime.utcfromtimestamp(
                                doc['timestamp']).strftime(
                                    "%B %d, %Y %H:%M:%S UTC"),
                        ),
                    ))

        except (discord.NotFound, discord.Forbidden, AttributeError):
            error = '. I was not able to DM them about this action'

        await ctx.send(
            f'{config.greenTick} The {doc["type"]} {"duration" if duration else "reason"} has been successfully updated for {user} ({user.id}){error}'
        )