--- /dev/null
+import asyncio
+import datetime
+import shlex
+
+from plugins import PluginCommand
+import bot
+import cat
+
+class Prune(object):
+ def description(self):
+ return 'Prunes inactive members.'
+
+ def valid_commands(self):
+ return ['prune']
+
+ async def handle_command(self, message, command, raw):
+ if command not in self.valid_commands():
+ return PluginCommand.ignored
+ await self.run_command(message, shlex.split(raw))
+ return PluginCommand.exclusive
+
+ async def handle_help(self, message, command, *args):
+ await self.help_command(message, command, *args)
+
+ async def run_command(self, message, args):
+ command = args[0]
+
+ if command == 'prune':
+ await self.handle_prune(message, args[1:])
+
+ async def help_command(self, message, command):
+ if command == 'prune':
+ lines = [
+ 'Identify and optionally kick inactive members.',
+ '```',
+ 'prune server SERVER [only] role ROLE days DAYS',
+ 'prune server SERVER [only] role ROLE days DAYS kick',
+ '```',
+ '',
+ 'Here are the parameters:',
+ '```server SERVER```',
+ 'Specify the server on which members will be searched.',
+ '',
+ '```role ROLE```',
+ 'Only find members who have the given role.',
+ '```only role ROLE```',
+ 'Omit members who have other roles as well as the given role. Include them if they have the role and no others.',
+ '',
+ '```days DAYS```',
+ 'Omit members who have been on the server for less than `DAYS` days.',
+ '',
+ '```kick```',
+ 'Kick matching members. Without `kick` they will just be listed'
+ ]
+ else:
+ lines = ['*shrugs*']
+ await bot.say_many(message.channel, lines)
+
+ async def handle_prune(self, message, args):
+ if args[0].lower() == 'help':
+ await self.help_command(message, 'prune')
+ return
+
+ if not bot.is_admin(message.author):
+ await cat.hiss(message.channel)
+ return
+
+ parsed = {
+ 'kick': False,
+ 'only': False
+ }
+ ok = False
+ i = 0
+ while i < len(args):
+ arg = args[i].lower()
+ if i > len(args) - 1:
+ break
+ try:
+ param = args[i + 1]
+ except IndexError:
+ param = None
+ log.info('{}: {}={}'.format(i, arg, param))
+ ok = False
+
+ if arg == 'days':
+ if param is not None:
+ try:
+ parsed['days'] = int(param)
+ if parsed['days'] < 1:
+ await bot.say(message.channel, 'days must be positive!')
+ return
+ ok = True
+ except:
+ await bot.say(message.channel, '{}?'.format(param))
+ return
+ elif arg == 'kick':
+ parsed['kick'] = True
+ i -= 1
+ elif arg == 'only':
+ ok = True
+ parsed['only'] = True
+ i -= 1
+ elif arg == 'role':
+ if param is not None:
+ if param in ['everyone', 'here']:
+ await bot.say(message.channel, 'role must not be *everyone* or *here*!')
+ return None
+ ok = True
+ parsed['role'] = await bot.parse_role(param, author = message.author)
+ elif arg == 'server':
+ if param is not None:
+ ok = True
+ parsed['server'] = bot.parse_server(param, author = message.author)
+
+ if ok:
+ i += 2
+ else:
+ break
+
+ if not ok:
+ log.info('Failed to parse prune. Got: {}'.format(parsed))
+ await bot.say(message.channel, '{}?'.format(arg))
+ return None
+
+ if 'days' not in parsed:
+ await bot.say(message.channel, 'How long?')
+ return None
+
+ if 'role' in parsed:
+ if parsed['role'] is None:
+ await bot.say(message.channel, 'Role?')
+ return None
+ if 'server' not in parsed:
+ if message.channel.is_private:
+ await bot.say(message.channel, 'Which server?')
+ return None
+ parsed['server'] = message.channel.server
+ elif parsed['server'] is None:
+ await bot.say(message.channel, 'Server?')
+ return None
+
+ if 'role' in parsed:
+ # Prune by role.
+ members = await self.find_members(**parsed)
+ if not len(members):
+ await cat.shrug(message.channel)
+ else:
+ await bot.say_many(message.channel, [member.name for member in members])
+ if parsed['kick']:
+ for member in members:
+ try:
+ await client.kick(member)
+ except:
+ log.exception('Failed to kick {}'.format(member.name))
+ await cat.yelp(message.channel)
+ break
+ else:
+ await bot.say(message.channel, 'Use `kick` option to kick the above members!')
+ else:
+ # Use server prune_members API call.
+ pruned = None
+ if parsed['kick']:
+ try:
+ log.info('Pruning members older than {} day{} from server {}.'.format(parsed['days'], 's' if parsed['days'] != 1 else '', parsed['server'].name))
+ pruned = await client.prune_members(parsed['server'], days = parsed['days'])
+ except:
+ log.exception('Failed to prune members from {}'.format(parsed['server'].name))
+ await cat.yelp(message.channel)
+ else:
+ try:
+ log.info('Estimating pruned members older than {} day{} from server {}.'.format(parsed['days'], 's' if parsed['days'] != 1 else '', parsed['server'].name))
+ pruned = await client.estimate_pruned_members(parsed['server'], days = parsed['days'])
+ except:
+ log.exception('Failed to estimate pruned members from {}'.format(parsed['server'].name))
+ await cat.yelp(message.channel)
+ if pruned is not None:
+ await bot.say(message.channel, str(pruned))
+ async def find_members(self, days = None, kick = False, only = False, role = None, server = None):
+ cutoff = datetime.datetime.utcnow() - datetime.timedelta(days)
+
+ members = []
+ for member in server.members:
+ if member.joined_at > cutoff:
+ log.debug('Member {} on server {} joined more recently than {}'.format(member.name, server.name, cutoff))
+ continue
+ if role is not None:
+ if role not in member.roles:
+ log.debug('Member {} on server {} does not have required role {}'.format(member.name, server.name, role.name))
+ continue
+ if only:
+ if len([other_role for other_role in member.roles if other_role not in [role, server.default_role]]):
+ log.debug('Member {} on server {} has roles other than {}'.format(member.name, server.name, role.name))
+ continue
+ members.append(member)
+ return members