예제 #1
0
파일: bot.py 프로젝트: robhaswell/dropbot
    def handle_session_start(self, event):
        self.get_roster()
        self.send_presence()

        # Join the defined MUC rooms
        for room in self.rooms:
            self.plugin['xep_0045'].joinMUC(room, self.nickname, wait=True)

        # Start the killchecker if we have corps to monitor
        if len(self.kill_corps) > 0 and not self.kills_disabled:
            logging.info('Starting ZKB Stomp monitor for corps: {}'.format(
                ', '.join(self.kill_corps)))
            self.stomp = ZKillboardStompListener(self)
            self.stomp.connect('tcp://eve-kill.net:61613')
        else:
            logging.info('Kill monitoring disabled.')
예제 #2
0
파일: bot.py 프로젝트: esmaxwill/dropbot
    def handle_session_start(self, event):
        self.get_roster()
        self.send_presence()

        # Join the defined MUC rooms
        for room in self.rooms:
            self.plugin['xep_0045'].joinMUC(room, self.nickname, wait=True)

        # Start the killchecker if we have corps to monitor
        if len(self.kill_corps) > 0 and not self.kills_disabled:
            logging.info('Starting ZKB Stomp monitor for corps: {}'.format(', '.join(self.kill_corps)))
            self.stomp = ZKillboardStompListener(self)
            self.stomp.connect('tcp://eve-kill.net:61613')
        else:
            logging.info('Kill monitoring disabled.')
예제 #3
0
파일: bot.py 프로젝트: esmaxwill/dropbot
class DropBot(ClientXMPP):
    def __init__(self, *args, **kwargs):
        self.rooms = kwargs.pop('rooms', [])
        self.nickname = kwargs.pop('nickname', 'Dropbot')
        self.cmd_prefix = kwargs.pop('cmd_prefix', '!')
        self.kos_url = kwargs.pop('kos_url', 'http://kos.cva-eve.org/api/')
        self.hidden_commands = ['cmd_prefix']
        self.last_killdate = datetime.utcnow()
        self.kill_corps = [int(x) for x in kwargs.pop('kill_corps', [])]
        self.kills_disabled = kwargs.pop('kills_disabled', '0') == '1'
        self.kills_muted = False
        self.office_api_key_keyid = kwargs.pop('office_api_keyid', None)
        self.office_api_key_vcode = kwargs.pop('office_api_vcode', None)
        self.market_systems = kwargs.pop('market_systems', ['Jita', 'Amarr', 'Rens', 'Dodixie', 'Hek'])

        if 'redis_url' in kwargs:
            self.redis_pool = ConnectionPool.from_url(kwargs.pop('redis_url', 'redis://localhost:6379/0'))
            self.redis = Redis(connection_pool=self.redis_pool)
        else:
            logging.warning('No DROPBOT_REDIS_URL defined, EVE API calls will not be cached!')
            self.redis = None
        self.map = Map.from_json(pkgutil.get_data('dropbot', 'data/map.json'))

        jid = kwargs.pop('jid', None)
        password = kwargs.pop('password', None)

        super(DropBot, self).__init__(jid, password)

        self.register_plugin('xep_0030')  # Service Discovery
        self.register_plugin('xep_0045')  # Multi-User Chat
        self.register_plugin('xep_0199')  # XMPP Ping

        # Basic bot auto config
        self.auto_subscribe = False
        self.auto_authorize = True

        # Handlers
        self.add_event_handler('session_start', self.handle_session_start)
        self.add_event_handler('message', self.handle_message)

    # Reference Data

    @property
    def types(self):
        if not hasattr(self, '_types'):
            data = pkgutil.get_data('dropbot', 'data/types.json')
            self._types = base_loads(data)
        return self._types

    @property
    def stations(self):
        if not hasattr(self, '_stations'):
            data = pkgutil.get_data('dropbot', 'data/stations.json')
            self._stations = base_loads(data)
            logging.debug('Getting ConquerableStationList')
            for x in self.get_eveapi().eve.ConquerableStationList().outposts:
                self._stations[unicode(x.stationID)] = x.solarSystemID
        return self._stations

    # Command / Connection Handling
    def handle_session_start(self, event):
        self.get_roster()
        self.send_presence()

        # Join the defined MUC rooms
        for room in self.rooms:
            self.plugin['xep_0045'].joinMUC(room, self.nickname, wait=True)

        # Start the killchecker if we have corps to monitor
        if len(self.kill_corps) > 0 and not self.kills_disabled:
            logging.info('Starting ZKB Stomp monitor for corps: {}'.format(', '.join(self.kill_corps)))
            self.stomp = ZKillboardStompListener(self)
            self.stomp.connect('tcp://eve-kill.net:61613')
        else:
            logging.info('Kill monitoring disabled.')

    def call_command(self, command, *args, **kwargs):
        if hasattr(self, 'cmd_%s' % command):
            try:
                resp = getattr(self, 'cmd_%s' % command)(*args, **kwargs)
            except:
                resp = 'Oops, something went wrong...'
                logging.getLogger(__name__).exception('Error handling command')
            if resp:
                if isinstance(resp, tuple) and len(resp) == 2:
                    return resp
                else:
                    return resp, None
            else:
                return None, None
        else:
            raise UnknownCommandException

    def handle_message(self, msg):
        args = msg['body'].split(' ')
        cmd = args[0].lower()
        args.pop(0)
        if msg['type'] == 'groupchat':
            if msg['mucnick'] == self.nickname:
                return
            if msg['body'][0] != self.cmd_prefix:
                # If its not a command, check for ZKB urls
                seen = set([])
                response_lines = []
                for match in zkillboard_regex.finditer(msg['body']):
                    kill_id = match.groupdict()['killID']
                    host = match.groupdict()['host']
                    logging.info('Found Kill ID {}'.format(kill_id))
                    if kill_id in seen:
                        continue
                    body, html = self.call_command('kill', [kill_id], msg, no_url=True, host=host)
                    response_lines.append(body)
                    seen.add(kill_id)
                response_lines = [x for x in response_lines if x]
                if len(response_lines):
                    msg.reply('\n'.join(response_lines)).send()
                return
            # Strip the cmd_prefix
            cmd = cmd[1:]

        # Call the command
        try:
            body, html = self.call_command(cmd, args, msg)
        except UnknownCommandException:
            if msg['type'] != 'groupchat':
                msg.reply('Unknown command, use "help" to list all commands available').send()
            pass
        else:
            if body:
                msg.reply(body).send()

    # Helpers
    def _system_picker(self, name):
        systems = self.map.get_systems(name)
        if len(systems) > 1:
            if len(systems) > 10:
                return 'More than 10 systems match {}, please provide a more complete name'.format(name)
            return 'Did you mean: {}?'.format(', '.join([self.map.get_system_name(x) for x in systems]))
        elif len(systems) == 0:
            return 'No systems found matching {}'.format(name)
        else:
            return systems[0]

    def _item_picker(self, item):
            if item.strip() == '':
                return 'Usage: !price <item>'
            if item.lower() == 'plex':
                return (u"29668", u"30 Day Pilot's License Extension (PLEX)")
            types = dict([(i, v) for i, v in self.types.iteritems() if item.lower() in v.lower()])
            if len(types) == 0:
                return "No items named {} found".format(item)
            elif len(types) > 1:
                for i, v in types.iteritems():
                    if item.lower() == v.lower():
                        return (i, v)
                else:
                    if len(types) > 10:
                        return "More than 10 items found, please narrow down what you want."
                    return "Did you mean: {}?".format(
                        ', '.join(types.itervalues())
                    )
            return types.popitem()

    def _get_evecentral_price(self, type_id, system_id):
        try:
            resp = requests.get('http://api.eve-central.com/api/marketstat?typeid={}&usesystem={}'.format(type_id, system_id))
            root = ElementTree.fromstring(resp.content)
        except:
            return None

        return (float(root.findall("./marketstat/type[@id='{}']/sell/min".format(type_id))[0].text),
                float(root.findall("./marketstat/type[@id='{}']/buy/max".format(type_id))[0].text))

    def _system_price(self, args, msg, system, system_id):
        item = ' '.join(args)
        res = self._item_picker(item)
        if isinstance(res, basestring):
            return res
        type_id, type_name = res

        try:
            resp = requests.get('http://api.eve-central.com/api/marketstat?typeid={}&usesystem={}'.format(type_id, system_id))
            root = ElementTree.fromstring(resp.content)
        except:
            return "An error occurred tying to get the price for {}".format(type_name)

        return "{} @ {} | Sell: {} | Buy: {}".format(
            type_name,
            system,
            intcomma(float(root.findall("./marketstat/type[@id='{}']/sell/min".format(type_id))[0].text)),
            intcomma(float(root.findall("./marketstat/type[@id='{}']/buy/max".format(type_id))[0].text)),
        )

    def _get_offices(self, keyid, vcode):
        """Returns a list of offices from a Corp API key"""
        logging.debug('Retreving offices for {}/{}'.format(keyid, vcode))
        if not keyid or not vcode:
            return []
        try:
            assets = self.get_eveapi_auth(keyid, vcode).corp.AssetList()
        except RuntimeError:
            logging.exception('Unable to retrieve asset listing for {}/{}'.format(keyid, vcode))
            return []

        def location_to_station(location_id):
            if location_id >= 67000000:
                return location_id - 6000000
            if location_id >= 66000000:
                return location_id - 6000001
            return location_id

        return [self.stations[unicode(location_to_station(x.locationID))] for x in assets.assets if x.typeID == 27]

    def get_eveapi(self):
        if self.redis:
            return EVEAPIConnection(cacheHandler=EVEAPIRedisCache(self.redis))
        return EVEAPIConnection()

    def get_eveapi_auth(self, keyid, vcode):
        return self.get_eveapi().auth(keyID=keyid, vCode=vcode)

    def check_eveapi_permission(self, keyid, vcode, bit):
        try:
            accessmask = int(self.get_eveapi_auth(keyid, vcode).account.APIKeyInfo().key.accessMask)
            logging.debug('Key ID {} - Access Mask: {}'.format(keyid, accessmask))
        except RuntimeError:
            return False
        mask = 1 << bit
        return (accessmask & mask) > 0

    # Commands

    def cmd_help(self, args, msg):
        if len(args) == 0:
            if msg['type'] == 'groupchat':
                return "Commands: {}\nAll commands are available in private chat without the {} prefix".format(
                    ', '.join([self.cmd_prefix + x[4:] for x in dir(self) if x[:4] == 'cmd_' and x not in self.hidden_commands]),
                    self.cmd_prefix
                )
            else:
                command_lines = ['{}{}: {}'.format(self.cmd_prefix, cmd[4:], getattr(self, cmd).__doc__ or 'No documentation available') for cmd in dir(self) if cmd[:4] == 'cmd_' and cmd not in self.hidden_commands]
                return "Available Commands\n\n{}".format('\n'.join(command_lines))
        cmd = args[0]
        if hasattr(self, 'cmd_%s' % cmd):
            if getattr(self, 'cmd_%s' % cmd).__doc__ is not None:
                return '{}{}: {}'.format(
                    self.cmd_prefix,
                    cmd,
                    getattr(self, 'cmd_%s' % cmd).__doc__
                )
            else:
                return 'This command has no documentation'
        else:
            return 'Unknown command'

    def cmd_bestprice(self, args, msg):
        """Returns the best price for an item out of the current known market hub systems"""
        item = ' '.join(args)
        res = self._item_picker(item)
        if isinstance(res, basestring):
            return res
        type_id, type_name = res

        min_sell = 0
        max_buy = 0
        sell_sys = None
        buy_sys = None

        for name in self.market_systems:
            sys_id = self.map.get_system_id(name)
            if not sys_id:
                continue
            sell, buy = self._get_evecentral_price(type_id, sys_id)
            if (sell < min_sell or min_sell == 0) and sell > 0:
                min_sell = sell
                sell_sys = name
            if buy > max_buy:
                max_buy = buy
                buy_sys = name
        return '{}\nBest Sell: {} @ {} ISK\nBest Buy: {} @ {} ISK'.format(
            type_name,
            sell_sys, intcomma(min_sell),
            buy_sys, intcomma(max_buy)
        )

    def cmd_price(self, args, msg):
        """Returns the price of an item in a particular system"""
        if len(args) < 2:
            return '!price <system name> <item>'
        item = ' '.join(args[1:])
        system_id = self._system_picker(args[0])
        if isinstance(system_id, basestring):
            return system_id
        item = self._item_picker(item)
        if isinstance(item, basestring):
            return item
        type_id, type_name = item
        sell, buy = self._get_evecentral_price(type_id, system_id)
        return '{} @ {} | Sell {} | Buy: {}'.format(
            type_name,
            self.map.get_system_name(system_id),
            intcomma(sell),
            intcomma(buy)
        )

    def cmd_jita(self, args, msg):
        """Returns the price of a item in Jita"""
        return self.cmd_price(['Jita'] + args, msg)

    def cmd_amarr(self, args, msg):
        """Returns the price of a item in Amarr"""
        return self.cmd_price(['Amarr'] + args, msg)

    def cmd_rens(self, args, msg):
        """Returns the price of a item in Rens"""
        return self.cmd_price(['Rens'] + args, msg)

    def cmd_dodixie(self, args, msg):
        """Returns the price of a item in Dodixie"""
        return self.cmd_price(['Dodixie'] + args, msg)
        
    def cmd_hek(self, args, msg):
        """Returns the price of a item in Hek"""
        return self.cmd_price(['Hek'] + args, msg)

    def cmd_uh(self, args, msg):
        """Returns the price of a item in U-HVIX"""
        return self.cmd_price(['U-HVIX'] + args, msg)

    def cmd_r(self, args, msg):
        return self.cmd_redditimg(args, msg)

    def cmd_redditimg(self, args, msg):
        """Shows a random picture from imgur.com reddit section"""
        if len(args) == 0:
            return "Usage: !redditimg <subreddit>"
        imgs = []
        for page in range(1, 11):
            for img in requests.get("http://imgur.com/r/%s/top/all/page/%s.json" % (args[0], page)).json()['data']:
                resp = "%s - http://i.imgur.com/%s%s" % (img['title'], img['hash'], img['ext'])
                if img['nsfw']:
                    resp = resp + " :nsfw:"
                imgs.append(resp)
        if len(imgs):
            return choice(imgs)

    def cmd_kos(self, args, msg):
        """Checks the CVA KOS list for a name"""
        arg = ' '.join(args)
        resp = requests.get(self.kos_url, params={
            'c': 'json',
            'q': arg,
            'type': 'unit',
            'details': None
        })
        if resp.status_code != requests.codes.ok:
            return "Something went wrong (Error %s)" % resp.status_code
        try:
            data = resp.json()
        except:
            return "KOS API returned invalid data."
        if data['message'] != 'OK':
            return "KOS API returned an error."
        if data['total'] == 0:
            return "KOS returned no results (Not on KOS)"

        results = []
        for result in data['results']:
            text = '{} ({}) - {}'.format(
                result['label'],
                result['type'],
                'KOS' if result['kos'] else 'Not KOS'
            )
            results.append(text)
        return '\n'.join(results)

    def cmd_range(self, args, msg):
        """Returns a count of the number of systems in jump range from a source system"""
        if len(args) == 0 or len(args) > 2:
            return '!range <system> <ship class>'

        system = args[0]
        if len(args) == 2:
            ship_class = args[1].lower()
        else:
            ship_class = 'blackops'

        if ship_class not in base_range.keys():
            return 'Unknown class {}, please use one of: {}'.format(
                ship_class,
                ', '.join(base_range.keys())
            )

        system_id = self._system_picker(system)
        if isinstance(system_id, basestring):
            return system_id

        res = {}
        systems = self.map.neighbors_jump(system_id, ship_class=ship_class)
        for sys, range in systems:
            if sys['region'] in res:
                res[sys['region']] += 1
            else:
                res[sys['region']] = 1

        return '{} systems in JDC5 {} range of {}:\n'.format(len(systems), ship_class, self.map.get_system_name(system_id)) + '\n'.join(['{} - {}'.format(x, y) for x, y in res.items()])

    def cmd_route(self, args, msg):
        """Shows the shortest route between two sytems"""
        if len(args) != 2:
            return '!route <source> <destination>'
        source, dest = args

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        route = self.map.route_gate(source, dest)
        route_names = ' -> '.join(['{} ({})'.format(x['name'], round(x['security'], 2)) for x in [self.map.node[y] for y in route]])

        return '{} jumps from {} to {}\n{}'.format(
            len(route)-1,
            self.map.get_system_name(source),
            self.map.get_system_name(dest),
            route_names
        )

    def cmd_addjb(self, args, msg):
        """Adds a jumpbridge to the internal map for routing purposes"""
        if len(args) != 2:
            return '!addjb <source> <destination>'
        source, dest = args

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        self.map.add_jumpbridge(source, dest)
        return "Done"

    def cmd_listjbs(self, args, msg):
        """List all known jumpbridges stored in the map"""
        resp_lines = []
        for u, v, d in self.map.edges_iter(data=True):
            if d['link_type'] == 'bridge':
                line = '{} <-> {} ({}ly)'.format(
                    self.map.get_system_name(u),
                    self.map.get_system_name(v),
                    round(self.map.system_distance(u, v), 2),
                )
                resp_lines.append(line)
        return '\n'.join(resp_lines)

    def cmd_mapstats(self, args, msg):
        """Gives the current overview of the internal map"""
        return '{} systems, {} gate jumps, {} jump bridges'.format(
            len(self.map.nodes()),
            len([u for u, v, d in self.map.edges_iter(data=True) if d['link_type'] == 'gate']),
            len([u for u, v, d in self.map.edges_iter(data=True) if d['link_type'] == 'bridge'])
        )

    def cmd_hit(self, args, msg):
        """Details what class and JDC level is required to jump between two systems"""
        if len(args) != 2:
            return '!hit <source> <destination>'
        source, dest = args

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        if self.map.node[dest]['security'] >= 0.5:
            return '{} is a highsec system'.format(self.map.get_system_name(dest))

        ly = self.map.system_distance(source, dest)

        if ly > 6.5 * (1 + (0.25 * 5)):
            return '{} to {} is greater than {}ly (maximum jump range of all ships)'.format(
                self.map.get_system_name(source),
                self.map.get_system_name(dest),
                6.5 * (1 + (0.25 * 5))
            )

        res = []
        for ship_class in base_range.keys():
            res1 = []
            for skill in [4, 5]:
                if ship_class_to_range(ship_class, skill) >= ly:
                    res1.append('JDC{}'.format(skill))
            if len(res1):
                res.append('{}: {}'.format(ship_class, ', '.join(res1)))

        return '{} -> {} ({}ly) Capable Ship Types:\n{}'.format(
            self.map.get_system_name(source),
            self.map.get_system_name(dest),
            round(ly, 2),
            '\n'.join(res)
        )

    def cmd_jump(self, args, msg):
        """Calculates the shortest jump route between two systems"""
        if len(args) < 2:
            return '!jump <source> <destination> (<ship class> <jdc level> <jfc level>)'
        elif len(args) == 2:
            source, dest = args
            ship_class = 'blackops'
            jdc = jfc = 5
        elif len(args) == 3:
            source, dest, ship_class = args
            jdc = jfc = 5
        elif len(args) == 4:
            source, dest, ship_class, jdc = args
            jfc = 5
        else:
            source, dest, ship_class, jdc, jfc = args
        jf = 5

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        if ship_class not in base_range.keys():
            return 'Unknown class {}, please use one of: {}'.format(
                ship_class,
                ', '.join(base_range.keys())
            )

        try:
            int(jdc)
            int(jfc)
        except ValueError:
            return 'Invalid JDC/JFC level'

        route = self.map.route_jump(source, dest, ship_class=ship_class)
        if len(route):
            return '{} to {} ({}/{}/{}), {} jumps ({}ly / {} isotopes):\n{}'.format(
                self.map.get_system_name(source),
                self.map.get_system_name(dest),
                ship_class,
                jdc,
                jfc,
                len(route)-1,
                round(self.map.route_jump_distance(route), 2),
                round(self.map.route_jump_isotopes(route, int(jfc), ship_class=ship_class, jf_skill=jf), 0),
                ' -> '.join([self.map.get_system_name(x) for x in route])
            )
        else:
            return 'No route found'

    def cmd_id(self, args, msg):
        """Provides an overview of a character's activity in-game"""
        if len(args) == 0:
            return '!id <character name>'
        char_name = ' '.join(args)

        result = self.get_eveapi().eve.CharacterID(names=char_name.strip())
        char_name = result.characters[0].name
        char_id = result.characters[0].characterID

        if char_id == 0:
            return 'Unknown character {}'.format(char_name)

        headers, res = ZKillboard().characterID(char_id).kills().pastSeconds(60 * 60 * 24 * 7).get()

        from collections import defaultdict, Counter

        kill_types = defaultdict(int)
        ship_types = defaultdict(int)
        alli_assoc = defaultdict(int)
        sum_value = 0.0
        for kill in res:
            kill_type_id = int(kill['victim']['shipTypeID'])
            if kill_type_id > 0:
                kill_types[self.types[unicode(kill_type_id)]] += 1
            sum_value += float(kill['zkb']['totalValue'])
            for attk in kill['attackers']:
                if attk['allianceName'].strip() != '' and attk['allianceName'] is not None:
                    alli_assoc[attk['allianceName']] += 1
                if int(attk['characterID']) == char_id:
                    ship_type_id = int(attk['shipTypeID'])
                    if ship_type_id > 0:
                        ship_types[self.types[unicode(ship_type_id)]] += 1
                    break
        if len(res) == 0:
            return '{} has had no kills in the last week'.format(char_name)

        kill_types = Counter(kill_types).most_common(5)
        ship_types = Counter(ship_types).most_common(5)
        alli_assoc = Counter(alli_assoc).most_common(5)

        return '{}, {} kill(s) ({} ISK) in the last week\nActive Systems: {}\nTop 5 Killed Types: {}\nTop 5 Ship: {}\nTop 5 Associates: {}'.format(
            char_name,
            len(res),
            intcomma(sum_value),
            ', '.join(set([self.map.node[int(x['solarSystemID'])]['name'] for x in res])),
            ', '.join(['{} ({})'.format(x, y) for x, y in kill_types]),
            ', '.join(['{} ({})'.format(x, y) for x, y in ship_types]),
            ', '.join([x for x, y in alli_assoc])
        )

    def cmd_kill(self, args, msg, no_url=False, raw=None, host=None):
        """Returns a summary of a zKillboard killmail"""
        if not raw:
            if len(args) == 0:
                return '!kill <Kill ID/zKillboard URL>'
            kill_id = args[0]
            try:
                kill_id = int(kill_id)
            except ValueError:
                m = zkillboard_regex.match(kill_id)
                if m:
                    kill_id = m.groupdict()['killID']
                    host = m.groupdict()['host']
                else:
                    return 'Invalid kill ID'

            headers, data = ZKillboard(base_url='https://{}/api/'.format(host)).killID(kill_id).get()
            kill = data[0]
        else:
            kill = raw
            kill_id = raw['killID']

        if no_url:
            url = ''
        else:
            url = ' - https://{}/kill/{}/'.format(host, kill_id)

        # Ignore kills over an hour old if they're from stomp
        age = (datetime.utcnow() - datetime.strptime(kill['killTime'], '%Y-%m-%d %H:%M:%S'))
        if age.total_seconds() > 60 * 60 and raw:
            return

        # Drop kills less than 1mil if they've come from stomp
        if raw and float(kill['zkb']['totalValue']) < 1000000:
            return

        if 'zkb' in kill and 'totalValue' in kill['zkb']:
            value_lost = intword(float(kill['zkb']['totalValue']))
        else:
            value_lost = '???'

        return '{} ({}) in {}, {}, {} attacker(s), {} ISK lost{}'.format(
            kill['victim']['characterName'],
            self.types[unicode(kill['victim']['shipTypeID'])],
            self.map.node[int(kill['solarSystemID'])]['name'],
            naturaltime(age),
            len(kill['attackers']),
            value_lost,
            url,
        )

    def cmd_mute(self, args, msg):
        """Mutes killmail broadcast for 30 minutes"""

        self.kills_muted = True

        def unmute(self):
            self.kills_muted = False

        self.schedule('unmute', 30 * 60, unmute, [self])
        return 'Killmails muted, posting will resume automatically in 30 minutes'

    def cmd_nearestoffice(self, args, msg):
        if len(args) != 1:
            return '!nearestoffice <system>'
        source = args[0]

        if not self.office_api_key_keyid or not self.office_api_key_vcode:
            return 'No Corp API key is setup'
        if not self.check_eveapi_permission(self.office_api_key_keyid, self.office_api_key_vcode, 1):
            return "The API key setup doesn't have the correct permissions"

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source

        min_route = None
        target_office = None
        for office in self._get_offices(self.office_api_key_keyid, self.office_api_key_vcode):
            if office == source:
                return 'An office is in the target system'
            route_length = len(self.map.route_gate(source, office)) - 1
            if not min_route or (route_length) < min_route:
                target_office = office
                min_route = route_length

        if target_office:
            return 'Nearest Office to {} is {}, {} jump(s)'.format(
                self.map.get_system_name(source),
                self.map.get_system_name(target_office),
                min_route,
            )
        return 'No known offices.'

    def cmd_rageping(self, args, msg):
        """Ping spams everyone's name in a room, use with caution"""
        if msg['type'] != 'groupchat':
            return 'This only works in MUC rooms'

        names = self.plugin['xep_0045'].getRoster(msg['from'].bare)
        return 'RAGE PING: {} :frogsiren:'.format(', '.join(names))
예제 #4
0
파일: bot.py 프로젝트: robhaswell/dropbot
class DropBot(ClientXMPP):
    def __init__(self, *args, **kwargs):
        self.rooms = kwargs.pop('rooms', [])
        self.nickname = kwargs.pop('nickname', 'Dropbot')
        self.cmd_prefix = kwargs.pop('cmd_prefix', '!')
        self.kos_url = kwargs.pop('kos_url', 'http://kos.cva-eve.org/api/')
        self.hidden_commands = ['cmd_prefix']
        self.last_killdate = datetime.utcnow()
        self.kill_corps = [int(x) for x in kwargs.pop('kill_corps', [])]
        self.kills_disabled = kwargs.pop('kills_disabled', '0') == '1'
        self.kills_muted = False
        self.office_api_key_keyid = kwargs.pop('office_api_keyid', None)
        self.office_api_key_vcode = kwargs.pop('office_api_vcode', None)

        self.redis_pool = ConnectionPool.from_url(
            kwargs.pop('redis_url', 'redis://localhost:6379/0'))
        self.redis = Redis(connection_pool=self.redis_pool)
        self.map = Map.from_json(pkgutil.get_data('dropbot', 'data/map.json'))

        jid = kwargs.pop('jid', None)
        password = kwargs.pop('password', None)

        super(DropBot, self).__init__(jid, password)

        self.register_plugin('xep_0030')  # Service Discovery
        self.register_plugin('xep_0045')  # Multi-User Chat
        self.register_plugin('xep_0199')  # XMPP Ping

        # Basic bot auto config
        self.auto_subscribe = False
        self.auto_authorize = True

        # Handlers
        self.add_event_handler('session_start', self.handle_session_start)
        self.add_event_handler('message', self.handle_message)

    # Reference Data

    @property
    def types(self):
        if not hasattr(self, '_types'):
            data = pkgutil.get_data('dropbot', 'data/types.json')
            self._types = base_loads(data)
        return self._types

    @property
    def stations(self):
        if not hasattr(self, '_stations'):
            data = pkgutil.get_data('dropbot', 'data/stations.json')
            self._stations = base_loads(data)
            logging.debug('Getting ConquerableStationList')
            for x in self.get_eveapi().eve.ConquerableStationList().outposts:
                self._stations[unicode(x.stationID)] = x.solarSystemID
        return self._stations

    # Command / Connection Handling
    def handle_session_start(self, event):
        self.get_roster()
        self.send_presence()

        # Join the defined MUC rooms
        for room in self.rooms:
            self.plugin['xep_0045'].joinMUC(room, self.nickname, wait=True)

        # Start the killchecker if we have corps to monitor
        if len(self.kill_corps) > 0 and not self.kills_disabled:
            logging.info('Starting ZKB Stomp monitor for corps: {}'.format(
                ', '.join(self.kill_corps)))
            self.stomp = ZKillboardStompListener(self)
            self.stomp.connect('tcp://eve-kill.net:61613')
        else:
            logging.info('Kill monitoring disabled.')

    def call_command(self, command, *args, **kwargs):
        if hasattr(self, 'cmd_%s' % command):
            try:
                resp = getattr(self, 'cmd_%s' % command)(*args, **kwargs)
            except:
                resp = 'Oops, something went wrong...'
                logging.getLogger(__name__).exception('Error handling command')
            if resp:
                if isinstance(resp, tuple) and len(resp) == 2:
                    return resp
                else:
                    return resp, None
            else:
                return None, None
        else:
            raise UnknownCommandException

    def handle_message(self, msg):
        args = msg['body'].split(' ')
        cmd = args[0].lower()
        args.pop(0)
        if msg['type'] == 'groupchat':
            if msg['mucnick'] == self.nickname:
                return
            if msg['body'][0] != self.cmd_prefix:
                # If its not a command, check for ZKB urls
                seen = set([])
                response_lines = []
                for match in zkillboard_regex.finditer(msg['body']):
                    kill_id = match.groupdict()['killID']
                    host = match.groupdict()['host']
                    logging.info('Found Kill ID {}'.format(kill_id))
                    if kill_id in seen:
                        continue
                    body, html = self.call_command('kill', [kill_id],
                                                   msg,
                                                   no_url=True,
                                                   host=host)
                    response_lines.append(body)
                    seen.add(kill_id)
                response_lines = [x for x in response_lines if x]
                if len(response_lines):
                    msg.reply('\n'.join(response_lines)).send()
                return
            # Strip the cmd_prefix
            cmd = cmd[1:]

        # Call the command
        try:
            body, html = self.call_command(cmd, args, msg)
        except UnknownCommandException:
            if msg['type'] != 'groupchat':
                msg.reply(
                    'Unknown command, use "help" to list all commands available'
                ).send()
            pass
        else:
            if body:
                msg.reply(body).send()

    # Helpers
    def _system_picker(self, name):
        systems = self.map.get_systems(name)
        if len(systems) > 1:
            if len(systems) > 10:
                return 'More than 10 systems match {}, please provide a more complete name'.format(
                    name)
            return 'Did you mean: {}?'.format(', '.join(
                [self.map.get_system_name(x) for x in systems]))
        elif len(systems) == 0:
            return 'No systems found matching {}'.format(name)
        else:
            return systems[0]

    def _item_picker(self, item):
        if item.strip() == '':
            return 'Usage: !price <item>'
        if item.lower() == 'plex':
            item = '30 Day'
        types = dict([(i, v) for i, v in self.types.iteritems()
                      if item.lower() in v.lower()])
        if len(types) == 0:
            return "No items named {} found".format(item)
        elif len(types) > 1:
            for i, v in types.iteritems():
                if item.lower() == v.lower():
                    return (i, v)
            else:
                if len(types) > 10:
                    return "More than 10 items found, please narrow down what you want."
                return "Did you mean: {}?".format(', '.join(
                    types.itervalues()))
        return types.popitem()

    def _get_evecentral_price(self, type_id, system_id):
        try:
            resp = requests.get(
                'http://api.eve-central.com/api/marketstat?typeid={}&usesystem={}'
                .format(type_id, system_id))
            root = ElementTree.fromstring(resp.content)
        except:
            return None

        return (float(
            root.findall("./marketstat/type[@id='{}']/sell/min".format(
                type_id))[0].text),
                float(
                    root.findall("./marketstat/type[@id='{}']/buy/max".format(
                        type_id))[0].text))

    def _system_price(self, args, msg, system, system_id):
        item = ' '.join(args)
        res = self._item_picker(item)
        if isinstance(res, basestring):
            return res
        type_id, type_name = res

        try:
            resp = requests.get(
                'http://api.eve-central.com/api/marketstat?typeid={}&usesystem={}'
                .format(type_id, system_id))
            root = ElementTree.fromstring(resp.content)
        except:
            return "An error occurred tying to get the price for {}".format(
                type_name)

        return "{} @ {} | Sell: {} | Buy: {}".format(
            type_name,
            system,
            intcomma(
                float(
                    root.findall("./marketstat/type[@id='{}']/sell/min".format(
                        type_id))[0].text)),
            intcomma(
                float(
                    root.findall("./marketstat/type[@id='{}']/buy/max".format(
                        type_id))[0].text)),
        )

    def _get_offices(self, keyid, vcode):
        """Returns a list of offices from a Corp API key"""
        logging.debug('Retreving offices for {}/{}'.format(keyid, vcode))
        if not keyid or not vcode:
            return []
        try:
            assets = self.get_eveapi_auth(keyid, vcode).corp.AssetList()
        except RuntimeError:
            logging.exception(
                'Unable to retrieve asset listing for {}/{}'.format(
                    keyid, vcode))
            return []

        def location_to_station(location_id):
            if location_id >= 67000000:
                return location_id - 6000000
            if location_id >= 66000000:
                return location_id - 6000001
            return location_id

        return [
            self.stations[unicode(location_to_station(x.locationID))]
            for x in assets.assets if x.typeID == 27
        ]

    def get_eveapi(self):
        return EVEAPIConnection(cacheHandler=EVEAPIRedisCache(self.redis))

    def get_eveapi_auth(self, keyid, vcode):
        return self.get_eveapi().auth(keyID=keyid, vCode=vcode)

    def check_eveapi_permission(self, keyid, vcode, bit):
        try:
            accessmask = int(
                self.get_eveapi_auth(
                    keyid, vcode).account.APIKeyInfo().key.accessMask)
            logging.debug('Key ID {} - Access Mask: {}'.format(
                keyid, accessmask))
        except RuntimeError:
            return False
        mask = 1 << bit
        return (accessmask & mask) > 0

    # Commands

    def cmd_help(self, args, msg):
        if len(args) == 0:
            if msg['type'] == 'groupchat':
                return "Commands: {}\nAll commands are available in private chat without the {} prefix".format(
                    ', '.join([
                        self.cmd_prefix + x[4:] for x in dir(self)
                        if x[:4] == 'cmd_' and x not in self.hidden_commands
                    ]), self.cmd_prefix)
            else:
                command_lines = [
                    '{}{}: {}'.format(
                        self.cmd_prefix, cmd[4:],
                        getattr(self, cmd).__doc__
                        or 'No documentation available') for cmd in dir(self)
                    if cmd[:4] == 'cmd_' and cmd not in self.hidden_commands
                ]
                return "Available Commands\n\n{}".format(
                    '\n'.join(command_lines))
        cmd = args[0]
        if hasattr(self, 'cmd_%s' % cmd):
            if getattr(self, 'cmd_%s' % cmd).__doc__ is not None:
                return '{}{}: {}'.format(self.cmd_prefix, cmd,
                                         getattr(self, 'cmd_%s' % cmd).__doc__)
            else:
                return 'This command has no documentation'
        else:
            return 'Unknown command'

    def cmd_bestprice(self, args, msg):
        """Returns the best price for an item out of the current known market hub systems"""
        item = ' '.join(args)
        res = self._item_picker(item)
        if isinstance(res, basestring):
            return res
        type_id, type_name = res

        min_sell = 0
        max_buy = 0
        sell_sys = None
        buy_sys = None

        for name, sys_id in market_systems:
            sell, buy = self._get_evecentral_price(type_id, sys_id)
            if (sell < min_sell or min_sell == 0) and sell > 0:
                min_sell = sell
                sell_sys = name
            if buy > max_buy:
                max_buy = buy
                buy_sys = name
        return '{}\nBest Sell: {} @ {} ISK\nBest Buy: {} @ {} ISK'.format(
            type_name, sell_sys, intcomma(min_sell), buy_sys,
            intcomma(max_buy))

    def cmd_price(self, args, msg):
        """Returns the price of an item in a particular system"""
        if len(args) < 2:
            return '!price <system name> <item>'
        item = ' '.join(args[1:])
        system_id = self._system_picker(args[0])
        if isinstance(system_id, basestring):
            return system_id
        item = self._item_picker(item)
        if isinstance(item, basestring):
            return item
        type_id, type_name = item
        sell, buy = self._get_evecentral_price(type_id, system_id)
        return '{} @ {} | Sell {} | Buy: {}'.format(
            type_name, self.map.get_system_name(system_id), intcomma(sell),
            intcomma(buy))

    def cmd_jita(self, args, msg):
        return self.cmd_price(['Jita'] + args, msg)

    def cmd_amarr(self, args, msg):
        return self.cmd_price(['Amarr'] + args, msg)

    def cmd_rens(self, args, msg):
        return self.cmd_price(['Rens'] + args, msg)

    def cmd_dodixie(self, args, msg):
        return self.cmd_price(['Dodixie'] + args, msg)

    def cmd_hedgp(self, args, msg):
        return self.cmd_price(['HED-GP'] + args, msg)

    def cmd_ge8(self, args, msg):
        return self.cmd_price(['GE-8JV'] + args, msg)

    def cmd_r(self, args, msg):
        return self.cmd_redditimg(args, msg)

    def cmd_redditimg(self, args, msg):
        """Shows a random picture from imgur.com reddit section"""
        if len(args) == 0:
            return "Usage: !redditimg <subreddit>"
        imgs = []
        for page in range(1, 11):
            for img in requests.get(
                    "http://imgur.com/r/%s/top/all/page/%s.json" %
                (args[0], page)).json()['data']:
                resp = "%s - http://i.imgur.com/%s%s" % (
                    img['title'], img['hash'], img['ext'])
                if img['nsfw']:
                    resp = resp + " :nsfw:"
                imgs.append(resp)
        if len(imgs):
            return choice(imgs)

    def cmd_kos(self, args, msg):
        """Checks the CVA KOS list for a name"""
        arg = ' '.join(args)
        resp = requests.get(self.kos_url,
                            params={
                                'c': 'json',
                                'q': arg,
                                'type': 'unit',
                                'details': None
                            })
        if resp.status_code != requests.codes.ok:
            return "Something went wrong (Error %s)" % resp.status_code
        try:
            data = resp.json()
        except:
            return "KOS API returned invalid data."
        if data['message'] != 'OK':
            return "KOS API returned an error."
        if data['total'] == 0:
            return "KOS returned no results (Not on KOS)"

        results = []
        for result in data['results']:
            text = '{} ({}) - {}'.format(result['label'], result['type'],
                                         'KOS' if result['kos'] else 'Not KOS')
            results.append(text)
        return '\n'.join(results)

    def cmd_range(self, args, msg):
        """Returns a count of the number of systems in jump range from a source system"""
        if len(args) == 0 or len(args) > 2:
            return '!range <system> <ship class>'

        system = args[0]
        if len(args) == 2:
            ship_class = args[1].lower()
        else:
            ship_class = 'blackops'

        if ship_class not in base_range.keys():
            return 'Unknown class {}, please use one of: {}'.format(
                ship_class, ', '.join(base_range.keys()))

        system_id = self._system_picker(system)
        if isinstance(system_id, basestring):
            return system_id

        res = {}
        systems = self.map.neighbors_jump(system_id, ship_class=ship_class)
        for sys, range in systems:
            if sys['region'] in res:
                res[sys['region']] += 1
            else:
                res[sys['region']] = 1

        return '{} systems in JDC5 {} range of {}:\n'.format(
            len(systems), ship_class,
            self.map.get_system_name(system_id)) + '\n'.join(
                ['{} - {}'.format(x, y) for x, y in res.items()])

    def cmd_route(self, args, msg):
        """Shows the shortest route between two sytems"""
        if len(args) != 2:
            return '!route <source> <destination>'
        source, dest = args

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        route = self.map.route_gate(source, dest)
        route_names = ' -> '.join([
            '{} ({})'.format(x['name'], round(x['security'], 2))
            for x in [self.map.node[y] for y in route]
        ])

        return '{} jumps from {} to {}\n{}'.format(
            len(route) - 1, self.map.get_system_name(source),
            self.map.get_system_name(dest), route_names)

    def cmd_addjb(self, args, msg):
        """Adds a jumpbridge to the internal map for routing purposes"""
        if len(args) != 2:
            return '!addjb <source> <destination>'
        source, dest = args

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        self.map.add_jumpbridge(source, dest)
        return "Done"

    def cmd_listjbs(self, args, msg):
        """List all known jumpbridges stored in the map"""
        resp_lines = []
        for u, v, d in self.map.edges_iter(data=True):
            if d['link_type'] == 'bridge':
                line = '{} <-> {} ({}ly)'.format(
                    self.map.get_system_name(u),
                    self.map.get_system_name(v),
                    round(self.map.system_distance(u, v), 2),
                )
                resp_lines.append(line)
        return '\n'.join(resp_lines)

    def cmd_mapstats(self, args, msg):
        """Gives the current overview of the internal map"""
        return '{} systems, {} gate jumps, {} jump bridges'.format(
            len(self.map.nodes()),
            len([
                u for u, v, d in self.map.edges_iter(data=True)
                if d['link_type'] == 'gate'
            ]),
            len([
                u for u, v, d in self.map.edges_iter(data=True)
                if d['link_type'] == 'bridge'
            ]))

    def cmd_hit(self, args, msg):
        """Details what class and JDC level is required to jump between two systems"""
        if len(args) != 2:
            return '!hit <source> <destination>'
        source, dest = args

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        if self.map.node[dest]['security'] >= 0.5:
            return '{} is a highsec system'.format(
                self.map.get_system_name(dest))

        ly = self.map.system_distance(source, dest)

        if ly > 6.5 * (1 + (0.25 * 5)):
            return '{} to {} is greater than {}ly (maximum jump range of all ships)'.format(
                self.map.get_system_name(source),
                self.map.get_system_name(dest), 6.5 * (1 + (0.25 * 5)))

        res = []
        for ship_class in base_range.keys():
            res1 = []
            for skill in [4, 5]:
                if ship_class_to_range(ship_class, skill) >= ly:
                    res1.append('JDC{}'.format(skill))
            if len(res1):
                res.append('{}: {}'.format(ship_class, ', '.join(res1)))

        return '{} -> {} ({}ly) Capable Ship Types:\n{}'.format(
            self.map.get_system_name(source), self.map.get_system_name(dest),
            round(ly, 2), '\n'.join(res))

    def cmd_jump(self, args, msg):
        """Calculates the shortest jump route between two systems"""
        if len(args) < 2:
            return '!jump <source> <destination> (<ship class> <jdc level> <jfc level>)'
        elif len(args) == 2:
            source, dest = args
            ship_class = 'blackops'
            jdc = jfc = 5
        elif len(args) == 3:
            source, dest, ship_class = args
            jdc = jfc = 5
        elif len(args) == 4:
            source, dest, ship_class, jdc = args
            jfc = 5
        else:
            source, dest, ship_class, jdc, jfc = args
        jf = 5

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source
        dest = self._system_picker(dest)
        if isinstance(dest, basestring):
            return dest

        if ship_class not in base_range.keys():
            return 'Unknown class {}, please use one of: {}'.format(
                ship_class, ', '.join(base_range.keys()))

        try:
            int(jdc)
            int(jfc)
        except ValueError:
            return 'Invalid JDC/JFC level'

        route = self.map.route_jump(source, dest, ship_class=ship_class)
        if len(route):
            return '{} to {} ({}/{}/{}), {} jumps ({}ly / {} isotopes):\n{}'.format(
                self.map.get_system_name(source),
                self.map.get_system_name(dest), ship_class, jdc, jfc,
                len(route) - 1, round(self.map.route_jump_distance(route), 2),
                round(
                    self.map.route_jump_isotopes(route,
                                                 int(jfc),
                                                 ship_class=ship_class,
                                                 jf_skill=jf), 0),
                ' -> '.join([self.map.get_system_name(x) for x in route]))
        else:
            return 'No route found'

    def cmd_id(self, args, msg):
        """Provides an overview of a character's activity in-game"""
        if len(args) == 0:
            return '!id <character name>'
        char_name = ' '.join(args)

        result = self.get_eveapi().eve.CharacterID(names=char_name.strip())
        char_name = result.characters[0].name
        char_id = result.characters[0].characterID

        if char_id == 0:
            return 'Unknown character {}'.format(char_name)

        headers, res = ZKillboard().characterID(char_id).kills().pastSeconds(
            60 * 60 * 24 * 7).get()

        from collections import defaultdict, Counter

        kill_types = defaultdict(int)
        ship_types = defaultdict(int)
        alli_assoc = defaultdict(int)
        sum_value = 0.0
        for kill in res:
            kill_type_id = int(kill['victim']['shipTypeID'])
            if kill_type_id > 0:
                kill_types[self.types[unicode(kill_type_id)]] += 1
            sum_value += float(kill['zkb']['totalValue'])
            for attk in kill['attackers']:
                if attk['allianceName'].strip(
                ) != '' and attk['allianceName'] is not None:
                    alli_assoc[attk['allianceName']] += 1
                if int(attk['characterID']) == char_id:
                    ship_type_id = int(attk['shipTypeID'])
                    if ship_type_id > 0:
                        ship_types[self.types[unicode(ship_type_id)]] += 1
                    break
        if len(res) == 0:
            return '{} has had no kills in the last week'.format(char_name)

        kill_types = Counter(kill_types).most_common(5)
        ship_types = Counter(ship_types).most_common(5)
        alli_assoc = Counter(alli_assoc).most_common(5)

        return '{}, {} kill(s) ({} ISK) in the last week\nActive Systems: {}\nTop 5 Killed Types: {}\nTop 5 Ship: {}\nTop 5 Associates: {}'.format(
            char_name, len(res), intcomma(sum_value), ', '.join(
                set([
                    self.map.node[int(x['solarSystemID'])]['name'] for x in res
                ])), ', '.join(['{} ({})'.format(x, y)
                                for x, y in kill_types]),
            ', '.join(['{} ({})'.format(x, y) for x, y in ship_types]),
            ', '.join([x for x, y in alli_assoc]))

    def cmd_kill(self, args, msg, no_url=False, raw=None, host=None):
        """Returns a summary of a zKillboard killmail"""
        if not raw:
            if len(args) == 0:
                return '!kill <Kill ID/zKillboard URL>'
            kill_id = args[0]
            try:
                kill_id = int(kill_id)
            except ValueError:
                m = zkillboard_regex.match(kill_id)
                if m:
                    kill_id = m.groupdict()['killID']
                    host = m.groupdict()['host']
                else:
                    return 'Invalid kill ID'

            headers, data = ZKillboard(
                base_url='https://{}/api/'.format(host)).killID(kill_id).get()
            kill = data[0]
        else:
            kill = raw
            kill_id = raw['killID']

        if no_url:
            url = ''
        else:
            url = ' - https://{}/kill/{}/'.format(host, kill_id)

        # Ignore kills over an hour old if they're from stomp
        age = (datetime.utcnow() -
               datetime.strptime(kill['killTime'], '%Y-%m-%d %H:%M:%S'))
        if age.total_seconds() > 60 * 60 and raw:
            return

        # Drop kills less than 1mil if they've come from stomp
        if raw and float(kill['zkb']['totalValue']) < 1000000:
            return

        if 'zkb' in kill and 'totalValue' in kill['zkb']:
            value_lost = intword(float(kill['zkb']['totalValue']))
        else:
            value_lost = '???'

        return '{} ({}) in {}, {}, {} attacker(s), {} ISK lost{}'.format(
            kill['victim']['characterName'],
            self.types[unicode(kill['victim']['shipTypeID'])],
            self.map.node[int(kill['solarSystemID'])]['name'],
            naturaltime(age),
            len(kill['attackers']),
            value_lost,
            url,
        )

    def cmd_mute(self, args, msg):
        """Mutes killmail broadcast for 30 minutes"""

        self.kills_muted = True

        def unmute(self):
            self.kills_muted = False

        self.schedule('unmute', 30 * 60, unmute, [self])
        return 'Killmails muted, posting will resume automatically in 30 minutes'

    def cmd_nearestoffice(self, args, msg):
        if len(args) != 1:
            return '!nearestoffice <system>'
        source = args[0]

        if not self.office_api_key_keyid or not self.office_api_key_vcode:
            return 'No Corp API key is setup'
        if not self.check_eveapi_permission(self.office_api_key_keyid,
                                            self.office_api_key_vcode, 1):
            return "The API key setup doesn't have the correct permissions"

        source = self._system_picker(source)
        if isinstance(source, basestring):
            return source

        min_route = None
        target_office = None
        for office in self._get_offices(self.office_api_key_keyid,
                                        self.office_api_key_vcode):
            if office == source:
                return 'An office is in the target system'
            route_length = len(self.map.route_gate(source, office)) - 1
            if not min_route or (route_length) < min_route:
                target_office = office
                min_route = route_length

        if target_office:
            return 'Nearest Office to {} is {}, {} jump(s)'.format(
                self.map.get_system_name(source),
                self.map.get_system_name(target_office),
                min_route,
            )
        return 'No known offices.'

    def cmd_rageping(self, args, msg):
        """Ping spams everyone's name in a room, use with caution"""
        if msg['type'] != 'groupchat':
            return 'This only works in MUC rooms'

        names = self.plugin['xep_0045'].getRoster(msg['from'].bare)
        return 'RAGE PING: {} :frogsiren:'.format(', '.join(names))