From d7d6914a2772c9d4cccbf08f26756d22c0458a9c Mon Sep 17 00:00:00 2001 From: CMDR furrycat Date: Sun, 6 Nov 2016 17:58:00 +0000 Subject: [PATCH] EDDB helper plugin. --- plugin/shopping/shopping.py | 325 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 plugin/shopping/shopping.py diff --git a/plugin/shopping/shopping.py b/plugin/shopping/shopping.py new file mode 100644 index 0000000..cfde0e4 --- /dev/null +++ b/plugin/shopping/shopping.py @@ -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) -- 2.7.4