Allow editing announcements.
authorCMDR furrycat <elite@furrycat.net>
Wed, 12 Oct 2016 21:24:16 +0000 (22:24 +0100)
committerCMDR furrycat <elite@furrycat.net>
Wed, 12 Oct 2016 21:24:16 +0000 (22:24 +0100)
bot.py

diff --git a/bot.py b/bot.py
index 35f454d..a69345c 100755 (executable)
--- a/bot.py
+++ b/bot.py
@@ -851,7 +851,7 @@ def can_manage_announcements(author, channel, command, **args):
             log.debug('Member {} on server {} can show announcement {}.'.format(member.name, server.name, announcement['id']))
             return announcement
 
-  if command in ['delete', 'schedule']:
+  if command in ['edit', 'delete', 'schedule']:
     if announcement is not None:
       if announcement['member_id'] == author.id:
         log.debug('Anyone can manage own announcements.')
@@ -895,6 +895,7 @@ def help_announcements(message, command = None):
       'asap',
       'create',
       'delete',
+      'edit',
       'list',
       'pause',
       'show',
@@ -950,6 +951,15 @@ def help_announcements(message, command = None):
       '```announcement delete ID```',
       'You can only delete your own announcements or public announcements created by someone in a lower role.'
     ]
+  elif command == 'edit':
+    lines = [
+      'Edit an announcement.',
+      '```announcement edit ID OPTIONS: MESSAGE```',
+      'Change one or more `OPTIONS` or the `MESSAGE` for the announcement with the given ID.',
+      'See the help for `announce create` for details of the OPTIONS you can set.',
+      'Some options can be set to *none* to delete that option.',
+      'For instance `to none` means that the announcement will no longer have an end date.'
+    ]
   elif command == 'help':
     lines = ['grr!']
   elif command == 'list':
@@ -1106,9 +1116,23 @@ def show_announcement(message, id):
 
   yield from say(message.channel, '\n'.join(lines))
 
+# Map a database key to a syntax parameter.
+def announcement_key(k):
+  d = {
+    'channel_id': 'in',
+    'end_date': 'to',
+    'interval': 'every',
+    'mention': 'tell',
+    'start_date': 'from',
+    'voice_id': 'voice'
+  }
+  if k in d:
+    return d[k]
+  else:
+    return k
+
 @asyncio.coroutine
-def create_announcement(message, raw):
-  # announcement create [params]: <text>
+def parse_announcement(message, raw, editing = False):
   # We want to split by colon but don't want to count any which are part of dates or URLs.
   m = re.match(r'(?:[^:]*\s+(?:(?:at|from|to)\s+"?\d\d\d\d-?\d\d-?\d\d(?:T|\s+)\d\d:?\d\d:?\d\dZ?"?|sound\s+https?:\S+))+', raw, re.IGNORECASE)
   if m is not None:
@@ -1121,12 +1145,17 @@ def create_announcement(message, raw):
     text = ':'.join(parts[1:]).strip()
   else:
     text = None
-  log.debug('announce create params {} text {}'.format(params, text))
 
-  create = { 'member_id': message.author.id }
+  parsed = {}
+  if editing:
+    command = 'edit'
+  else:
+    command = 'create'
+    parsed['member_id'] = message.author.id
   if text:
-    create['message'] = text
+    parsed['message'] = text
 
+  log.debug('announce {} params {} text {}'.format(command, params, text))
   args = shlex.split(params)
   log.info(args)
   ok = False
@@ -1142,33 +1171,50 @@ def create_announcement(message, raw):
     log.info('{}: {}={}'.format(i, arg, param))
     ok = False
 
-    if arg == 'create':
-      i -= 1
+    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 == 'once':
-      create['interval'] = 0
+      parsed['interval'] = 0
       i -= 1
       ok = True
     elif arg in ['at', 'from', 'to']:
       k = 'end_date' if arg == 'to' else 'start_date'
-      m = re.match(r'(\d\d\d\d)-?(\d\d)-?(\d\d)(?:T|\s+)(\d\d):?(\d\d):?(\d\d)Z?', param)
-      if m is not None:
-        create[k] = int(datetime.datetime(*([int(n) for n in m.groups()] + [0, pytz.UTC])).timestamp())
+      if editing and param == 'none':
+        parsed[k] = None
         ok = True
       else:
-        break
+        m = re.match(r'(\d\d\d\d)-?(\d\d)-?(\d\d)(?:T|\s+)(\d\d):?(\d\d):?(\d\d)Z?', param)
+        if m is not None:
+          parsed[k] = int(datetime.datetime(*([int(n) for n in m.groups()] + [0, pytz.UTC])).timestamp())
+          ok = True
+        else:
+          break
     elif arg == 'every':
       seconds = parse_seconds(param)
       if seconds is not None:
-        create['interval'] = seconds
+        parsed['interval'] = seconds
         ok = True
       else:
         break
     elif arg == 'tell':
-      if param == 'me':
-        create['mention'] = message.author.id
-        if 'channel_id' not in create:
-          create['channel_id'] = 'private'
+      if editing and param == 'none':
+        parsed['mention'] = None
+        ok = True
+      elif param == 'me':
+        parsed['mention'] = message.author.id
+        if 'channel_id' not in parsed:
+          parsed['channel_id'] = 'private'
         ok = True
       else:
         m = re.match(r'<@((?:&?|#)\d+)>', param)
@@ -1177,32 +1223,35 @@ def create_announcement(message, raw):
           if m is None:
             m = re.match('r@(everyone|here)', param)
         if m is not None:
-          create['mention'] = m.group(1)
+          parsed['mention'] = m.group(1)
           ok = True
         else:
           break
     elif arg in ['in', 'voice']:
       k = 'channel_id' if arg == 'in' else 'voice_id'
-      if param == 'here':
+      if editing and param == 'none':
+        parsed['mention'] = None
+        ok = True
+      elif param == 'here':
         if arg != 'in':
           break
         if message.channel.is_private:
-          create[k] = 'private'
+          parsed[k] = 'private'
           ok = True
         else:
-          create[k] = message.channel.id
+          parsed[k] = message.channel.id
           ok = True
       elif param == 'private':
         if arg != 'in':
           break
-        create[k] = 'private'
+        parsed[k] = 'private'
         ok = True
       else:
         m = re.match(r'<#(\d+)>', param)
         if m is None:
           m = re.match(r'#(\d+)', param)
         if m is not None:
-          create[k] = m.group(1)
+          parsed[k] = m.group(1)
           ok = True
         else:
           m = re.match(r'#(.+)', param)
@@ -1211,14 +1260,18 @@ def create_announcement(message, raw):
           # Look for a channel with that name.
           for channel in client.get_all_channels():
             if channel.name == m.group(1):
-              create[k] = channel.id
+              parsed[k] = channel.id
               ok = True
               break
     elif arg == 'sound':
-      create['sound'] = param
-      ok = True
+      if editing and param == 'none':
+        parsed['mention'] = None
+        ok = True
+      else:
+        parsed['sound'] = param
+        ok = True
     elif arg == 'probability':
-      create['probability'] = param
+      parsed['probability'] = param
       ok = True
     else:
       yield from say(message.channel, 'What is {}?'.format(arg))
@@ -1230,39 +1283,54 @@ def create_announcement(message, raw):
       break
 
   if not ok:
-    log.info('Failed to parse announcement.  Got: {}'.format(create))
+    log.info('Failed to parse announcement.  Got: {}'.format(parsed))
     yield from say(message.channel, '{}?'.format(arg))
-    return
+    return None
 
-  if 'channel_id' not in create:
-    if 'voice_id' in create:
-      create['channel_id'] = create['voice_id']
+  if 'channel_id' in parsed:
+    if parsed['channel_id'] == 'private':
+      parsed['server_id'] = 'private'
+      if 'mention' in parsed:
+        if parsed['mention'] != message.author.id:
+          yield from say(message.channel, "Can't mention someone else in private message!")
+          return None
+      parsed['mention'] = None
+      if 'voice_id' in parsed or 'sound' in parsed:
+        yield from say(message.channel, 'No sounds for private messages!')
+        return None
     else:
-      yield from say(message.channel, 'Missing channel!')
-      return
+      channel = client.get_channel(parsed['channel_id'])
+      if not channel:
+        yield from say(message.channel, 'Invalid channel!')
+        return None
+      server = channel.server
+      parsed['server_id'] = server.id
+
+  if 'start_date' in parsed and 'end_date' in parsed:
+    if parsed['start_date'] > parsed['end_date']:
+      yield from say(message.channel, "Start date must not be before end date!")
+      return None
 
-  if create['channel_id'] == 'private':
-    create['server_id'] = 'private'
-    if 'mention' in create:
-      if create['mention'] != message.author.id:
-        yield from say(message.channel, "Can't mention someone else in private message!")
-        return
-    create['mention'] = None
-    if 'voice_id' in create or 'sound' in create:
-      yield from say(message.channel, 'No sounds for private messages!')
-      return
+  if editing:
+    if 'id' not in parsed:
+      yield from say(message.channel, 'Missing ID!')
+      return None
   else:
-    channel = client.get_channel(create['channel_id'])
-    if not channel:
-      yield from say(message.channel, 'Invalid channel!')
-      return
-    server = channel.server
-    create['server_id'] = server.id
+    if 'channel_id' not in parsed:
+      if 'voice_id' in parsed:
+        parsed['channel_id'] = parsed['voice_id']
+      else:
+        yield from say(message.channel, 'Missing channel!')
+        return None
 
-  if 'start_date' in create and 'end_date' in create:
-    if create['start_date'] > create['end_date']:
-      yield from say(message.channel, "Start date must not be before end date!")
-      return
+  return parsed
+
+@asyncio.coroutine
+def create_announcement(message, raw):
+  # announcement create [params]: <text>
+  create = yield from parse_announcement(message, raw)
+  if create is None:
+    return
 
   announce = yield from can_manage_announcements(message.author, message.channel, 'create', create = create)
   if not announce:
@@ -1279,6 +1347,27 @@ def create_announcement(message, raw):
     yield from say(message.channel, 'yelp!')
 
 @asyncio.coroutine
+def edit_announcement(message, raw):
+  update = yield from parse_announcement(message, raw, True)
+  if update is None:
+    return
+
+  id = update['id']
+  del(update['id'])
+  announce = yield from can_manage_announcements(message.author, message.channel, 'edit', id = id)
+  if not announce:
+    return
+
+  if db.update_announcement(client, id, **update):
+    log.info('Edited announcement {}: {}'.format(id, update))
+    yield from say(message.channel, ', '.join([announcement_key(k) for k in update]))
+    yield from play_sound(voice_channel_for_channel(message.channel), 'purr.wav', join = False)
+  else:
+    log.info('Failed to edit announcement {}: {}'.format(id, update))
+    yield from say(message.channel, 'yelp!')
+
+
+@asyncio.coroutine
 def delete_announcement(message, id):
   announcement = yield from can_manage_announcements(message.author, message.channel, 'delete', id = id)
   if not announcement:
@@ -1356,6 +1445,8 @@ def manage_announcements(message, command, raw):
     yield from schedule_announcement(message, args[1], pause = False)
   elif command == 'create':
     yield from create_announcement(message, text)
+  elif command == 'edit':
+    yield from edit_announcement(message, text)
 
 @asyncio.coroutine
 def announce(announcement):