Browse Source

more deeply BATCAVE integration

Almost all information is now queried from BATCAVE instead of being
stored locally.
Helge Jung 9 years ago
parent
commit
4a220ec0d5
6 changed files with 243 additions and 266 deletions
  1. 5 0
      modules/batcave/__init__.py
  2. 88 0
      modules/batcave/client.py
  3. 74 200
      modules/ffpb.py
  4. 1 1
      modules/ffpb_fun.py
  5. 34 28
      modules/ffpb_netstatus.py
  6. 41 37
      modules/ffpb_nodeinfo.py

+ 5 - 0
modules/batcave/__init__.py

@@ -0,0 +1,5 @@
+from .client import BatcaveClient
+
+__ALL__ = [
+    'BatcaveClient'
+]

+ 88 - 0
modules/batcave/client.py

@@ -0,0 +1,88 @@
+import json
+import logging
+
+from urllib2 import urlopen, URLError
+
+
+class BatcaveClient(object):
+    def __init__(self, url):
+        assert url.startswith("http://") or url.startswith("https://"), \
+            "BATCAVE URL must use http(s) protocol"
+        assert url.endswith("/"), "BATCAVE URL must end with slash."
+
+        self.base_url = url
+        self.logger = logging.getLogger('BATCAVE')
+
+    def __load_response(self, url, error_context):
+        raw_data = None
+        try:
+            raw_data = urlopen(self.base_url + url)
+        except URLError as err:
+            self.logger.error("Failed to contact BATCAVE for %s: %s",
+                              error_context, err)
+            return None
+
+        try:
+            return json.load(raw_data)
+        except ValueError as err:
+            self.logger.error("Could not parse response for %s: %s",
+                              error_context, err)
+            return None
+
+    def get_nodes(self):
+        url = 'nodes.json'
+        response = self.__load_response(url, 'nodes')
+        return response.get('nodes') if response is not None else None
+
+    def get_node(self, nodeid):
+        """Query the given node's data from the BATCAVE."""
+
+        url = 'node/{0}.json'.format(nodeid)
+        return self.__load_response(url, 'node \'' + nodeid + '\'')
+
+    def find_node_by_name(self, name, fuzzymatch=True, single_match_only=True):
+        """Tries to find a node by given name."""
+
+        url = 'find?name=' + name + '&fuzzy=' + ('1' if fuzzymatch else '0')
+        matches = self.__load_response(url, 'find_name=' + name)
+
+        if matches is None:
+            return None
+
+        if single_match_only:
+            if len(matches) == 1:
+                return matches[0]
+            else:
+                return None
+
+        return matches
+
+    def find_node_by_mac(self, mac, single_match_only=True):
+        """Tries to find a node by given MAC address."""
+
+        url = 'find?mac=' + mac
+        matches = self.__load_response(url, 'find_mac=' + mac)
+
+        if single_match_only:
+            if len(matches) == 1:
+                return matches[0]
+            else:
+                return None
+
+        return matches
+
+    def get_nodefield(self, nodeid, field):
+        """Query the given field for the given nodeid from the BATCAVE."""
+
+        url = 'node/{0}/{1}'.format(nodeid, field)
+        ctx = "node '{0}'->'{1}'".format(nodeid, field)
+
+        return self.__load_response(url, ctx)
+
+    def get_providers(self):
+        url = 'providers?format=json'
+        return self.__load_response(url, 'providers')
+
+    def get_status(self):
+        url = 'status'
+        return self.__load_response(url, 'status')

+ 74 - 200
modules/ffpb.py

@@ -15,18 +15,26 @@ import os
 import random
 import shelve
 import subprocess
+import sys
 import time
 
 import dns.resolver, dns.reversename
 import SocketServer
 import threading
 
+# ensure our directory is on path (in order to load batcave module)
+__my_dir = os.path.dirname(__file__)
+if __my_dir not in sys.path:
+    sys.path.append(__my_dir)
+
+from batcave import BatcaveClient
+
 msgserver = None
 peers_repo = None
 
 nodeaccess = None
 
-alfred_method = None
+__batcave = None
 
 ffpb_resolver = dns.resolver.Resolver()
 ffpb_resolver.nameservers = ['10.132.254.53']
@@ -80,7 +88,7 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
 def setup(bot):
     """Called by willie upon loading this plugin."""
 
-    global msgserver, peers_repo, alfred_method, nodeaccess
+    global __batcave, msgserver, peers_repo, nodeaccess
 
     # signal begin of setup routine
     bot.memory['ffpb_in_setup'] = True
@@ -120,13 +128,8 @@ def setup(bot):
         msgserver_thread.daemon = True
         msgserver_thread.start()
 
-    # initially fetch ALFRED data
-    alfred_method = bot.config.ffpb.alfred_method
-    if not 'alfred_data' in bot.memory:
-        bot.memory['alfred_data'] = {}
-    if not 'alfred_update' in bot.memory:
-        bot.memory['alfred_update'] = datetime(1970, 1, 1, 23, 42)
-    ffpb_updatealfred(bot)
+    # initialize BATCAVE
+    __batcave = BatcaveClient(bot.config.ffpb.batcave_url)
 
     # signal end of setup routine
     bot.memory['ffpb_in_setup'] = False
@@ -367,7 +370,7 @@ def ffpb_ensurenodeid(nodedata):
     return result
 
 
-def ffpb_findnode(name, alfred_data=None, allow_fuzzymatching=True):
+def ffpb_findnode(name, allow_fuzzymatching=True):
     """helper: try to identify the node the user meant by the given name"""
 
     # no name, no node
@@ -382,30 +385,19 @@ def ffpb_findnode(name, alfred_data=None, allow_fuzzymatching=True):
         name = name[1:-1]
         allow_fuzzymatching = False
 
-    names = {}
-
-    if not alfred_data is None:
-        # try to match MAC
-        m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
-        if not m is None:
-            mac = m.group(0).lower()
-            if mac in alfred_data:
-                return ffpb_ensurenodeid(alfred_data[mac])
-
-            # try to find alias MAC in ALFRED data
-            for nodeid in alfred_data:
-                node = alfred_data[nodeid]
-                if "network" in node:
-                    if node["network"].get("mac", "").lower() == mac:
-                        return ffpb_ensurenodeid(node)
-                    if "mesh_interfaces" in node["network"]:
-                        for mim in node["network"]["mesh_interfaces"]:
-                            if mim.lower() == mac:
-                                return ffpb_ensurenodeid(node)
+    # try to match MAC
+    m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
+    if not m is None:
+        mac = m.group(0).lower()
+        node = __batcave.find_node_by_mac(mac)
+
+        if node is not None:
+            return node
 
+        else:
             nodeid = mac.replace(':', '').lower()
             return {
-                'nodeid': nodeid,
+                'node_id': nodeid,
                 'hostname': '?-' + nodeid,
                 'network': {
                     'addresses': [mac2ipv6(mac, 'fdca:ffee:ff12:132:')],
@@ -416,15 +408,10 @@ def ffpb_findnode(name, alfred_data=None, allow_fuzzymatching=True):
                 },
             }
 
-        # look through the ALFRED peers
-        for nodeid in alfred_data:
-            node = alfred_data[nodeid]
-            if 'hostname' in node:
-                h = node['hostname']
-                if h.lower() == name.lower():
-                    return node
-                else:
-                    names[h] = nodeid
+    # try to find by NAME
+    node = __batcave.find_node_by_name(name, fuzzymatch=allow_fuzzymatching)
+    if node is not None:
+        return __batcave.get_node(node['id'])
 
     # not found in ALFRED data -> try peers_repo
     if not peers_repo is None:
@@ -458,18 +445,6 @@ def ffpb_findnode(name, alfred_data=None, allow_fuzzymatching=True):
                 },
             }
 
-    # do a similar name lookup in the ALFRED data
-    if allow_fuzzymatching and not alfred_data is None:
-        allnames = [x for x in names]
-        possibilities = difflib.get_close_matches(name, allnames, cutoff=0.75)
-        print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(
-            name,
-            len(possibilities), ', '.join(possibilities))
-        )
-        if len(possibilities) == 1:
-            # if we got exactly one candidate that might be it
-            return ffpb_ensurenodeid(alfred_data[names[possibilities[0]]])
-
     # none of the above was able to identify the requested node
     return None
 
@@ -482,14 +457,7 @@ def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata=True):
             bot.reply("Grün.")
         return None
 
-    alfred_data = get_alfred_data(bot, ensure_recent_alfreddata)
-    if ensure_recent_alfreddata and alfred_data is None:
-        if not bot is None:
-            bot.say('Informationen sind ausverkauft bzw. veraltet, ' +
-                    'daher sage ich mal lieber nichts zu \'' + name + '\'.')
-        return None
-
-    node = ffpb_findnode(name, alfred_data)
+    node = ffpb_findnode(name)
     if node is None:
         if not bot is None:
             bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
@@ -507,142 +475,48 @@ def mac2ipv6(mac, prefix=None):
     return result
 
 
-@willie.module.interval(30)
-def ffpb_updatealfred(bot):
-    """Aktualisiere ALFRED-Daten"""
-
-    if alfred_method is None or alfred_method == "None":
+def ffpb_notify_newly_seen_nodes(bot, new):
+    if not isinstance(bot, dict):
         return
-
-    updated = None
-    if alfred_method == "exec":
-        rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
-        updated = datetime.now()
-
-    elif alfred_method.startswith("http"):
-        try:
-            rawdata = urllib2.urlopen(alfred_method)
-        except urllib2.URLError as err:
-            print("Failed to download ALFRED data:" + str(err))
-            return
-        last_modified = rawdata.info().getdate_tz("Last-Modified")
-        updated = datetime.fromtimestamp(mktime_tz(last_modified))
-
-    else:
-        print("Unknown ALFRED data method '{0}', cannot load new data.".format(alfred_method))
-        alfred_data = None
+    if len(new) == 0 or bot.memory['ffpb_in_setup']:
         return
 
-    try:
-        alfred_data = json.load(rawdata)
-        #print("Fetched new ALFRED data:", len(alfred_data), "entries")
-
-    except ValueError as err:
-        print("Failed to parse ALFRED data: " + str(err))
-        return
-
-    bot.memory['alfred_data'] = alfred_data
-    bot.memory['alfred_update'] = updated
-
-    seen_nodes = bot.memory.get('seen_nodes', None)
-    if not seen_nodes is None:
-        new = []
-        for nodeid in alfred_data:
-            nodeid = str(nodeid)
-            if not nodeid in seen_nodes:
-                seen_nodes[nodeid] = updated
-                new.append((nodeid, alfred_data[nodeid]['hostname']))
-                print('First time seen: ' + str(nodeid))
-        if len(new) > 0 and not bot.memory['ffpb_in_setup']:
-            action_msg = None
-            if len(new) == 1:
-                action_msg = random.choice((
-                    'bemerkt den neuen Knoten {0}',
-                    'entdeckt {0}',
-                    'reibt sich die Augen und erblickt einen verpackungsfrischen Knoten {0}',
-                    u'heißt {0} im Mesh willkommen',
-                    'freut sich, dass {0} aufgetaucht ist',
-                    'traut seinen Augen kaum. {0} sagt zum ersten Mal: Hallo Freifunk Paderborn',
-                    u'sieht die ersten Herzschläge von {0}',
-                    u'stellt einen großen Pott Heißgetränk zu {0} und fragt ob es hier Meshpartner gibt.',
-                )).format('\'' + str(new[0][1]) + '\'')
-
-                # try to fetch location from BATCAVE in order to add a geomap URL
-                location = ffpb_get_batcave_nodefield(str.replace(new[0][0], ':', ''), 'location')
-                if not location is None:
-                    action_msg += ' http://map.paderborn.freifunk.net/geomap.html?lat=' + location['latitude'] + '&lon=' + location['longitude']
-            else:
-                action_msg = random.choice((
-                    'bemerkt die neuen Knoten {0} und {1}',
-                    'hat {0} und {1} entdeckt',
-                    'bewundert {0} sowie {1}',
-                    'freut sich, dass {0} und {1} nun auch online sind',
-                    u'heißt {0} und {1} im Mesh willkommen',
-                    'fragt sich ob die noch jungen Herzen von {0} und {1} synchron schlagen',
-                ))
-                all_but_last = [str(x[1]) for x in new[0:-1]]
-                last = str(new[-1][1])
-                action_msg = action_msg.format(
-                    '\'' + '\', \''.join(all_but_last) + '\'',
-                    '\'' + last + '\''
-                )
-            action_target = bot.config.ffpb.msg_target
-            if not bot.config.ffpb.msg_target_public is None:
-                action_target = bot.config.ffpb.msg_target_public
-            bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
-
-
-def get_alfred_data(bot, ensure_not_outdated=True):
-    """
-    Retrieves the stored alfred_data and optionally checks
-    that it has been updated no more than 5 minutes ago.
-    """
-
-    alfred_data = bot.memory.get('alfred_data', None)
-    alfred_update = bot.memory.get('alfred_update', 0)
-
-    if alfred_data is None:
-        return None
-
-    if ensure_not_outdated:
-        timeout = datetime.now() - timedelta(minutes=5)
-        is_outdated = timeout > alfred_update
-        if is_outdated:
-            return None
-
-    return alfred_data
-
-
-def ffpb_get_batcave_nodefield(nodeid, field):
-    """Query the given field for the given nodeid from the BATCAVE."""
-
-    raw_data = None
-    try:
-        # query BATCAVE for node's field
-        raw_data = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/node/{0}/{1}'.format(nodeid, field))
-    except urllib2.URLError as err:
-        print('Failed to contact BATCAVE for \'{0}\'->\'{1}\': {2}'.format(
-              nodeid, field, err))
-        return None
-
-    try:
-        return json.load(raw_data)
-
-    except ValueError as err:
-        print('Could not parse BATCAVE\'s response as JSON for \'{0}\'->\'{1}\':'.format(nodeid, field, err))
-        return None
-
-
-@willie.module.commands('debug-alfred')
-def ffpb_debug_alfred(bot, trigger):
-    """Show statistics of available ALFRED data."""
-
-    alfred_data = get_alfred_data(bot)
-    if alfred_data is None:
-        bot.say("Keine ALFRED-Daten vorhanden.")
+    action_msg = None
+    if len(new) == 1:
+        action_msg = random.choice((
+            'bemerkt den neuen Knoten {0}',
+            'entdeckt {0}',
+            'reibt sich die Augen und erblickt einen verpackungsfrischen Knoten {0}',
+            u'heißt {0} im Mesh willkommen',
+            'freut sich, dass {0} aufgetaucht ist',
+            'traut seinen Augen kaum. {0} sagt zum ersten Mal: Hallo Freifunk Paderborn',
+            u'sieht die ersten Herzschläge von {0}',
+            u'stellt einen großen Pott Heißgetränk zu {0} und fragt ob es hier Meshpartner gibt.',
+        )).format('\'' + str(new[0][1]) + '\'')
+
+        # try to fetch location from BATCAVE in order to add a geomap URL
+        location = __batcave.get_nodefield(str.replace(new[0][0], ':', ''), 'location')
+        if not location is None:
+            action_msg += ' http://map.paderborn.freifunk.net/geomap.html?lat=' + location['latitude'] + '&lon=' + location['longitude']
     else:
-        bot.say("ALFRED Daten: count={0} lastupdate={1}".format(
-                len(alfred_data), bot.memory['alfred_update']))
+        action_msg = random.choice((
+            'bemerkt die neuen Knoten {0} und {1}',
+            'hat {0} und {1} entdeckt',
+            'bewundert {0} sowie {1}',
+            'freut sich, dass {0} und {1} nun auch online sind',
+            u'heißt {0} und {1} im Mesh willkommen',
+            'fragt sich ob die noch jungen Herzen von {0} und {1} synchron schlagen',
+        ))
+        all_but_last = [str(x[1]) for x in new[0:-1]]
+        last = str(new[-1][1])
+        action_msg = action_msg.format(
+            '\'' + '\', \''.join(all_but_last) + '\'',
+            '\'' + last + '\''
+        )
+    action_target = bot.config.ffpb.msg_target
+    if not bot.config.ffpb.msg_target_public is None:
+        action_target = bot.config.ffpb.msg_target_public
+    bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
 
 
 @willie.module.interval(60)
@@ -783,9 +657,9 @@ def ffpb_ping(bot, trigger=None, target_name=None, reply_directly=True):
     if node is None:
         return None
 
-    # get the first non-linklocal address from the node
-    target = [x for x in node["network"]["addresses"]
-              if not x.lower().startswith("fe80:")][0]
+    # derive node address from MAC
+    node_mac = node.get('mac')
+    target = mac2ipv6(node_mac, 'fdca:ffee:ff12:132:')
     target_alias = node["hostname"]
 
     # execute the actual ping and reply the result
@@ -831,7 +705,7 @@ def ffpb_nodemesh(bot, trigger):
         return
 
     # query BATCAVE for node's neighbours (result is a list of MAC addresses)
-    cave_result = ffpb_get_batcave_nodefield(nodeid, 'neighbours')
+    cave_result = node['neighbours']
     if cave_result is None:
         msg = 'Hm, scheinbar liegen zu \'{0}\' keine Daten vor. ' + \
               'Klingt komisch, ist aber so.'
@@ -840,7 +714,7 @@ def ffpb_nodemesh(bot, trigger):
 
     # query BATCAVE for neighbour's names
     data = '&'.join([str(n) for n in cave_result])
-    req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', data)
+    req = urllib2.urlopen(bot.config.ffpb.batcave_url + 'idmac2name', data)
 
     # filter out duplicate names
     neighbours = set()
@@ -904,9 +778,9 @@ def ffpb_remoteexec(bot, trigger):
     if not playitsafe(bot, trigger, via_channel=True, node=node):
         return
 
-    # use the node's first non-linklocal address
-    naddrs = node["network"]["addresses"]
-    target = [x for x in naddrs if not x.lower().startswith("fe80:")][0]
+    # derive target from node's MAC
+    node_mac = node.get('mac')
+    target = mac2ipv6(node_mac, 'fdca:ffee:ff12:132:')
     target_alias = node["hostname"]
 
     # assemble SSH command

+ 1 - 1
modules/ffpb_fun.py

@@ -31,7 +31,7 @@ def ffpb_greeting(bot, trigger):
         'Hi {0}, bist du einer der {2} Clients an unseren {1} Knoten?',
         'Hey {0}, schön dich zu sehen. Gerade sind übrigens {1} Knoten mit {2} Clients online.',
         '{1} Knoten online, {2} Clients im Netz und {0} gibt uns die Ehre - Herzlich Willkommen :)'))
-    bot.say(greeting.format(trigger.nick, stats["nodes_active"], stats["clients"]))
+    bot.say(greeting.format(trigger.nick, stats["nodes_active"], stats["clients_unique"]))
 
 
 @willie.module.rule(r'(o/|\\o)$')

+ 34 - 28
modules/ffpb_netstatus.py

@@ -7,15 +7,19 @@ import shelve
 import time
 import urllib2
 
-from ffpb import ffpb_fetch_stats, get_alfred_data, pretty_date
+from ffpb import pretty_date
+from batcave import BatcaveClient
 
+__batcave = None
 highscores = None
 
 
 def setup(bot):
     """Called by willie upon loading this plugin."""
 
-    global highscores
+    global __batcave, highscores
+
+    __batcave = BatcaveClient(bot.config.ffpb.batcave_url)
 
     # load highscores from disk
     highscores = shelve.open('highscoredata', writeback=True)
@@ -39,12 +43,19 @@ def shutdown(bot):
         highscores = None
 
 
-@willie.module.interval(15)
+@willie.module.interval(5)
 def ffpb_get_stats(bot):
     """Fetch current statistics, if the highscore changes signal this."""
 
-    (nodes_active, nodes_total, clients_count) = \
-        ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
+    status = __batcave.get_status()
+    if status is None:
+        bot.say('Yikes, offenbar ist das allwissende Auge gerade schlafen.')
+        return
+
+    bot.memory['ffpb_stats'] = status
+
+    (nodes_active, clients_count) = \
+        (status['nodes_active'], status['clients_unique'])
 
     highscore_changed = False
     if nodes_active > highscores['nodes']:
@@ -81,20 +92,20 @@ def ffpb_get_stats(bot):
 def ffpb_status(bot, trigger):
     """State of the network: count of nodes + clients"""
 
-    stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
+    stats = bot.memory.get('ffpb_stats')
     if stats is None:
         bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
         return
 
     bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(
-            stats["nodes_active"], stats["clients"]))
+            stats["nodes_active"], stats["clients_unique"]))
 
 
-@willie.module.commands('batcave-status')
+@willie.module.commands('raw-status')
 def ffpb_batcave_status(bot, trigger):
     """State as given by BATCAVE."""
 
-    status = json.loads(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/status').read())
+    status = __batcave.get_status()
     bot.say('Status: ' + str(json.dumps(status))[1:-1])
 
 
@@ -109,7 +120,7 @@ def ffpb_highscore(bot, trigger):
 
 @willie.module.commands('rollout-status')
 def ffpb_rolloutstatus(bot, trigger):
-    """Display statistic on how many nodes have installed the given firmware version."""
+    """Display statistic on how many nodes have installed which firmware."""
 
     # initialize results dictionary
     result = {}
@@ -120,26 +131,22 @@ def ffpb_rolloutstatus(bot, trigger):
         bot.reply('Dieses Kommando nimmt keinen Parameter mehr an.')
         return
 
-    # get ALFRED data (and ensure it is current)
-    alfred_data = get_alfred_data(bot, True)
-    if alfred_data is None:
-        bot.say('Ich habe irgendein Memo verpasst, sorry - bitte später nochmal fragen.')
-        return
+    nodes = __batcave.get_nodes()
 
     # check each node in ALFRED data
-    for nodeid in alfred_data:
-        item = alfred_data[nodeid]
-        if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
+    for item in nodes:
+        release = item.get('firmware')
+        branch = item.get('autoupdater')
+        enabled = branch != 'off'
+
+        if release is None or branch is None:
             skipped += 1
             continue
 
-        release = item['software']['firmware']['release']
-        branch = item['software']['autoupdater']['branch']
-        enabled = item['software']['autoupdater']['enabled']
         if not release in result or result[release] is None:
-            result[release] = {'stable': None, 'testing': None,}
+            result[release] = {'stable': None, 'testing': None, }
         if not branch in result[release] or result[release][branch] is None:
-            result[release][branch] = {'auto': 0, 'manual': 0, 'total': 0,}
+            result[release][branch] = {'auto': 0, 'manual': 0, 'total': 0, }
 
         result[release][branch]['total'] += 1
         mode = 'auto' if enabled else 'manual'
@@ -160,27 +167,26 @@ def ffpb_rolloutstatus(bot, trigger):
                 output += ','
             first = False
 
-            total = item['total']
             auto_count = item['auto']
             manual_count = item['manual']
 
-            output += ' {2} {0}'.format(branch, total, auto_count, manual_count)
+            output += ' {1} {0}'.format(branch, auto_count)
             if manual_count > 0:
-                output += ' (+{3} manuell)'.format(branch, total, auto_count, manual_count)
+                output += ' (+{0} manuell)'.format(manual_count)
 
         bot.say(output)
 
     # output count of nodes for which the autoupdater's branch and/or
     # firmware version could not be retrieved
     if skipped > 0:
-        bot.say('plus {0} Knoten deren Status gerade nicht abfragbar war'.format(skipped))
+        bot.say('plus {0} Knoten mit unklarem Status'.format(skipped))
 
 
 @willie.module.commands('providers')
 def ffpb_providers(bot, trigger):
     """Fetch the top 5 providers from BATCAVE."""
 
-    providers = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/providers?format=json'))
+    providers = __batcave.get_providers()
     providers.sort(key=lambda x: x['count'], reverse=True)
 
     top5 = providers[:5]

+ 41 - 37
modules/ffpb_nodeinfo.py

@@ -5,7 +5,6 @@ import willie
 
 from ffpb import \
     ffpb_findnode_from_botparam, \
-    ffpb_get_batcave_nodefield, \
     mac2ipv6, playitsafe, pretty_date
 
 
@@ -21,7 +20,7 @@ def shutdown(bot):
     pass
 
 
-@willie.module.commands('alfred-data')
+@willie.module.commands('raw-data')
 def ffpb_peerdata(bot, trigger):
     """Show ALFRED data of the given node."""
 
@@ -42,7 +41,9 @@ def ffpb_peerdata(bot, trigger):
         if key in ['hostname']:
             continue
 
-        bot.say("{0}.{1} = {2}".format(node['hostname'], key, node[key]))
+        bot.say("{0}.{1} = {2}".format(
+            node.get('hostname', '?-' + target_name),
+            key, node[key]))
 
 
 @willie.module.commands('info')
@@ -56,35 +57,27 @@ def ffpb_peerinfo(bot, trigger):
         return
 
     # read node information
-    info_mac = node['network']['mac'] if 'network' in node and 'mac' in node['network'] else '??:??:??:??:??:??'
-    info_id = node['node_id'] if 'node_id' in node else info_mac.replace(':', '')
-    info_name = node['hostname'] if 'hostname' in node else '?-' + info_id
+    info_mac = node.get('network', {}).get('mac', '??:??:??:??:??:??')
+    info_id = node.get('node_id', info_mac.replace(':', ''))
+    info_name = node.get('hostname', '?-' + info_id)
 
     info_hw = ""
     if "hardware" in node:
-        if "model" in node["hardware"]:
-            model = node["hardware"]["model"]
-            info_hw = " model='" + model + "'"
+        model = node["hardware"]
+        info_hw = " model='" + model + "'"
 
     info_fw = ""
     info_update = ""
     if "software" in node:
         if "firmware" in node["software"]:
-            if "release" in node["software"]["firmware"]:
-                info_fw = " firmware=" + str(node["software"]["firmware"]["release"])
-            else:
-                info_fw = " unknown firmware"
+            info_fw = " firmware=" + str(node["software"]["firmware"])
 
         if "autoupdater" in node["software"]:
-            autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
+            autoupdater = node["software"]["autoupdater"]
             info_update = " (autoupdater="+autoupdater+")"
 
     info_uptime = ""
-    uptime = -1
-    if "statistics" in node and "uptime" in node["statistics"]:
-        uptime = int(float(node["statistics"]["uptime"]))
-    elif 'uptime' in node:
-        uptime = int(float(node['uptime']))
+    uptime = node.get('uptime', -1)
 
     if uptime > 0:
         days, rem_d = divmod(uptime, 86400)
@@ -98,12 +91,15 @@ def ffpb_peerinfo(bot, trigger):
             info_uptime = ' up {0}m'.format(minutes)
 
     info_clients = ""
-    clientcount = ffpb_get_batcave_nodefield(info_id, 'clientcount')
+    clientcount = node.get('clientcount')
     if not clientcount is None:
         clientcount = int(clientcount)
         info_clients = ' clients={0}'.format(clientcount)
 
-    bot.say('[{1}]{2}{3}{4}{5}{6}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime, info_clients))
+    bot.say('[{1}]{2}{3}{4}{5}{6}'.format(
+            info_mac, info_name,
+            info_hw, info_fw, info_update,
+            info_uptime, info_clients))
 
 
 @willie.module.commands('last-seen')
@@ -118,30 +114,37 @@ def ffpb_lastseen(bot, trigger):
     if node is None:
         return
 
-    last_seen = ffpb_get_batcave_nodefield(node['node_id'], '__UPDATED__')
-    a_value = int(last_seen['alfred']) if (not last_seen is None) and 'alfred' in last_seen else None
-    a_delta = time.time() - a_value if not a_value is None else None
-    b_value = int(last_seen['batadv']) if (not last_seen is None) and 'batadv' in last_seen else None
-    b_delta = time.time() - b_value if not b_value is None else None
+    node_name = node.get('hostname')
+
+    last_seen = node.get('__UPDATED__')
+    if last_seen is not None:
+        a_value = int(last_seen.get('alfred'))
+        b_value = int(last_seen.get('batadv'))
+    else:
+        a_value = b_value = None
+
+    a_delta = time.time() - a_value if a_value is not None else None
+    b_delta = time.time() - b_value if b_value is not None else None
 
     if a_value is None and b_value is None:
-        bot.say('{0} wurde offenbar noch gar nicht gesehen?'.format(node['hostname']))
+        bot.say('{0} wurde offenbar noch gar nicht gesehen?'.format(node_name))
         return
 
     if a_delta < 30 and b_delta < 30:
-        bot.say('{0} wurde gerade eben gesehen.'.format(node['hostname']))
+        bot.say('{0} wurde gerade eben gesehen.'.format(node_name))
         return
 
-    if a_value is not None and b_value is not None and abs(a_value - b_value) < 60:
+    if a_value is not None and b_value is not None and \
+       abs(a_value - b_value) < 60:
         bot.say('{0} wurde zuletzt gesehen: {1}'.format(
-                node['hostname'],
+                node_name,
                 pretty_date((a_value + b_value) / 2)))
     else:
         bot.say('{0} wurde zuletzt gesehen: {1} (ALFRED,) bzw. {2} (BATMAN)'.format(
-            node['hostname'],
-            pretty_date(a_value) if not a_value is None else "nie",
-            pretty_date(b_value) if not b_value is None else "nie"
-        ))
+                node_name,
+                pretty_date(a_value) if not a_value is None else "nie",
+                pretty_date(b_value) if not b_value is None else "nie"
+                ))
 
 
 @willie.module.commands('uptime')
@@ -193,11 +196,12 @@ def ffpb_peerlink(bot, trigger):
         return
 
     # get node's MAC
-    info_mac = node["network"]["mac"]
-    info_name = node["hostname"]
+    info_mac = node.get('mac')
+    info_name = node.get('hostname')
 
     # get node's v6 address in the mesh (derived from MAC address)
     info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
 
     # reply to user
-    bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
+    bot.say('[{1}] mac {0} -> http://[{2}]/'.format(
+            info_mac, info_name, info_v6))