Exemple #1
0
    async def on_help_claim(self, channel, member):
        async with self.claim_lock:
            self.claimed[member.id] = channel.id

            log.info('%s claiming %s', po(member), po(channel))

            try:
                await channel.edit(category=self.claimed_category,
                                   topic=f'Channel claimed by {member.name}.',
                                   sync_permissions=True)
            except discord.HTTPException:
                log.warning('Failed moving %s to claimed category.',
                            po(channel))
                return

            if len(self.open_category.text_channels) >= 5:
                return

            try:
                new_channel = self.pool_category.text_channels[-1]
            except IndexError:
                log.warning('No more channels in pool.')
                return

            try:
                await new_channel.edit(
                    category=self.open_category,
                    topic='Type a message in this channel to claim it',
                    sync_permissions=True)
                await new_channel.send(embed=discord.Embed(
                    description=GUIDE_MESSAGE))
            except discord.HTTPException:
                log.warning('Failed moving pool channel %s to ',
                            po(new_channel))
Exemple #2
0
    async def close(self, ctx):
        ''' Un-claims a help channel, and moves it back into the pool of available help channels. '''

        is_mod = await ctx.is_mod()

        if is_mod or self.claimed.get(ctx.author.id, None) == ctx.channel.id:
            if is_mod:
                log.info('%s manually closing %s', po(ctx.author),
                         po(ctx.channel))

            self.bot.dispatch('help_release', ctx.channel)
        else:
            raise commands.CommandError('You can\'t do that.')
Exemple #3
0
    async def on_welcome(self, member, channel, message):
        replace_table = dict(guild=member.guild.name,
                             user=member.mention,
                             member_count=member.guild.member_count)

        for key, val in replace_table.items():
            message = message.replace('{' + key + '}', str(val))

        log.info('Sending welcome message for %s in %s', po(member),
                 po(member.guild))

        try:
            await channel.send(message)
        except disnake.HTTPException:
            pass
Exemple #4
0
    async def unmute(self, ctx, *, member: discord.Member):
        '''Unmute a member. Requires Manage Roles perms.'''

        if await ctx.is_mod(member):
            raise commands.CommandError('Can\'t unmute this member.')

        conf = await self.config.get_entry(ctx.guild.id)
        mute_role = conf.mute_role

        if mute_role is None:
            raise commands.CommandError('Mute role not set or not found.')

        if mute_role not in member.roles:
            raise commands.CommandError('Member not previously muted.')

        pretty_author = po(ctx.author)

        try:
            await member.remove_roles(
                mute_role, reason='Unmuted by {0}'.format(pretty_author))
        except discord.HTTPException:
            raise commands.CommandError('Failed removing mute role.')

        try:
            await ctx.send('{0} unmuted.'.format(str(member)))
        except discord.HTTPException:
            pass

        self.bot.dispatch('log',
                          ctx.guild,
                          member,
                          action='UNMUTE',
                          severity=Severity.RESOLVED,
                          message=ctx.message,
                          responsible=pretty_author)
Exemple #5
0
	async def remindme(self, ctx, *, when_and_what: ReminderConverter()):
		'''Create a new reminder.'''

		now, when, message = when_and_what

		if when < now:
			raise commands.CommandError('Specified time is in the past.')

		if when - now > MAX_DELTA:
			raise commands.CommandError('Sorry, can\'t remind in more than a year in the future.')

		if message is not None and len(message) > 1024:
			raise commands.CommandError('Sorry, keep the message below 1024 characters!')

		count = await self.db.fetchval('SELECT COUNT(id) FROM remind WHERE user_id=$1', ctx.author.id)
		if count > MAX_REMINDERS:
			raise commands.CommandError(f'Sorry, you can\'t have more than {MAX_REMINDERS} active reminders at once.')

		await self.db.execute(
			'INSERT INTO remind (guild_id, channel_id, user_id, made_on, remind_on, message) VALUES ($1, $2, $3, $4, $5, $6)',
			ctx.guild.id, ctx.channel.id, ctx.author.id, now, when, message
		)

		self.timer.maybe_restart(when)

		remind_in = when - now
		remind_in += timedelta(microseconds=1000000 - (remind_in.microseconds % 1000000))

		await ctx.send('You will be reminded in {}.'.format(pretty_timedelta(remind_in)))

		log.info('%s set a reminder for %s.', po(ctx.author), pretty_datetime(when))
Exemple #6
0
    async def on_reminder_complete(self, record):
        _id = record.get('id')
        guild_id = record.get('guild_id')
        channel_id = record.get('channel_id')
        user_id = record.get('user_id')
        message_id = record.get('message_id')
        made_on = record.get('made_on')
        message = record.get('message')

        channel = self.bot.get_channel(channel_id)
        user = self.bot.get_user(user_id)

        desc = message or DEFAULT_REMINDER_MESSAGE

        if message_id is not None:
            jump_url = 'https://discord.com/channels/{0}/{1}/{2}'.format(
                guild_id, channel_id, message_id)
            desc += f'\n\n[Click for context!]({jump_url})'

        e = discord.Embed(title='Reminder',
                          description=desc,
                          timestamp=made_on)

        e.set_footer(text=f'#{channel.name}')

        try:
            if channel is not None:
                await channel.send(content=f'<@{user_id}>', embed=e)
            elif user is not None:
                await user.send(embed=e)
        except discord.HTTPException as exc:
            log.info('Failed sending reminder #%s for %s - %s', _id, po(user),
                     str(exc))
Exemple #7
0
    async def ban_complete(self, record):
        guild_id = record.get('guild_id')
        user_id = record.get('user_id')
        mod_id = record.get('mod_id')
        duration = record.get('duration')
        userdata = loads(record.get('userdata'))
        reason = record.get('reason')

        guild = self.bot.get_guild(guild_id)
        if guild is None:
            return

        mod = guild.get_member(mod_id)
        pretty_mod = '(ID: {0})'.format(
            str(mod_id)) if mod is None else po(mod)

        member = FakeUser(user_id, guild, **userdata)

        try:
            await guild.unban(
                member,
                reason='Completed tempban issued by {0}'.format(pretty_mod))
        except discord.HTTPException:
            return  # rip :)

        self.bot.dispatch('log',
                          guild,
                          member,
                          action='TEMPBAN COMPLETED',
                          severity=Severity.RESOLVED,
                          responsible=pretty_mod,
                          duration=pretty_timedelta(duration),
                          reason=reason)
Exemple #8
0
    async def do_action(self, message, action, reason, delete_message_days=0):
        '''Called when an event happens.'''

        member: disnake.Member = message.author
        guild: disnake.Guild = message.guild

        ctx = await self.bot.get_context(message, cls=AceContext)

        # ignore if member is mod
        if await ctx.is_mod():
            self.bot.dispatch(
                'log',
                guild,
                member,
                action='IGNORED {0} (MEMBER IS MOD)'.format(action.name),
                severity=Severity.LOW,
                message=message,
                reason=reason,
            )

            return

        # otherwise, check against security actions and perform punishment
        try:
            if action is SecurityAction.TIMEOUT:
                await member.timeout(until=datetime.utcnow() +
                                     timedelta(days=28),
                                     reason=reason)
            elif action is SecurityAction.KICK:
                await member.kick(reason=reason)
            elif action is SecurityAction.BAN:
                await member.ban(delete_message_days=delete_message_days,
                                 reason=reason)

        except Exception as exc:
            # log error if something happened
            self.bot.dispatch('log',
                              guild,
                              member,
                              action='{0} FAILED'.format(action.name),
                              severity=Severity.HIGH,
                              message=message,
                              reason=reason,
                              error=str(exc))
            return

        # log successful security event
        self.bot.dispatch('log',
                          guild,
                          member,
                          action=action.name,
                          severity=Severity(action.value),
                          message=message,
                          reason=reason)

        try:
            await message.channel.send('{0} {1}: {2}'.format(
                po(member), SecurityVerb[action.name].value, reason))
        except disnake.HTTPException:
            pass
Exemple #9
0
    async def on_ready(self):
        if not self.ready.is_set():
            self.load_extensions()

            self.loop.create_task(self.update_dbl())

            self.ready.set()
            log.info('Ready! %s', po(self.user))
Exemple #10
0
	async def on_open_message(self, message: disnake.Message):
		claimed_id = await self.has_channel(message.author.id)
		if claimed_id is not None:
			log.info(f'User has already claimed #{message.guild.get_channel(claimed_id)}')
			await self.post_error(message, f'You have already claimed <#{claimed_id}>, please ask your question there.')
			return

		now = disnake.utils.utcnow()
		author: disnake.Member = message.author

		# check whether author has claimed another channel within the last MINIMUM_CLAIM_INTERVAL time
		last_claim_at = self._claimed_at.get(author.id, None)
		if last_claim_at is not None and last_claim_at > now - MINIMUM_CLAIM_INTERVAL:  # and False:
			log.info(
				f'{author} previously claimed a channel {pretty_timedelta(now - last_claim_at)} ago, '
				f'should wait {pretty_timedelta(MINIMUM_CLAIM_INTERVAL - (now - last_claim_at))}'
			)

			await self.post_error(message, f'Please wait a bit before claiming another channel.')
			return

		channel: disnake.TextChannel = message.channel

		log.info('%s claiming %s', po(author), po(channel))

		# activate the channel
		self.set_state(channel, ChannelState.ACTIVATING)
		await self.set_claimant(channel.id, message.author.id)
		create_task(self.activate_channel(message))

		# maybe open new channel

		if len(self.open_channels(forecast=True)) >= OPEN_CHANNEL_COUNT:
			log.info('No need to open another channel')
			return

		closed_channels = self.closed_channels(forecast=False)
		if not closed_channels:
			log.info('No closed channels available to move to open category!')
			return

		to_open = closed_channels[0]

		self.set_state(to_open, ChannelState.OPENING)
		create_task(self.open_channel(to_open))
Exemple #11
0
	async def close(self, ctx):
		controller: Controller = self.controllers.get(ctx.message.guild.id, None)
		if not controller:
			return

		# check that this is a channel in an active category that can actually be closed
		if ctx.channel.category != controller.active_category:
			return

		is_mod = await ctx.is_mod()
		channel_claimant_id = await controller.get_claimant(ctx.channel.id)

		if not is_mod and channel_claimant_id != ctx.author.id:
			raise commands.CommandError('You can\'t do that.')

		log.info('%s is closing %s', po(ctx.author), po(ctx.channel))

		await controller.close_channel(ctx.channel)
Exemple #12
0
	async def close(self, ctx):
		'''Releases a help channel, and moves it back into the pool of closed help channels.'''

		if ctx.channel.category != self.active_category:
			return

		is_mod = await ctx.is_mod()
		claimed_channel_id = self.claimed_channel.get(ctx.author.id, None)
		claimed_at = self.claimed_at.get(ctx.author.id, None)

		if is_mod or claimed_channel_id == ctx.channel.id:
			if is_mod:
				log.info('%s force-closing closing %s', po(ctx.author), po(ctx.channel))
			elif claimed_at is not None and claimed_at > datetime.utcnow() - MINIMUM_LEASE:
				raise commands.CommandError(f'Please wait at least {pretty_timedelta(MINIMUM_LEASE)} after claiming before closing a help channel.')

			await self.close_channel(ctx.channel)
		else:
			raise commands.CommandError('You can\'t do that.')
Exemple #13
0
    async def clear(self,
                    ctx,
                    message_count: int,
                    user: MaybeMemberConverter = None):
        '''Simple purge command. Clear messages, either from user or indiscriminately.'''

        if message_count < 1:
            raise commands.CommandError(
                'Please choose a positive message amount to clear.')

        if message_count > 100:
            raise commands.CommandError(
                'Please choose a message count below 100.')

        def all_check(msg):
            if msg.id == RULES_MSG_ID:
                return False
            return True

        def user_check(msg):
            return msg.author.id == user.id and all_check(msg)

        try:
            await ctx.message.delete()
        except discord.HTTPException:
            pass

        try:
            deleted = await ctx.channel.purge(
                limit=message_count,
                check=all_check if user is None else user_check)
        except discord.HTTPException:
            raise commands.CommandError(
                'Failed deleting messages. Does the bot have the necessary permissions?'
            )

        count = len(deleted)

        log.info('%s cleared %s messages in %s', po(ctx.author), count,
                 po(ctx.guild))

        await ctx.send(f'Deleted {count} message{"s" if count > 1 else ""}.',
                       delete_after=5)
Exemple #14
0
    async def helper_purge(self):
        guild = self.bot.get_guild(AHK_GUILD_ID)
        if guild is None:
            return

        role = guild.get_role(HELPERS_ROLE_ID)
        if role is None:
            return

        past = datetime.utcnow() - INACTIVITY_LIMIT

        all_helpers = list(member for member in guild.members
                           if role in member.roles)
        helpers = list(member for member in all_helpers
                       if member.joined_at < past)

        spare = await self.bot.db.fetch(
            'SELECT user_id FROM seen WHERE guild_id=$1 AND user_id=ANY($2::bigint[]) AND seen>$3',
            AHK_GUILD_ID, list(member.id for member in helpers), past)

        spare_ids = list(record.get('user_id') for record in spare)
        remove = list(member for member in helpers
                      if member.id not in spare_ids)

        if not remove:
            return

        log.info('About to purge %s helpers. Current list: %s', len(remove),
                 ', '.join(list(str(member.id) for member in all_helpers)))

        log.info('Removing inactive helpers:\n%s',
                 '\n'.join(list(po(member) for member in remove)))

        reason = 'Removed helper inactive for over {0}.'.format(
            pretty_timedelta(INACTIVITY_LIMIT))

        for member in remove:
            try:
                await member.remove_roles(role, reason=reason)
            except discord.HTTPException as e:
                self.bot.dispatch(
                    'log',
                    guild,
                    member,
                    action='FAILED REMOVING HELPER',
                    reason='Failed removing role.\n\nException:\n```{}```'.
                    format(str(e)),
                )
                continue

            self.bot.dispatch('log',
                              guild,
                              member,
                              action='REMOVE HELPER',
                              reason=reason)
Exemple #15
0
    async def muterole(self, ctx, *, role: discord.Role = None):
        '''Set the mute role. Only modifiable by server administrators. Leave argument empty to clear.'''

        conf = await self.config.get_entry(ctx.guild.id)

        if role is None:
            await conf.update(mute_role_id=None)
            await ctx.send('Mute role cleared.')
        else:
            await conf.update(mute_role_id=role.id)
            await ctx.send('Mute role has been set to {0}'.format(po(role)))
Exemple #16
0
    async def logchannel(self, ctx, *, channel: discord.TextChannel = None):
        '''Set a channel for the bot to log moderation-related messages.'''

        conf = await self.config.get_entry(ctx.guild.id)

        if channel is None:
            await conf.update(log_channel_id=None)
            await ctx.send('Log channel cleared.')
        else:
            await conf.update(log_channel_id=channel.id)
            await ctx.send('Log channel has been set to {0}'.format(
                po(channel)))
Exemple #17
0
	async def close_channel(self, channel: discord.TextChannel):
		'''Release a claimed channel from a member, making it closed.'''

		log.info(f'Closing #{channel}')

		owner_id = None

		for channel_owner_id, channel_id in self.claimed_channel.items():
			if channel_id == channel.id:
				owner_id = channel_owner_id
				break

		self.claimed_channel.pop(owner_id, None)
		self.claimed_messages.pop(channel.id, None)

		self._store_claims()

		log.info('Reclaiming %s from user id %s', po(channel), owner_id or 'UNKNOWN (not found in claims cache)')

		if self.should_open():
			log.info('Moving channel to open category since it needs channels')
			await self.open_channel(channel)
		else:
			log.info('Moving channel to closed category')

			try:
				current_first = self.closed_category.text_channels[0]
				to_pos = max(current_first.position - 1, 0)
			except IndexError:
				to_pos = max(c.position for c in channel.guild.channels) + 1

			opt = dict(
				position=to_pos,
				category=self.closed_category,
				sync_permissions=True,
				topic=f'<#{GET_HELP_CHAN_ID}>'
			)

			if self.has_postfix(channel):
				opt['name'] = self._stripped_name(channel)

			# send this before moving channel in case of rate limit shi
			try:
				await channel.send(embed=discord.Embed(description=CLOSED_MESSAGE, color=discord.Color.red()))
			except discord.HTTPException:
				pass

			await channel.edit(**opt)
Exemple #18
0
    async def on_help_release(self, channel):
        async with self.release_lock:
            owner_id = None

            for channel_owner_id, channel_id in self.claimed.items():
                if channel_id == channel.id:
                    owner_id = channel_owner_id

            if owner_id is not None:
                self.claimed.pop(owner_id)

            log.info('Reclaiming %s from user id %s', po(channel), owner_id)

            await channel.edit(category=self.pool_category,
                               topic='Open for claiming.',
                               sync_permissions=True)
Exemple #19
0
	async def newusers(self, ctx, *, count=5):
		'''List newly joined members.'''

		count = min(max(count, 5), 25)

		now = datetime.now(timezone.utc)
		e = disnake.Embed()

		for idx, member in enumerate(sorted(ctx.guild.members, key=lambda m: m.joined_at, reverse=True)):
			if idx >= count:
				break

			value = 'Joined {0} ago\nCreated {1} ago'.format(pretty_timedelta(now - member.joined_at), pretty_timedelta(now - member.created_at))
			e.add_field(name=po(member), value=value, inline=False)

		await ctx.send(embed=e)
Exemple #20
0
    async def mute_complete(self, record):
        conf = await self.config.get_entry(record.get('guild_id'))
        mute_role = conf.mute_role

        if mute_role is None:
            return

        guild_id = record.get('guild_id')
        user_id = record.get('user_id')
        mod_id = record.get('mod_id')
        duration = record.get('duration')
        reason = record.get('reason')

        guild = self.bot.get_guild(guild_id)
        if guild is None:
            return

        member = guild.get_member(user_id)
        if member is None:
            return

        mod = guild.get_member(mod_id)
        pretty_mod = '(ID: {0})'.format(
            str(mod_id)) if mod is None else po(mod)

        try:
            await member.remove_roles(
                mute_role,
                reason='Completed tempmute issued by {0}'.format(pretty_mod))
        except discord.HTTPException:
            return

        self.bot.dispatch('log',
                          guild,
                          member,
                          action='TEMPMUTE COMPLETED',
                          severity=Severity.RESOLVED,
                          responsible=pretty_mod,
                          duration=pretty_timedelta(duration),
                          reason=reason)
Exemple #21
0
    async def mute(self,
                   ctx,
                   member: discord.Member,
                   *,
                   reason: reason_converter = None):
        '''Mute a member. Requires Manage Roles perms.'''

        # TODO: should also handle people with manage roles perms
        if await ctx.is_mod(member):
            raise commands.CommandError('Can\'t mute this member.')

        conf = await self.config.get_entry(ctx.guild.id)
        mute_role = conf.mute_role

        if mute_role is None:
            raise commands.CommandError('Mute role not set or not found.')

        if mute_role in member.roles:
            raise commands.CommandError('Member already muted.')

        try:
            await member.add_roles(mute_role, reason=reason)
        except discord.HTTPException:
            raise commands.CommandError('Mute failed.')

        try:
            await ctx.send('{0} muted.'.format(str(member)))
        except discord.HTTPException:
            pass

        self.bot.dispatch('log',
                          ctx.guild,
                          member,
                          action='MUTE',
                          severity=Severity.LOW,
                          message=ctx.message,
                          responsible=po(ctx.author),
                          reason=reason)
Exemple #22
0
	async def on_reminder_complete(self, record):
		_id = record.get('id')
		channel_id = record.get('channel_id')
		user_id = record.get('user_id')
		made_on = record.get('made_on')
		message = record.get('message')

		channel = self.bot.get_channel(channel_id)
		user = self.bot.get_user(user_id)

		e = discord.Embed(
			title='Reminder:',
			description=message or DEFAULT_REMINDER_MESSAGE,
			timestamp=made_on
		)

		try:
			if channel is not None:
				await channel.send(content=f'<@{user_id}>', embed=e)
			elif user is not None:
				await user.send(embed=e)
		except discord.HTTPException as exc:
			log.info('Failed sending reminder #%s for %s - %s', _id, po(user), str(exc))
Exemple #23
0
 async def on_guild_unavailable(self, guild):
     log.info('Unavailable guild %s', po(guild))
Exemple #24
0
 async def on_guild_remove(self, guild):
     log.info('Left guild %s', po(guild))
     await self.update_dbl()
Exemple #25
0
 async def on_guild_join(self, guild):
     log.info('Join guild %s', po(guild))
     await self.update_dbl()
Exemple #26
0
 async def on_command(self, ctx):
     spl = ctx.message.content.split('\n')
     log.info('%s in %s: %s', po(ctx.author), po(ctx.guild),
              spl[0] + (' ...' if len(spl) > 1 else ''))
Exemple #27
0
    async def on_open_message(self, message):
        message: discord.Message = message
        author: discord.Member = message.author
        channel: discord.TextChannel = message.channel

        log.info(
            f'New message in open category: {message.author} - #{message.channel}'
        )

        # check whether author already has a claimed channel
        claimed_id = self.claimed_channel.get(author.id, None)
        if claimed_id is not None:  # and False:
            log.info(
                f'User has already claimed #{message.guild.get_channel(claimed_id)}'
            )
            await self.post_error(
                message,
                f'You have already claimed <#{claimed_id}>, please ask your question there.'
            )
            return

        now = datetime.utcnow()

        # check whether author has claimed another channel within the last MINIMUM_CLAIM_INTERVAL time
        last_claim_at = self.claimed_at.get(author.id, None)
        if last_claim_at is not None and last_claim_at > now - MINIMUM_CLAIM_INTERVAL:  # and False:
            log.info(
                f'User previously claimed a channel {pretty_timedelta(now - last_claim_at)} ago'
            )
            await self.post_error(
                message,
                f'Please wait at least {pretty_timedelta(MINIMUM_CLAIM_INTERVAL)} between each help channel claim.'
            )
            return

        if self.is_claimed(channel.id):
            log.info('Channel is already claimed (in the claimed category?)')
            return

        log.info('%s claiming %s', po(author), po(channel))

        # move channel
        try:
            opt = dict(position=self.active_info_channel.position + 1,
                       category=self.active_category,
                       sync_permissions=True,
                       topic=message.jump_url)

            if not self.has_postfix(channel):
                opt['name'] = channel.name + '-' + NEW_EMOJI

            await channel.edit(**opt)

        except discord.HTTPException as exc:
            log.warning('Failed moving %s to claimed category: %s',
                        po(channel), str(exc))
            return

        # set some metadata
        self.claimed_channel[author.id] = channel.id
        self.claimed_at[author.id] = now

        self._store_claims()

        # check whether we need to move any open channels

        try:
            channel = self.closed_category.text_channels[-1]
        except IndexError:
            log.warning('No more openable channels in closed pool!')
        else:
            log.info(f'Opening new channel after claim: {channel}')
            await self.open_channel(channel)
Exemple #28
0
 def stamp(self):
     return 'TIME: {}\nGUILD: {}\nCHANNEL: #{}\nAUTHOR: {}\nMESSAGE ID: {}'.format(
         pretty_datetime(self.message.created_at), po(self.guild),
         po(self.channel), po(self.author), str(self.message.id))
Exemple #29
0
    async def do_action(self, message, action, reason):
        '''Called when an event happens.'''

        member = message.author
        guild = message.guild

        conf = await self.config.get_entry(member.guild.id)
        ctx = await self.bot.get_context(message, cls=AceContext)

        # ignore if member is mod
        if await ctx.is_mod():
            self.bot.dispatch(
                'log',
                guild,
                member,
                action='IGNORED {0} (MEMBER IS MOD)'.format(action.name),
                severity=Severity.LOW,
                message=message,
                reason=reason,
            )

            return

        # otherwise, check against security actions and perform punishment
        try:
            if action is SecurityAction.MUTE:
                mute_role = conf.mute_role

                if mute_role is None:
                    raise ValueError('No mute role set.')

                await member.add_roles(mute_role, reason=reason)

            elif action is SecurityAction.KICK:
                await member.kick(reason=reason)

            elif action is SecurityAction.BAN:
                await member.ban(delete_message_days=0, reason=reason)

        except Exception as exc:
            # log error if something happened
            self.bot.dispatch('log',
                              guild,
                              member,
                              action='{0} FAILED'.format(action.name),
                              severity=Severity.HIGH,
                              message=message,
                              reason=reason,
                              error=str(exc))
            return

        # log successful security event
        self.bot.dispatch('log',
                          guild,
                          member,
                          action=action.name,
                          severity=Severity(action.value),
                          message=message,
                          reason=reason)

        try:
            await message.channel.send('{0} {1}: {2}'.format(
                po(member), SecurityVerb[action.name].value, reason))
        except discord.HTTPException:
            pass
Exemple #30
0
    async def purge(self, ctx, *, args: str = None):
        '''Advanced purge command. Do `help purge` for usage examples and argument list.

		Arguments are parsed as command line arguments.

		Examples:
		Delete all messages within the last 200 containing the word "spam": `purge --check 200 --contains "spam"`
		Delete all messages within the last 100 from two members: `purge --user @runie @dave`
		Delete maximum 6 messages within the last 400 starting with "ham": `purge --check 400 --max 6 --starts "ham"`

		List of arguments:
		```
		--check <int>
		Total amount of messages the bot will check for deletion.

		--max <int>
		Total amount of messages the bot will delete.

		--bot
		Only delete messages from bots.

		--user member [...]
		Only delete messages from these members.

		--after message_id
		Start deleting after this message id.

		--before message_id
		Delete, at most, up until this message id.

		--contains
		Delete messages containing this string(s).

		--starts <string>
		Delete messages starting with this string(s).

		--ends <string>
		Delete messages ending with this string(s).```'''

        parser = NoExitArgumentParser(prog='purge',
                                      add_help=False,
                                      allow_abbrev=False)

        parser.add_argument(
            '-c',
            '--check',
            type=int,
            metavar='message_count',
            help='Total amount of messages checked for deletion.')
        parser.add_argument(
            '-m',
            '--max',
            type=int,
            metavar='message_count',
            help='Total amount of messages the bot will delete.')
        parser.add_argument('--bot',
                            action='store_true',
                            help='Only delete messages from bots.')
        parser.add_argument('-u',
                            '--user',
                            nargs='+',
                            metavar='user',
                            help='Only delete messages from this member(s).')
        parser.add_argument('-a',
                            '--after',
                            type=int,
                            metavar='id',
                            help='Start deleting after this message id.')
        parser.add_argument('-b',
                            '--before',
                            type=int,
                            metavar='id',
                            help='Delete, at most, up until this message id.')
        parser.add_argument('--contains',
                            nargs='+',
                            metavar='text',
                            help='Delete messages containing this string(s).')
        parser.add_argument(
            '--starts',
            nargs='+',
            metavar='text',
            help='Delete messages starting with this string(s).')
        parser.add_argument('--ends',
                            nargs='+',
                            metavar='text',
                            help='Delete messages ending with this string(s).')

        if args is None:
            await ctx.send('```\n{0}\n```'.format(parser.format_help()))
            return

        try:
            args = parser.parse_args(shlex.split(args))
        except Exception as e:
            raise commands.CommandError(str(e).partition('error: ')[2])

        preds = [
            lambda m: m.id != ctx.message.id, lambda m: m.id != RULES_MSG_ID
        ]

        if args.user:
            converter = MaybeMemberConverter()
            members = []

            for id in args.user:
                try:
                    member = await converter.convert(ctx, id)
                    members.append(member)
                except commands.CommandError:
                    raise commands.CommandError(
                        'Unknown user: "******"'.format(id))

            # yes, if both objects were discord.Member I could do m.author in members,
            # but since member can be FakeUser I need to do an explicit id comparison
            preds.append(
                lambda m: any(m.author.id == member.id for member in members))

        if args.contains:
            preds.append(lambda m: any(
                (s in m.content) for s in args.contains))

        if args.bot:
            preds.append(lambda m: m.author.bot)

        if args.starts:
            preds.append(
                lambda m: any(m.content.startswith(s) for s in args.starts))

        if args.ends:
            preds.append(
                lambda m: any(m.content.endswith(s) for s in args.ends))

        count = args.max
        deleted = 0

        def predicate(message):
            nonlocal deleted

            if count is not None and deleted >= count:
                return False

            if all(pred(message) for pred in preds):
                deleted += 1
                return True

        # limit is 100 be default
        limit = 100
        after = None
        before = None

        # set to 512 if after flag is set
        if args.after:
            after = discord.Object(id=args.after)
            limit = PURGE_LIMIT

        if args.before:
            before = discord.Object(id=args.before)

        # if we actually want to manually specify it doe
        if args.check is not None:
            limit = max(0, min(PURGE_LIMIT, args.check))

        try:
            deleted_messages = await ctx.channel.purge(limit=limit,
                                                       check=predicate,
                                                       before=before,
                                                       after=after)
        except discord.HTTPException:
            raise commands.CommandError(
                'Error occurred when deleting messages.')

        deleted_count = len(deleted_messages)

        log.info('%s purged %s messages in %s', po(ctx.author), deleted_count,
                 po(ctx.guild))

        await ctx.send('{0} messages deleted.'.format(deleted_count),
                       delete_after=10)