Users can create announcements.
authorCMDR furrycat <elite@furrycat.net>
Thu, 6 Oct 2016 11:44:49 +0000 (12:44 +0100)
committerCMDR furrycat <elite@furrycat.net>
Thu, 6 Oct 2016 11:44:49 +0000 (12:44 +0100)
bot.py
db.py

diff --git a/bot.py b/bot.py
index 232a03a..d681463 100755 (executable)
--- a/bot.py
+++ b/bot.py
@@ -722,34 +722,54 @@ def can_manage_announcements(member, channel, command, **args):
 
   if 'id' in args:
     announcement = db.get_announcement(client, args['id'])
+  elif 'create' in args:
+    announcement = args['create']
   else:
     announcement = None
 
   # Anyone on the server can show details of an announcement.
   if command == 'show':
     if announcement is not None:
-      server = client.get_server(announcement['server_id'])
-      if server is not None:
-        if member in server.members:
-          log.debug('Member {} on server {} can show announcement {}.'.format(member.name, server.name, announcement['id']))
-          return announcement
+      if announcement['member_id'] == member.id:
+        log.debug('Anyone can show own announcements.')
+        return announcement
+      if announcement['channel_id'] != 'private':
+        server = client.get_server(announcement['server_id'])
+        if server is not None:
+          if member in server.members:
+            log.debug('Member {} on server {} can show announcement {}.'.format(member.name, server.name, announcement['id']))
+            return announcement
 
   if command in ['delete', 'schedule']:
     if announcement is not None:
-      server = client.get_server(announcement['server_id'])
-      announcer = server.get_member(announcement['member_id'])
-      if member.id == announcer.id:
-        log.debug('Member {} on server {} can edit own announcements.'.format(member.name, server.name))
-        return announcement
-      announcer_role = highest_role(announcer.roles)
-      member_role = highest_role(member.roles)
-      if member_role > announcer_role:
-        log.debug('Member {} on server {} can edit announcements from lower role.'.format(member.name, server.name))
+      if announcement['member_id'] == member.id:
+        log.debug('Anyone can manage own announcements.')
         return announcement
-      elif member_role == announcer_role:
-        if member_role == highest_role(server.roles):
-          log.debug('Member {} with role {} on server {} can edit announcements.'.format(member.name, member_role.name, server.name))
+      if announcement['channel_id'] != 'private':
+        server = client.get_server(announcement['server_id'])
+        announcer = server.get_member(announcement['member_id'])
+        announcer_role = highest_role(announcer.roles)
+        member_role = highest_role(member.roles)
+        if member_role > announcer_role:
+          log.debug('Member {} on server {} can edit announcements from lower role.'.format(member.name, server.name))
           return announcement
+        elif member_role == announcer_role:
+          if member_role == highest_role(server.roles):
+            log.debug('Member {} with role {} on server {} can edit announcements.'.format(member.name, member_role.name, server.name))
+            return announcement
+
+  if command == 'create':
+    if announcement['channel_id'] == 'private':
+      log.debug('Anyone can schedule a private announcement.')
+      return announcement
+    server = client.get_server(announcement['server_id'])
+    member = server.get_member(announcement['member_id'])
+    bot = server.get_member(client.user.id)
+    member_role = highest_role(member.roles)
+    bot_role = highest_role(bot.roles)
+    if member_role > bot_role:
+      log.debug('Member {} with role {} on server {} can create announcements.'.format(member.name, member_role.name, server.name))
+      return announcement
 
   yield from say(channel, 'hiss!')
   return False
@@ -761,6 +781,7 @@ def list_announcements(message):
     return
   servers = []
   if message.channel.is_private:
+    servers.append('private')
     for server in client.servers:
       if message.author in server.members:
         servers.append(server)
@@ -786,8 +807,15 @@ def show_announcement(message, id):
     return
   lines = []
 
-  server = client.get_server(announcement['server_id'])
-  member = server.get_member(announcement['member_id'])
+  if announcement['channel_id'] == 'private':
+    for server in client.servers:
+      try:
+        member = server.get_member(announcement['member_id'])
+      except:
+        pass
+  else:
+    server = client.get_server(announcement['server_id'])
+    member = server.get_member(announcement['member_id'])
   text = '**announcement {} by {}'.format(id, member.name)
   now = int(time.time())
   if announcement['last_spoke']:
@@ -829,7 +857,10 @@ def show_announcement(message, id):
       else:
         text += ' tell <@{}>'.format(mention)
   if announcement['channel_id'] != announcement['voice_id']:
-    text += ' in <#{}>'.format(announcement['channel_id'])
+    if announcement['channel_id'] == 'private':
+      text += ' in private'
+    else:
+      text += ' in <#{}>'.format(announcement['channel_id'])
   if announcement['voice_id']:
     text += ' voice <#{}>'.format(announcement['voice_id'])
     if announcement['sound']:
@@ -846,6 +877,148 @@ def show_announcement(message, id):
   yield from say(message.channel, '\n'.join(lines))
 
 @asyncio.coroutine
+def create_announcement(message, raw):
+  # announcement create [params]: <text>
+  parts = raw.split(':')
+  params = parts[0]
+  if len(parts) > 1:
+    text = ':'.join(parts[1:]).strip()
+  else:
+    text = None
+  log.debug('announce create params {} text {}'.format(params, text))
+
+  create = { 'member_id': message.author.id }
+  if text:
+    create['message'] = 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
+    param = args[i + 1]
+    log.info('{}: {}={}'.format(i, arg, param))
+    ok = False
+
+    if arg == 'create':
+      i -= 1
+      ok = True
+    elif arg == 'once':
+      create['interval'] = 0
+      i -= 1
+      ok = True
+    elif arg in ['from', 'to']:
+      k = 'start_date' if arg == 'from' else 'end_date'
+      if re.match(r'\d\d\d\d-\d\d-\d\d[T\s]\d\d:\d\d:\d\dZ?', param):
+        create[k] = param
+        ok = True
+      else:
+        break
+    elif arg == 'every':
+      m = re.match(r'(\d+)s?', param)
+      if m is not None:
+        create['interval'] = m.group(1)
+        ok = True
+      else:
+        break
+    elif arg == 'tell':
+      if param == 'me':
+        create['mention'] = message.author.id
+        ok = True
+      else:
+        m = re.match(r'<@((?:&?|#)\d+)>', param)
+        if m is None:
+          m = re.match('r@((?:&?|#)\d+)', param)
+          if m is None:
+            m = re.match('r@(everyone|here)', param)
+        if m is not None:
+          create['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 arg != 'in':
+          break
+        if message.channel.is_private:
+          create[k] = 'private'
+          ok = True
+        else:
+          create[k] = message.channel.id
+          ok = True
+      elif param == 'private':
+        if arg != 'in':
+          break
+        create[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)
+          ok = True
+        else:
+          break
+    elif arg == 'sound':
+      create['sound'] = param
+      ok = True
+    elif arg == 'probability':
+      create['probability'] = param
+      ok = True
+    else:
+      yield from say(message.channel, 'What is {}?'.format(arg))
+      return
+
+    if ok:
+      i += 2
+    else:
+      break
+
+  if not ok:
+    log.info('Failed to parse announcement.  Got: {}'.format(create))
+    yield from say(message.channel, '{}?'.format(arg))
+    return
+
+  if 'channel_id' not in create:
+    yield from say(message.channel, 'Missing channel!')
+    return
+
+  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
+  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
+
+  announce = yield from can_manage_announcements(message.author, message.channel, 'create', create = create)
+  if not announce:
+    return
+
+  log.info('Creating announcement: {}'.format(create))
+  id = db.create_announcement(client, **create)
+  if id:
+    yield from say(message.channel, id)
+    yield from play_sound(voice_channel_for_channel(message.channel), 'purr.wav', join = False)
+  else:
+    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:
@@ -895,7 +1068,8 @@ def manage_announcements(message, command, raw):
   if m is None:
     yield from list_announcements(message)
     return
-  args = shlex.split(m.group(1))
+  text = m.group(1)
+  args = shlex.split(text)
   if len(args) == 1:
     if args[0] == 'list':
       yield from list_announcements(message)
@@ -914,12 +1088,27 @@ def manage_announcements(message, command, raw):
     yield from schedule_announcement(message, args[1], pause = True)
   elif args[0] == 'resume':
     yield from schedule_announcement(message, args[1], pause = False)
+  elif args[0] == 'create':
+    yield from create_announcement(message, text)
 
 @asyncio.coroutine
 def announce(announcement):
-  channel = client.get_channel(str(announcement['channel_id']))
+  channel = None
+  channel_id = str(announcement['channel_id'])
+  if channel_id == 'private':
+    for server in client.servers:
+      try:
+        member = server.get_member(announcement['member_id'])
+        channel = yield from client.start_private_message(member)
+        break
+      except:
+        pass
+  else:
+    channel = client.get_channel(channel_id)
   if channel is None:
     log.warning("Can't get channel for announcement {}".format(announcement['message']))
+    # Set last_spoke so we don't spam.
+    db.set_announcement(client, announcement['id'], announcement['digest'] if 'digest' in announcement else None)
     return
 
   voice_only = announcement['voice_id'] == announcement['channel_id']
diff --git a/db.py b/db.py
index f87e0d4..1217bdf 100644 (file)
--- a/db.py
+++ b/db.py
@@ -80,7 +80,10 @@ class DBConnection(object):
     if len(servers):
       sql += ' and server_id in ({})'.format(', '.join(['?'] * len(servers)))
       for server in servers:
-        params.append(server.id)
+        if type(server) == discord.server.Server:
+          params.append(server.id)
+        else:
+          params.append(server)
     cursor = self.query(sql, params)
     for row in cursor.fetchall():
       yield dict(row)
@@ -98,7 +101,7 @@ class DBConnection(object):
 
   def get_announcements(self, client):
     now = self.now()
-    cursor = self.query('select id, server_id, channel_id, voice_id, sound, mention, start_date, end_date, interval, probability, last_spoke, message, digest from announcements where (start_date is null or start_date <= ?) and (end_date is null or end_date > ? or end_date=start_date) and (last_spoke is null or last_spoke < ? - interval) and bot_id=?', [now, now, now, client.user.id])
+    cursor = self.query('select id, server_id, member_id, channel_id, voice_id, sound, mention, start_date, end_date, interval, probability, last_spoke, message, digest from announcements where (start_date is null or start_date <= ?) and (end_date is null or end_date > ? or end_date=start_date) and (last_spoke is null or last_spoke < ? - interval) and (last_spoke is null or interval > 0) and bot_id=?', [now, now, now, client.user.id])
     for row in cursor.fetchall():
       yield dict(row)
     self.close_db()
@@ -109,6 +112,26 @@ class DBConnection(object):
     self.dbh.commit()
     self.close_db()
 
+  def create_announcement(self, client, **args):
+    id = self.uuid()
+
+    values = []
+    params = [id, client.user.id]
+    for k, v in args.items():
+      values.append(k)
+      params.append(v)
+    sql = 'insert into announcements (id, bot_id, {}) values (?, ?, {})'.format(', '.join(values), ', '.join(['?'] * len(values)))
+    cursor = self.query(sql, params)
+    if cursor.rowcount:
+      self.dbh.commit()
+      ret = id
+    else:
+      log.error('Failed to create announcement {}: {}'.format(id, args))
+      self.dbh.rollback()
+      ret = None
+    self.close_db()
+    return ret
+
   def update_announcement(self, client, id, **args):
     values = []
     params = []