--- /dev/null
+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)