EDDB helper plugin.
authorCMDR furrycat <elite@furrycat.net>
Sun, 6 Nov 2016 17:58:00 +0000 (17:58 +0000)
committerCMDR furrycat <elite@furrycat.net>
Sun, 6 Nov 2016 17:58:00 +0000 (17:58 +0000)
plugin/shopping/shopping.py [new file with mode: 0644]

diff --git a/plugin/shopping/shopping.py b/plugin/shopping/shopping.py
new file mode 100644 (file)
index 0000000..cfde0e4
--- /dev/null
@@ -0,0 +1,325 @@
+import asyncio
+import json
+import logging
+import math
+import os
+import re
+import shlex
+
+from plugins import PluginCommand
+import bot
+import cat
+
+class Shopping(object):
+  def __init__(self):
+    self.path = os.path.dirname(__file__)[(len(os.getcwd()) + 1):]
+    self.COMMODITIES = os.sep.join([self.path, 'commodities.json'])
+    self.COMMODITY_KEYS = ['id', 'average_price', 'is_rare', 'name']
+    self.MODULES = os.sep.join([self.path, 'modules.json'])
+    self.MODULE_KEYS = ['class', 'id', 'group.name', 'name', 'price', 'rating', 'ship', 'weapon_mode']
+    self.SHIPS = os.sep.join([self.path, 'ships.json'])
+    self.SHIP_KEYS = ['aliases', 'id', 'name']
+
+  def valid_commands(self):
+    return ['shopping']
+
+  def description(self):
+    return 'Advise where to buy ships, modules or commodities.'
+
+  @asyncio.coroutine
+  def handle_command(self, message, command, raw):
+    if command not in self.valid_commands():
+      return PluginCommand.ignored
+    yield from self.handle_shopping(message, shlex.split(raw)[1:])
+    return PluginCommand.exclusive
+
+  @asyncio.coroutine
+  def handle_help(self, message, *args):
+    lines = [
+      '{} can help you find ships, modules and commodities.'.format(client.user.mention),
+      '```shopping [discounted] [near SYSTEM] [SHIPS] [MODULES] [COMMODITIES]```',
+      '```',
+      'discounted',
+      '```',
+      'Only search for stations in Li Yong-Rui space.',
+      '',
+      '```near SYSTEM```',
+      'Search for stations close to `SYSTEM`.',
+      '',
+      '```',
+      'SHIPS',
+      '```',
+      'One or more ships to find.',
+      '',
+      '```',
+      'MODULES',
+      '```',
+      'One ore more modules to find.  When searching for weapons you should specify the class, rating and weapon type, eg `4A Fixed Beam Laser`.  When searching for armour you should specify the ship, eg `Anaconda Military Grade Composite`.',
+      '',
+      'Example: `shopping near Bugayaman Gold`'
+    ]
+    yield from bot.say_many(message.channel, lines)
+
+  @asyncio.coroutine
+  def handle_shopping(self, message, args):
+    if not len(args):
+      return
+
+    if args[0].lower() == 'help':
+      yield from self.handle_help(message)
+      return
+
+    yield from client.send_typing(message.channel)
+
+    params = {}
+    names = []
+    i = 0
+    while i < len(args):
+      arg = args[i].lower()
+      log.info(arg)
+      if arg == 'discounted':
+        params['discount'] = True
+      elif arg == 'near':
+        try:
+          params['system'] = args[i + 1]
+          i += 1
+        except IndexError:
+          yield from bot.say(message.channel, 'Where?')
+          return
+      else:
+        names.append(args[i])
+      i += 1
+
+    yield from self.find_shopping_stations(message, names, **params)
+
+  def filter_key(self, entry, key, value = None):
+    if key is None:
+      return entry
+    parts = key.split('.')
+    ok = False
+    e = entry
+    while len(parts):
+      k = parts[0]
+      if k not in e:
+        return None
+      e = e[k]
+      if type(e) != dict:
+        if value is None or e == value:
+          return { key: e }
+      parts = parts[1:]
+    return None
+
+  def read_json(self, filename, *, key = None, value = None, include = None):
+    try:
+      fd = open(filename, 'r')
+      parsed = json.loads(fd.read())
+      fd.close()
+
+      for entry in parsed:
+        if self.filter_key(entry, key, value) is None:
+          continue
+        if include is None:
+          yield entry
+        filtered = {}
+        for i in include:
+          value = self.filter_key(entry, i)
+          if value is not None:
+            filtered[i] = value[i]
+        yield filtered
+    except:
+      logging.exception('read_json')
+      return None
+
+  def parse_commodities(self, **args):
+    for commodity in self.read_json(self.COMMODITIES, include = self.COMMODITY_KEYS, **args):
+      if 'price' in commodity:
+        if commodity['price'] == -1:
+          del(commodity['price'])
+      if 'is_rare' in commodity:
+        commodity['is_rare'] = bool(commodity['is_rare'])
+      yield commodity
+
+  def parse_modules(self, **args):
+    for module in self.read_json(self.MODULES, include = self.MODULE_KEYS, **args):
+      if 'group.name' in module:
+        if module['name'] is None:
+          module['name'] = module['group.name']
+        del(module['group.name'])
+      if 'ship' in module:
+        if module['ship'] is not None:
+          module['name'] = '{} {}'.format(module['ship'], module['name'])
+        del(module['ship'])
+      if 'price' in module:
+        if module['price'] == -1:
+          del(module['price'])
+      yield module
+
+  def parse_ships(self, **args):
+    for ship in self.read_json(self.SHIPS, include = self.SHIP_KEYS, **args):
+      yield ship
+
+  def eddb_query(self, root, params, expand = None):
+    try:
+      queries = []
+      for k, v in params.items():
+        queries.append('{}[{}]={}'.format(root, k, v))
+      if expand is not None:
+        queries.append('expand={}'.format(expand))
+      fd = bot.open_url('https://eddb.io/{}/search?{}'.format(root, '&'.join(queries)))
+      parsed = json.loads(fd.read().decode('utf-8'))
+      fd.close()
+      return parsed
+    except:
+      logging.exception('eddb_query')
+      return None
+
+  def find_system(self, name, expand = None):
+    return self.eddb_query('system', { 'name': name }, expand)
+
+  def find_station(self, params, expand = None):
+    return self.eddb_query('station', params, expand)
+
+  def get_commodities_by_name(self, names):
+    commodities = []
+    for commodity in self.parse_commodities():
+      for name in names:
+        if commodity['name'].lower() == name.lower():
+          commodity['search'] = name
+          commodities.append(commodity)
+          break
+    return commodities
+
+  def filter_module(self, module, canon):
+    for canon_module in canon:
+      if module['name'].lower() != canon_module['name'].lower():
+        continue
+      if 'weapon_mode' in canon_module:
+        if 'weapon_mode' not in module:
+          continue
+        if module['weapon_mode'] != canon_module['weapon_mode']:
+          continue
+      if 'class' in canon_module:
+        if module['class'] != canon_module['class']:
+          continue
+      if 'rating' in canon_module:
+        if module['rating'] != canon_module['rating']:
+          continue
+      module['search'] = canon_module['search']
+      return True
+    return False
+
+  def get_modules_by_name(self, names):
+    modules = []
+    canon = []
+    for name in names:
+      m = re.match(r'(?:([1-8][A-I]|[A-I][1-8])\s*)?(.+)', name, re.IGNORECASE)
+      if m is None:
+        continue
+      module = { 'search': name }
+      classrating = m.group(1)
+      module_name = m.group(2)
+      m = re.match(r'(Fixed|Gimbal|Turret)\s*(.+)', module_name, re.IGNORECASE)
+      if m is not None:
+        module['weapon_mode'] = m.group(1)[0].upper() + m.group(1)[1:].lower()
+        module_name = m.group(2)
+      module['name'] = module_name
+      if classrating:
+        module['class'] = int(re.sub(r'[^\d]', '', classrating))
+        module['rating'] = re.sub(r'\d', '', classrating).upper()
+      canon.append(module)
+    if len(canon):
+      for module in self.parse_modules():
+        if self.filter_module(module, canon):
+          modules.append(module)
+    return modules
+
+  def get_ships_by_name(self, names):
+    ships = []
+    for ship in self.parse_ships():
+      for name in names:
+        if ship['name'].lower() != name.lower():
+          if 'aliases' not in ship:
+            continue
+          if name.lower() not in [alias.lower() for alias in ship['aliases']]:
+            continue
+          ship['search'] = name
+          ships.append(ship)
+          break
+    return ships
+
+  @asyncio.coroutine
+  def build_shopping_list(self, message, names):
+    commodities = list(self.get_commodities_by_name(names))
+    modules = list(self.get_modules_by_name(names))
+    ships = list(self.get_ships_by_name(names))
+    found = set([c['search'] for c in commodities] + [m['search'] for m in modules] + [s['search'] for s in ships])
+    if len(found) < len(names):
+      missing = [name for name in names if name not in found]
+      yield from bot.say(message.channel, '{}?'.format(', '.join(missing)))
+      return None
+    params = {}
+    if len(commodities):
+      params['sellsCommodityIds'] = ','.join([str(c['id']) for c in commodities])
+    if len(modules):
+      params['sellingModulesIdsString'] = ','.join([str(m['id']) for m in modules])
+    if len(ships):
+      params['sellingShipIdsString'] = ','.join([str(s['id']) for s in ships])
+    return params
+
+  def format_station(self, station, reference_coords = None):
+    text = ''
+
+    system = station['system']
+
+    if reference_coords is not None:
+      distance = math.sqrt(((system['x'] - reference_coords['x']) ** 2) + ((system['y'] - reference_coords['y']) ** 2) + ((system['z'] - reference_coords['z']) ** 2))
+      text = '{:.2f}Ly '.format(distance)
+
+    text += '**{}** '.format(station['name'])
+    if station['max_landing_pad_size'] != 'L':
+      text += '(medium pad) '
+    if station['distance_to_star']:
+      text += '{}Ls from *{}* star'.format(station['distance_to_star'], system['name'])
+    else:
+      text += 'in *{}*'.format(system['name'])
+    return text
+
+  @asyncio.coroutine
+  def find_shopping_stations(self, message, names, *, discount = False, system = None):
+    log.info('Shopping for {}; discount={}; system={}.'.format(', '.join(names), discount, system))
+    params = yield from self.build_shopping_list(message, names)
+    if params is None:
+      return False
+
+    reference_coords = None
+
+    if system is not None:
+      systems = list(self.find_system(system))
+      if len(systems) > 1:
+        log.warning('More than one system matched {}.'.format(system))
+        yield from bot.say('Which system? {}', ','.join([s['name'] for s in systems]))
+        return False
+      elif not len(systems):
+        log.warning('No system matched {}.'.format(system))
+        yield from bot.say('Where is {}?'.format(system))
+        return False
+      params['referenceSystemId'] = systems[0]['id']
+      if 'x' in systems[0]:
+        reference_coords = { 'x': systems[0]['x'], 'y': systems[0]['y'], 'z': systems[0]['z'] }
+      elif 'stations' in systems[0]:
+        # Hack to get coords from station.
+        for station in self.find_station({ 'id': systems[0]['stations'][0]['id'], 'referenceSystemId': systems[0]['id'] }, 'system'):
+          reference_coords = { 'x': station['system']['x'], 'y': station['system']['y'], 'z': station['system']['z'] }
+          break
+
+    if discount:
+      # LYR.
+      params['powerIds'] = 9
+
+    log.info(params)
+    lines = []
+    for station in self.find_station(params, 'system'):
+      lines.append(self.format_station(station, reference_coords))
+    if not len(lines):
+      lines = ['*shrugs*']
+    yield from bot.say_many(message.channel, lines)