Commands to manage greetings.
authorCMDR furrycat <elite@furrycat.net>
Thu, 27 Oct 2016 20:41:22 +0000 (21:41 +0100)
committerCMDR furrycat <elite@furrycat.net>
Thu, 27 Oct 2016 20:41:22 +0000 (21:41 +0100)
plugin/greetings/greetings.py

index 9859959..615a30d 100644 (file)
@@ -1,4 +1,6 @@
 import asyncio
+import re
+import shlex
 
 from db import DBConnection
 from plugins import PluginCommand
@@ -38,6 +40,26 @@ class Greetings(DBConnection):
   def on_member_join(self, member):
     result = yield from self.maybe_greet(member)
 
+  def valid_commands(self):
+    return ['greet', 'greeting', 'greetings']
+
+  @asyncio.coroutine
+  def handle_command(self, message, command, raw):
+    if command not in self.valid_commands():
+      return PluginCommand.ignored
+    yield from self.manage_greetings(message, raw)
+    return PluginCommand.exclusive
+
+  @asyncio.coroutine
+  def handle_help(self, message, command, *args):
+    yield from self.help_greetings(message, *args)
+
+  def get_all_greetings(self, client, servers = []):
+    yield from self.get_all_from_table(client, 'greetings', servers)
+
+  def get_greeting(self, client, id):
+    return self.get_from_table(client, 'greetings', id)
+
   def get_greetings(self, client):
     now = self.now()
     cursor = self.query("select id, server_id, channel_id, voice_id, sound, message, role_id, maximum from greetings where bot_id=? and enabled='true'", [client.user.id])
@@ -45,6 +67,443 @@ class Greetings(DBConnection):
       yield dict(row)
     self.close_db()
 
+  def create_greeting(self, client, **args):
+    return self.insert_into_table(client, 'greetings', **args)
+
+  def update_greeting(self, client, id, **args):
+    return self.update_table(client, 'greetings', id, **args)
+
+  def delete_greeting(self, client, id):
+    return self.delete_from_table(client, 'greetings', id)
+
+  @asyncio.coroutine
+  def can_manage_greetings(self, author, channel, command, **args):
+    # Anyone can list greetings.
+    if command == 'list':
+      log.debug('Anyone can list greetings.')
+      return True
+
+    if 'id' in args:
+      greeting = self.get_greeting(client, args['id'])
+    elif 'create' in args:
+      greeting = args['create']
+    else:
+      greeting = None
+
+    # Anyone on the server can show details of a greeting.
+    if command == 'show':
+      if greeting is not None:
+        server = client.get_server(greeting['server_id'])
+        if server is not None:
+          member = server.get_member(author.id)
+          if member in server.members:
+            log.debug('Member {} on server {} can show greeting {}.'.format(member.name, server.name, greeting['id']))
+            return greeting
+
+    if command in ['create', 'edit', 'delete', 'schedule']:
+      if greeting is not None:
+        server = client.get_server(greeting['server_id'])
+        member = server.get_member(author.id)
+        bot_member = server.get_member(client.user.id)
+        member_role = bot.highest_role(member.roles)
+        bot_role = bot.highest_role(bot_member.roles)
+        if member_role.position >= bot_role.position:
+          log.debug('Member {} with role {} on server {} can manage greetings.'.format(member.name, member_role.name, server.name))
+          return greeting
+
+    yield from cat.hiss(channel)
+    return False
+
+  @asyncio.coroutine
+  def list_greetings(self, message):
+    result = yield from self.can_manage_greetings(message.author, message.channel, 'list')
+    if not result:
+      return
+    servers = []
+    if message.channel.is_private:
+      for server in client.servers:
+        if message.author in server.members:
+          servers.append(server)
+    else:
+      servers = [message.channel.server]
+
+    results = []
+    for greeting in self.get_all_greetings(client, servers):
+      server = client.get_server(greeting['server_id'])
+      text = '**{}** in <#{}>'.format(greeting['id'], greeting['channel_id'])
+      if greeting['role_id']:
+        text += ' assign &{}'.format(greeting['role_id'])
+      if greeting['voice_id']:
+        if greeting['sound']:
+          text += ' sound {} in <#{}>'.format(greeting['sound'], greeting['voice_id'])
+      short = greeting['message'][:100]
+      if short != greeting['message']:
+        short += '...'
+      text += ': {}'.format(short.replace('\n', ' '))
+      results.append(text)
+
+    if len(results):
+      yield from bot.say_many(message.channel, results)
+    else:
+      yield from cat.shrug(message.channel)
+
+  @asyncio.coroutine
+  def show_greeting(self, message, id):
+    greeting = yield from self.can_manage_greetings(message.author, message.channel, 'show', id = id)
+    if not greeting:
+      return
+    lines = []
+    text = '**greeting {}'.format(id)
+    if not bot.parse_boolean(greeting['enabled']):
+      text += ' paused'
+    text += '**'
+    lines.append(text)
+
+    text = 'greeting create'
+    text += ' in <#{}>'.format(greeting['channel_id'])
+    if greeting['role_id']:
+      text += ' assign &{}'.format(greeting['role_id'])
+    if greeting['voice_id']:
+      text += ' voice <#{}>'.format(greeting['voice_id'])
+      text += ' sound {}'.format(greeting['sound'])
+    text += ':'
+    lines.append(text)
+    lines.append(greeting['message'])
+
+    print(lines)
+    yield from bot.say_many(message.channel, lines)
+
+  # Map a database key to a syntax parameter.
+  def greeting_key(self, k):
+    d = {
+      'channel_id': 'in',
+      'role_id': 'assign',
+      'voice_id': 'voice'
+    }
+    if k in d:
+      return d[k]
+    else:
+      return k
+
+  @asyncio.coroutine
+  def parse_greeting(self, message, raw, editing = False):
+    parts = raw.split(':')
+    params = parts[0]
+    if len(parts) > 1:
+      text = ':'.join(parts[1:]).strip()
+    else:
+      text = None
+
+    parsed = {}
+    if editing:
+      command = 'edit'
+    else:
+      command = 'create'
+    if text:
+      parsed['message'] = text
+
+    log.debug('announce {} params {} text {}'.format(command, params, text))
+    args = shlex.split(params)
+    log.info(args)
+    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 == command:
+        # UUID missing 'id'.
+        if editing and re.match(r'[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$', param):
+          parsed['id'] = param
+        else:
+          i -= 1
+        ok = True
+      elif arg == 'id':
+        if editing:
+          parsed['id'] = param
+          ok = True
+        else:
+          break
+      elif arg in ['in', 'voice']:
+        k = 'channel_id' if arg == 'in' else 'voice_id'
+        if editing and arg == 'voice' and param == 'none':
+          parsed[k] = None
+          ok = True
+        elif param:
+          m = re.match(r'<#(\d+)>', param)
+          if m is None:
+            m = re.match(r'#(\d+)', param)
+          if m is not None:
+            parsed[k] = m.group(1)
+            ok = True
+          else:
+            m = re.match(r'#(.+)', param)
+            if m is None:
+              break
+            # Look for a channel with that name.
+            for channel in client.get_all_channels():
+              if channel.name == m.group(1):
+                parsed[k] = channel.id
+                ok = True
+                break
+      elif arg == 'sound':
+        k = arg
+        if editing and param == 'none':
+          parsed[k] = None
+          ok = True
+        else:
+          parsed[k] = param
+          ok = True
+      elif arg == 'assign':
+        k = 'role_id'
+        if editing and param == 'none':
+          parsed[k] = None
+          ok = True
+        elif param:
+          m = re.match(r'<@&(\d+)>', param)
+          if m is None:
+            m = re.match(r'@?&(\d+)', param)
+          if m is not None:
+            parsed[k] = m.group(1)
+            ok = True
+      else:
+        yield from bot.say(message.channel, 'What is {}?'.format(arg))
+        return
+
+      if ok:
+        i += 2
+      else:
+        break
+
+    if not ok:
+      log.info('Failed to parse greeting.  Got: {}'.format(parsed))
+      yield from bot.say(message.channel, '{}?'.format(arg))
+      return None
+
+    if 'channel_id' in parsed:
+      channel = client.get_channel(parsed['channel_id'])
+      if not channel:
+        yield from bot.say(message.channel, 'Invalid channel!')
+        return None
+      server = channel.server
+      parsed['server_id'] = server.id
+
+    if 'message' not in parsed:
+      yield from bot.say(message.channel, 'Missing message!')
+      return None
+
+    if editing:
+      if 'id' not in parsed:
+        yield from bot.say(message.channel, 'Missing ID!')
+        return None
+    else:
+      if 'channel_id' not in parsed:
+        yield from bot.say(message.channel, 'Missing channel!')
+        return None
+
+    return parsed
+
+  @asyncio.coroutine
+  def create_new_greeting(self, message, raw):
+    # greeting create [params]: <text>
+    create = yield from self.parse_greeting(message, raw)
+    if create is None:
+      return
+
+    greeting = yield from self.can_manage_greetings(message.author, message.channel, 'create', create = create)
+    if not greeting:
+      return
+
+    id = self.create_greeting(client, **create)
+    if id:
+      create['id'] = id
+      log.info('Created greeting: {}'.format(create))
+      yield from bot.say(message.channel, id)
+      yield from cat.purr(None, voice_channel_for_user(message.author), join = False)
+    else:
+      log.info('Failed to create greeting: {}'.format(create))
+      yield from cat.yelp(message.channel)
+
+  @asyncio.coroutine
+  def edit_existing_greeting(self, message, raw):
+    update = yield from self.parse_greeting(message, raw, editing = True)
+    if update is None:
+      return
+
+    id = update['id']
+    del(update['id'])
+    greeting = yield from self.can_manage_greetings(message.author, message.channel, 'edit', id = id)
+    if not greeting:
+      return
+
+    if not len(update.keys()):
+      yield from bot.say(message.channel, '?')
+      return
+
+    if self.update_greeting(client, id, **update):
+      log.info('Edited greeting {}: {}'.format(id, update))
+      yield from bot.say(message.channel, ', '.join([self.greeting_key(k) for k in update]))
+      yield from cat.purr(None, voice_channel_for_user(message.author), join = False)
+    else:
+      log.info('Failed to edit greeting {}: {}'.format(id, update))
+      yield from cat.yelp(message.channel)
+
+  @asyncio.coroutine
+  def delete_existing_greeting(self, message, id):
+    greeting = yield from self.can_manage_greetings(message.author, message.channel, 'delete', id = id)
+    if not greeting:
+      return
+    if bot.get('dryrun'):
+      log.info('Not deleting greeting {}'.format(id))
+    else:
+      log.info('Deleting greeting {}'.format(id))
+      if self.delete_greeting(client, id):
+        yield from cat.purr(message.channel)
+      else:
+        yield from cat.yelp(message.channel)
+
+  @asyncio.coroutine
+  def schedule_greeting(self, message, id, **args):
+    greeting = yield from self.can_manage_greetings(message.author, message.channel, 'schedule', id = id)
+    if not greeting:
+      return
+
+    update = {}
+
+    # Pause greeting.
+    if 'pause' in args:
+      if args['pause']:
+        update['enabled'] = 'false'
+      else:
+        update['enabled'] = 'true'
+
+    if bot.get('dryrun'):
+      log.info('Not updating greeting {}: {}'.format(id, update))
+    else:
+      log.info('Updating greeting {}: {}'.format(id, update))
+      if self.update_greeting(client, id, **update):
+        yield from cat.purr(message.channel)
+      else:
+        yield from cat.yelp(message.channel)
+
+  @asyncio.coroutine
+  def manage_greetings(self, message, raw):
+    m = re.match(r'greet(?:ings?)?\s+(.+)', raw, re.IGNORECASE | re.DOTALL)
+    if m is None:
+      yield from self.list_greetings(message)
+      return
+    text = m.group(1)
+    args = shlex.split(text)
+    command = args[0].lower()
+    if len(args) == 1:
+      if command == 'list':
+        yield from self.list_greetings(message)
+        return
+      elif command == 'help':
+        yield from self.help_greetings(message)
+        return
+      else:
+        yield from cat.yelp(message.channel)
+        return
+
+    if command == 'show':
+      yield from self.show_greeting(message, args[1])
+    elif command == 'help':
+      yield from self.help_greetings(message, args[1])
+    elif command == 'delete':
+      yield from self.delete_existing_greeting(message, args[1])
+    elif command == 'pause':
+      yield from self.schedule_greeting(message, args[1], pause = True)
+    elif command == 'resume':
+      yield from self.schedule_greeting(message, args[1], pause = False)
+    elif command == 'create':
+      yield from self.create_new_greeting(message, text)
+    elif command == 'edit':
+      yield from self.edit_existing_greeting(message, text)
+
+  @asyncio.coroutine
+  def help_greetings(self, message, command = None):
+    if command is None:
+      lines = [
+        'Commands to manage greetings are:',
+        '```',
+        'create',
+        'delete',
+        'edit',
+        'list',
+        'pause',
+        'show',
+        'resume',
+        '```',
+        'Send `greeting help COMMAND` for help on a specific command.'
+      ]
+    elif command == 'create':
+      lines = [
+        'Create a new greeting.', 
+        '```greeting create OPTIONS: MESSAGE```',
+        'Use the `OPTIONS` to define when and where to send the greeting.',
+        'You need to include the **:** after the OPTIONS.  Anything you include after it will be part of the MESSAGE!',
+        '',
+        'Here are the OPTIONS you can use:',
+        '',
+        '```in CHANNEL```',
+        'Post the greeting to the specified #channel.',
+        '',
+        '```assign ROLE```',
+        'Assign the `ROLE` to new members with no other roles.',
+        '',
+        '```voice CHANNEL```',
+        'Play a sound to the specified voice #CHANNEL.',
+        '',
+        '```sound FILE```',
+        "File to play.!",
+        '',
+      ]
+    elif command == 'delete':
+      lines = [
+        'Delete the greeting with the given ID.',
+        '```greeting delete ID```',
+      ]
+    elif command == 'edit':
+      lines = [
+        'Edit a greeting.',
+        '```greeting edit ID OPTIONS```',
+        'Change one or more `OPTIONS` for the greeting with the given ID.',
+        'See the help for `greeting create` for details of the OPTIONS you can set.',
+      ]
+    elif command == 'help':
+      lines = ['grr!']
+    elif command == 'list':
+      lines = [
+        'List all greetings, one `ID` per line.  You can use the `ID` in other commands.',
+        "When listing IDs to a public channel I won't show IDs for greetings that are for channels on another server.  Send `greeting list` to me in a private message to see them.",
+      ]
+    elif command == 'show':
+      lines = [
+        'Show the greeting with the given ID.',
+        '```greeting show ID```',
+        'I will tell you the details in a format which you could copy and paste to create a new greeting.',
+      ]
+    elif command in ['pause', 'resume']:
+      lines = [
+        'Schedule the greeting with the given ID.',
+        '```',
+        'greeting pause ID',
+        'greeting resume ID',
+        '```',
+        'Use `pause` and `resume` to put a greeting on hold temporarily.'
+      ]
+    else:
+      lines = ['*shrugs*']
+    yield from bot.say_many(message.channel, lines)
+
   def greeting_for_member(self, member, channel, candidate):
     if candidate is None:
       greetings = [dict(greeting) for greeting in self.get_greetings(client)]