123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969 |
- # -*- coding: utf-8 -*-
- from __future__ import print_function
- import willie
- from datetime import datetime, timedelta
- import difflib
- from email.utils import mktime_tz
- from fnmatch import fnmatch
- import git
- import netaddr
- import json
- import urllib2
- import re
- import os
- import random
- import shelve
- import subprocess
- import sys
- import time
- import dns.resolver, dns.reversename
- import socket
- 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
- __batcave = None
- ffpb_resolver = dns.resolver.Resolver()
- ffpb_resolver.nameservers = ['10.132.251.53']
- msg_cache = {}
- msg_cache_time = 10
- class MsgHandler(SocketServer.BaseRequestHandler):
- """Reads line from TCP stream and forwards it to configured IRC channels."""
- def handle(self):
- msg = str (self.request.recv(2048).strip())
- sender = self.resolve_name(self.client_address[0])
- bot = self.server.bot
- if bot is None:
- print("ERROR: No bot in handle() :-(")
- return
- # temporarily mute spam lines
- if sender.startswith('gw') and \
- ('Net::ReadTimeout' in msg or 'SSL_connect (Errno::ECONNRESET)' in msg):
- print('IGNORING spam message from "' + sender + '": "' + msg + '"')
- return
- target = bot.config.core.owner
- if bot.config.has_section('ffpb'):
- is_public = msg.lstrip().startswith("PUBLIC:")
- if is_public and not bot.config.ffpb.msg_target_public is None:
- msg = msg[7:].lstrip()
- target = bot.config.ffpb.msg_target_public
- elif not bot.config.ffpb.msg_target is None:
- target = bot.config.ffpb.msg_target
- # Try to aggregate peer updates and print them in batch
- if msg.startswith ("Peers updated:"):
- if msg not in msg_cache:
- msg_cache[msg] = {
- 'time' : time.time (),
- 'nodes' : []
- }
- # Save message in message cache, will be delivered later
- msg_cache[msg]['nodes'].append (sender)
- return
- # Aggregate identical salt changes reported from different hosts
- match = re.search ("^(\S+): (.*)$", msg)
- if sender == "salt" and match:
- salt_msg = match.group (2)
- if salt_msg not in msg_cache:
- msg_cache[salt_msg] = {
- 'time' : time.time (),
- 'nodes' : []
- }
- msg_cache[salt_msg]['nodes'].append (match.group (1))
- return
- bot.msg(target, "[{0}] {1}".format(sender, msg))
- def resolve_name(self, ipaddr):
- """
- Resolves the host name of the given IP address
- and strips away the suffix ((.srv)?.in)?.ffho.net.
- """
- if ipaddr.startswith("127."):
- return "localhost"
- try:
- addr = dns.reversename.from_address(ipaddr)
- ptr = str(ffpb_resolver.query(addr, "PTR")[0])
- ptr = re.sub("^(bb-[a-z0-9-]+|vlan[0-9]+|[-a-z]+-vpn)\.", "", ptr)
- ptr = re.sub("((.srv)?.in)?.ffho.net.", "", ptr)
- ptr = re.sub("((.srv)?.infra)?.ffpb.", "", ptr)
- return ptr
- except dns.resolver.NXDOMAIN:
- return ipaddr
- except Exception:
- return "E: " + ipaddr
- class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
- """Defines a threaded TCP socket server."""
- bot = None
- def __init__(self, endpoint, handler):
- if ':' in endpoint[0]:
- self.address_family = socket.AF_INET6
- SocketServer.TCPServer.__init__(self, endpoint, handler)
- def setup(bot):
- """Called by willie upon loading this plugin."""
- global __batcave, msgserver, peers_repo, nodeaccess
- # signal begin of setup routine
- bot.memory['ffpb_in_setup'] = True
- # load list of seen nodes from disk
- seen_nodes = shelve.open('nodes.seen', writeback=True)
- bot.memory['seen_nodes'] = seen_nodes
- # load list of node ACL from disk (used in playitsafe())
- nodeaccess = shelve.open('nodes.acl', writeback=True)
- # no need to configure anything else if the ffpb config section is missing
- if not bot.config.has_section('ffpb'):
- bot.memory['ffpb_in_setup'] = False
- return
- # open the git repository containing the peers files
- if not bot.config.ffpb.peers_directory is None:
- peers_repo = git.Repo(bot.config.ffpb.peers_directory)
- assert peers_repo.bare is False
- # if configured, start the messaging server
- if int(bot.config.ffpb.msg_enable) == 1:
- host = "localhost"
- port = 2342
- if not bot.config.ffpb.msg_host is None:
- host = bot.config.ffpb.msg_host
- if not bot.config.ffpb.msg_port is None:
- port = int(bot.config.ffpb.msg_port)
- msgserver = ThreadingTCPServer((host, port), MsgHandler)
- msgserver.bot = bot
- print("Messaging server listening on {}:{}".format(host, port))
- msgserver_thread = threading.Thread(target=msgserver.serve_forever)
- msgserver_thread.daemon = True
- msgserver_thread.start()
- # initialize BATCAVE
- if bot.config.ffpb.batcave_url is None:
- raise Exception("You did not specify [ffpb].batcave_url ...")
- else:
- print('using BATCAVE at ' + bot.config.ffpb.batcave_url)
- __batcave = BatcaveClient(bot.config.ffpb.batcave_url)
- # signal end of setup routine
- bot.memory['ffpb_in_setup'] = False
- def shutdown(bot):
- global msgserver, nodeaccess
- # Print cached message before shutdown, if any
- ffpb_print_cached_messages (bot)
- # store node acl
- if not nodeaccess is None:
- nodeaccess.sync()
- nodeaccess.close()
- nodeaccess = None
- # store seen nodes
- if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
- bot.memory['seen_nodes'].close()
- bot.memory['seen_nodes'] = None
- del bot.memory['seen_nodes']
- # shutdown messaging server
- if not msgserver is None:
- msgserver.shutdown()
- print("Closed messaging server.")
- msgserver = None
- @willie.module.commands("help")
- @willie.module.commands("hilfe")
- @willie.module.commands("man")
- def ffpb_help(bot, trigger):
- """Display commony ulsed functions."""
- functions = {
- "!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
- "!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
- "!highscore": "Höchstwerte von Anzahl Knoten und Clients",
- "!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
- "!link <knoten>": "MAC-Adresse und Link zur Status-Seite des Knotens anzeigen.",
- "!mesh <knoten>": "Zeige Mesh-Partner eines Knotens",
- "!lastseen <knoten>": "Zeitpunkt an dem der Knoten zuletzt gesehen wurde",
- }
- param = trigger.group(2)
- if param is None:
- bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
- return
- if param.startswith("!"):
- param = param[1:]
- for fun in functions.keys():
- if fun.startswith("!" + param + " "):
- bot.say("Hilfe zu '" + fun + "': " + functions[fun])
- return
- bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
- def playitsafe(bot, trigger,
- botadmin=False, admin_channel=False,
- via_channel=False, via_privmsg=False, need_op=False,
- node=None, reply_directly=True,
- debug_user=None, debug_ignorebotadmin=False):
- """
- helper: checks that the triggering user has the necessary rights
- Returns true if everything is okay.
- If it's not, a reply is send via the bot and false is returned.
- """
- if via_channel and via_privmsg:
- raise Exception('Der Entwickler ist ein dummer, dummer Junge. ' +
- '(playitsafe: via_channel && via_privmsg).')
- user = trigger.nick if debug_user is None else debug_user
- user = user.lower()
- # botadmin: you need to be configured as a bot admin
- if botadmin and not trigger.admin:
- if reply_directly:
- bot.say('Du brauchst Super-Kuh-Kräfte um dieses Kommando auszuführen.')
- return False
- # via_channel: the request must not be a private conversation
- if via_channel and trigger.is_privmsg:
- if reply_directly:
- bot.say('Bitte per Channel - mehr Transparenz wagen und so!')
- return False
- # via_privmsg: the request must be a private conversation
- if via_privmsg and not trigger.is_privmsg:
- if reply_directly:
- bot.say('Solche Informationen gibt es nur per PM, ' +
- 'da bin ich ja schon ein klein wenig sensibel ...')
- return False
- # need_op: if the message is in a channel, check that the user has OP there
- if need_op and (not trigger.is_privmsg) and \
- (not user in bot.ops[trigger.sender]):
- if reply_directly:
- bot.say('Keine Zimtschnecke, keine Kekse.')
- return False
- # node: check that the user is whitelisted (or is admin)
- if not node is None and (debug_ignorebotadmin or not trigger.admin):
- acluser = [x for x in nodeaccess if x.lower() == user]
- acluser = acluser[0] if len(acluser) == 1 else None
- if nodeaccess is None or acluser is None:
- if reply_directly:
- bot.reply('You! Shall! Not! Access!')
- return False
- nodeid = node['node_id'] if 'node_id' in node else None
- matched = False
- for x in nodeaccess[acluser]:
- if x == nodeid or fnmatch(node['hostname'], x):
- matched = True
- break
- if not matched:
- if reply_directly:
- bot.reply('Mach das doch bitte auf deinen Knoten, kthxbye.')
- return False
- return True
- @willie.module.commands('nodeacl')
- def ffpb_nodeacl(bot, trigger):
- """Configure ACL for nodes."""
- if not playitsafe(bot, trigger, botadmin=True):
- # the check function already gives a bot reply, just exit here
- return
- # ensure the user gave arguments
- if trigger.group(2) is None or len(trigger.group(2)) == 0:
- bot.say('Sag doch was du willst ... einmal mit Profis arbeiten, ey -.-')
- return
- # read additional arguments
- cmd = trigger.group(3).lower()
- if cmd == 'list':
- user = trigger.group(4)
- if user is None:
- usernames = [x for x in nodeaccess]
- bot.say('ACLs gesetzt für die User: ' + ', '.join(usernames))
- return
- user = user.lower()
- uid = [x for x in nodeaccess if x.lower() == user]
- if len(uid) == 0:
- bot.say('Für \'{0}\' ist keine Node ACL gesetzt.'.format(user))
- return
- bot.say('Node ACL für \'{0}\' = \'{1}\''.format(
- uid[0],
- '\', \''.join(nodeaccess[uid[0]]))
- )
- return
- if cmd in ['add', 'del', 'check']:
- user = trigger.group(4)
- value = trigger.group(5)
- if user is None or value is None:
- bot.say('Du bist eine Pappnase - User und Knoten, bitte.')
- return
- user = str(user)
- print('NodeACL ' + cmd + ' \'' + value + '\' for user \'' + user + '\'')
- uid = [x for x in nodeaccess if x == user or x.lower() == user]
- if cmd == 'add':
- uid = uid[0] if len(uid) > 0 else user
- if not uid in nodeaccess:
- nodeaccess[uid] = []
- if not value in nodeaccess[uid]:
- nodeaccess[uid].append(value)
- bot.say("201 nodeACL '{0}' +'{1}'".format(uid, value))
- else:
- bot.say("304 nodeACL '{0}' contains '{1}'".format(uid, value))
- elif cmd == 'del':
- if len(uid) == 0:
- bot.say("404 nodeACL '{0}'".format(uid))
- return
- if value in nodeaccess[uid]:
- nodeaccess[uid].remove(value)
- bot.say("200 nodeACL '{0}' -'{1}'".format(uid, value))
- else:
- bot.say("404 nodeACL '{0}' has no '{1}'".format(uid, value))
- elif cmd == 'check':
- if len(uid) == 0:
- bot.say('Nope, keine ACL gesetzt.')
- return
- node = ffpb_findnode(value)
- if node is None:
- bot.say('Nope, kein Plan was für ein Knoten das ist.')
- return
- result = playitsafe(bot, trigger,
- debug_user=uid[0], debug_ignorebotadmin=True,
- node=node, reply_directly=False)
- if result is True:
- bot.say('Jupp.')
- elif result is False:
- bot.say('Nope.')
- else:
- bot.say('Huh? result=' + str(result))
- return
- bot.say('Unbekanntes Kommando. Probier ' +
- '"list [user]", "add user value" oder "del user value". ' +
- 'Value kann node_id oder hostname-Maske sein.')
- def ffpb_ensurenodeid(nodedata):
- """Makes sure that the given dict has a 'node_id' field."""
- if 'node_id' in nodedata:
- return nodedata
- # derive node's id
- nodeid = None
- if 'network' in nodedata and 'mac' in nodedata['network']:
- nodeid = nodedata['network']['mac'].replace(':', '')
- # assemble extended data
- result = {'node_id': nodeid}
- for key in nodedata:
- result[key] = nodedata[key]
- return result
- def ffpb_findnode(name, allow_fuzzymatching=True):
- """helper: try to identify the node the user meant by the given name"""
- # no name, no node
- if name is None or len(name) == 0:
- return None
- name = str(name).strip()
- # disable fuzzy matching if name is enclosed in quotes
- if name.startswith('\'') and name.endswith('\'') or \
- name.startswith('"') and name.endswith('"'):
- name = name[1:-1]
- allow_fuzzymatching = False
- # 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 __batcave.get_node(node['id'])
- else:
- nodeid = mac.replace(':', '').lower()
- return {
- 'node_id': nodeid,
- 'hostname': '?-' + nodeid,
- 'mac': mac,
- 'hardware': 'derived-from-mac',
- }
- # 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:
- peer_name = None
- peer_mac = None
- peer_file = None
- for b in peers_repo.heads.master.commit.tree.blobs:
- if b.name.lower() == name.lower():
- peer_name = b.name
- peer_file = b.abspath
- break
- if (not peer_file is None) and os.path.exists(peer_file):
- peerfile = open(peer_file, "r")
- for line in peerfile:
- if line.startswith("# MAC:"):
- peer_mac = line[6:].strip()
- peerfile.close()
- if not peer_mac is None:
- return {
- 'node_id': peer_mac.replace(':', ''),
- 'hostname': peer_name,
- 'mac': peer_mac,
- 'hardware': 'derived-from-vpnkeys',
- }
- # none of the above was able to identify the requested node
- return None
- def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata=True):
- """helper: call ffpb_findnode() and give common answers via bot on error"""
- if name is None or len(name) == 0:
- if not bot is None:
- bot.reply("Grün.")
- return None
- node = ffpb_findnode(name)
- if node is None:
- if not bot is None:
- bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
- return node
- def mac2ipv6(mac, prefix=None):
- """Calculate IPv6 address from given MAC,
- optionally replacing the fe80:: prefix with a given one."""
- result = str(netaddr.EUI(mac).ipv6_link_local())
- if (not prefix is None) and (result.startswith("fe80::")):
- result = prefix + result[6:]
- return result
- def ffpb_notify_newly_seen_nodes(bot, new):
- if not isinstance(bot, dict):
- return
- if len(new) == 0 or bot.memory['ffpb_in_setup']:
- return
- 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:
- 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(300)
- def ffpb_updatepeers(bot):
- """Refresh list of peers and message the diff."""
- if peers_repo is None:
- print('WARNING: peers_repo is None')
- return
- old_head = peers_repo.head.commit
- peers_repo.remotes.origin.pull()
- new_head = peers_repo.head.commit
- if new_head != old_head:
- print('git pull: from ' + str(old_head) + ' to ' + str(new_head))
- added = []
- changed = []
- renamed = []
- deleted = []
- for f in old_head.diff(new_head):
- if f.new_file:
- added.append(f.b_blob.name)
- elif f.deleted_file:
- deleted.append(f.a_blob.name)
- elif f.renamed:
- renamed.append([f.rename_from, f.rename_to])
- else:
- changed.append(f.a_blob.name)
- response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(
- len(added), len(renamed)+len(changed), len(deleted))
- for f in added:
- response += " +'{}'".format(f)
- for f in changed:
- response += " %'{}'".format(f)
- for f in renamed:
- response += " '{}'->'{}'".format(f[0], f[1])
- for f in deleted:
- response += " -'{}'".format(f)
- bot.msg(bot.config.ffpb.msg_target, response)
- @willie.module.interval(15)
- def ffpb_print_cached_messages (bot):
- for msg, m_info in msg_cache.items ():
- if time.time () - m_info['time'] > msg_cache_time:
- sender = _ffpb_aggregate_node_ids (bot, m_info['nodes'])
- bot.msg (bot.config.ffpb.msg_target, "[{0}] {1}".format (sender, msg))
- del msg_cache[msg]
- def _ffpb_aggregate_node_ids (bot, nodes):
- """Aggregate given list of node IDs as far as possbile (read: combine gw*, lnx*, ...)
- and return aggregated and ordered list."""
- aggr_patterns = ('gw', 'lnx')
- nodes_temp = {}
- nodes_aggr = []
- for node in nodes:
- match = re.search ("^([a-z-]+(\d+))(\.([a-z]+)\.?)?.*", node, re.I)
- if match:
- site = match.group (4) if match.group (4) else None
- expr = match.group (1)
- for pattern in aggr_patterns:
- expr = re.sub ("^%s(\d+)" % pattern, "%s##" % pattern, expr)
- if site not in nodes_temp:
- nodes_temp[site] = {}
- if expr not in nodes_temp[site]:
- nodes_temp[site][expr] = []
- nodes_temp[site][expr].append (match.group (2))
- continue
- nodes_aggr.append (node)
- for site, hosts in nodes_temp.items ():
- for node, n_list in hosts.items ():
- if len (n_list) == 1:
- node = re.sub ("##", "%s" % n_list[0], node)
- else:
- node = re.sub ("##", "{%s}" % ",".join (sorted (n_list)), node)
- if site:
- node += "." + site
- nodes_aggr.append (node)
- return ", ".join (sorted (nodes_aggr))
- def ffpb_fetch_stats(bot, url, memoryid):
- """Fetch a ffmap-style nodes.json from the given URL and
- store it in the bot's memory."""
- response = urllib2.urlopen(url)
- data = json.load(response)
- nodes_active = 0
- nodes_total = 0
- clients_count = 0
- for key,node in data['nodes'].iteritems():
- if node['flags']['gateway'] or (node['flags'].has_key('client') and node['flags']['client']):
- continue
- nodes_total += 1
- if node['flags']['online']:
- nodes_active += 1
- if 'legacy' in node['flags'] and node['flags']['legacy']:
- clients_count -= 1
- clients_count += node['statistics']['clients']
- if not memoryid in bot.memory:
- bot.memory[memoryid] = {}
- stats = bot.memory[memoryid]
- stats["fetchtime"] = time.time()
- stats["nodes_active"] = nodes_active
- stats["nodes_total"] = nodes_total
- stats["clients"] = clients_count
- return (nodes_active, nodes_total, clients_count)
- def pretty_date(timestamp=False):
- """
- Get a datetime object or a int() Epoch timestamp and return a
- pretty string like 'an hour ago', 'Yesterday', '3 months ago',
- 'just now', etc
- """
- now = datetime.now()
- compare = None
- if type(timestamp) is int:
- compare = datetime.fromtimestamp(timestamp)
- elif type(timestamp) is float:
- compare = datetime.fromtimestamp(int(timestamp))
- elif isinstance(timestamp, datetime):
- compare = timestamp
- elif not timestamp:
- compare = now
- diff = now - compare
- second_diff = diff.seconds
- day_diff = diff.days
- if day_diff < 0:
- return ''
- if day_diff == 0:
- if second_diff < 10:
- return "gerade eben"
- if second_diff < 60:
- return "vor " + str(second_diff) + " Sekunden"
- if second_diff < 120:
- return "vor einer Minute"
- if second_diff < 3600:
- return "vor " + str(second_diff / 60) + " Minuten"
- if second_diff < 7200:
- return "vor einer Stunde"
- if second_diff < 86400:
- return "vor " + str(second_diff / 3600) + " Stunden"
- if day_diff == 1:
- return "gestern"
- if day_diff < 7:
- return "vor " + str(day_diff) + " Tagen"
- return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr')
- @willie.module.commands('ping')
- def ffpb_ping(bot, trigger=None, target_name=None, reply_directly=True):
- """Ping the given node"""
- # identify node or bail out
- if target_name is None:
- target_name = trigger.group(2)
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return None
- # 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
- print("pinging '{0}' at {1} ...".format(target_name, target))
- result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
- if result == 0:
- print("ping to '{0}' succeeded".format(target_name))
- if reply_directly:
- bot.say('Knoten "' + target_alias + '" antwortet \\o/')
- return True
- elif result == 1 or result == 256:
- print("ping to '{0}' failed".format(target_name))
- if reply_directly:
- bot.say('Keine Antwort von "' + target_alias + '" :-(')
- return False
- else:
- print("ping to '{0}' broken: result='{1}'".format(target_name, result))
- if reply_directly:
- bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
- return None
- @willie.module.commands('mesh')
- def ffpb_nodemesh(bot, trigger):
- """Display mesh partners of the given node."""
- # identify node or bail out
- target_name = trigger.group(2)
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return None
- # derive node's id
- nodeid = node['node_id'] if 'node_id' in node else None
- if nodeid is None:
- msg = 'Mist, ich habe gerade den Zettel verlegt auf dem die Node-ID' + \
- ' von \'{0}\' steht, bitte frag später noch einmal.'
- bot.say(msg.format(node.get('hostname', target_name)))
- return
- # query BATCAVE for node's neighbours (result is a list of MAC addresses)
- cave_result = node.get('neighbours')
- if cave_result is None:
- msg = 'Hm, scheinbar liegen zu \'{0}\' keine Daten vor. ' + \
- 'Klingt komisch, ist aber so.'
- bot.say(msg.format(node.get('hostname', target_name)))
- return
- # query BATCAVE for neighbour's names
- data = '&'.join([str(n) for n in cave_result])
- req = urllib2.urlopen(bot.config.ffpb.batcave_url + 'idmac2name', data)
- # filter out duplicate names
- neighbours = set()
- gateways = set()
- for line in req:
- ident, name = line.strip().split('=')
- if ident == name and ident.startswith('c0:ff:ee:ba:be:'):
- gateways.add('Gateway ' + ident[len('c0:ff:ee:ba:be:'):])
- else:
- neighbours.add(name)
- neighbours = [x for x in neighbours]
- gateways = sorted([x for x in gateways])
- # respond to the user
- reply = node['hostname']
- if len(neighbours) == 0:
- reply += ' hat keinen Mesh-Partner'
- elif len(neighbours) == 1:
- reply += u' mesht mit \'{0}\''.format(neighbours[0])
- else:
- all_except_last = '\', \''.join(neighbours[:-1])
- last = neighbours[-1]
- reply += ' mesht mit \'{0}\' und \'{1}\''.format(all_except_last, last)
- if len(gateways) > 0:
- if len(neighbours) == 0:
- reply += ', aber hat eine Verbindung zu '
- else:
- reply += ' und hat zudem eine Verbindung zu '
- if len(gateways) == 1:
- reply += gateways[0]
- else:
- all_except_last = '\', \''.join(gateways[:-1])
- last = gateways[-1]
- reply += '{0} und {1}'.format(all_except_last, last)
- bot.say(reply)
- @willie.module.commands('exec-on-peer')
- def ffpb_remoteexec(bot, trigger):
- """Remote execution on the given node"""
- bot_params = trigger.group(2).split(' ', 1) if trigger.group(2) is not None else []
- if len(bot_params) != 2:
- bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
- bot.say('Tipp: !exec-on-peer <peer> <cmd>')
- return
- target_name = bot_params[0]
- target_cmd = bot_params[1]
- # identify requested node or bail out
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return
- # check ACL
- if not playitsafe(bot, trigger, via_channel=True, node=node):
- return
- # 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
- cmd = [
- 'ssh',
- '-6n',
- '-l', 'root',
- '-o', 'BatchMode=yes',
- '-o', 'StrictHostKeyChecking=no',
- target,
- target_cmd,
- ]
- print("REMOTE EXEC = " + str(cmd))
- try:
- # call SSH
- result = subprocess.check_output(
- cmd,
- stderr=subprocess.STDOUT,
- shell=False,
- )
- # fetch results and sent at most 8 of them as response
- lines = str(result).splitlines()
- if len(lines) == 0:
- bot.say('exec-on-peer(' + target_alias + '): No output')
- return
- msg = 'exec-on-peer({0}): {1} Zeilen'.format(target_alias, len(lines))
- if len(lines) > 8:
- msg += ' (zeige max. 8)'
- bot.say(msg + ':')
- for line in lines[0:8]:
- bot.say(line)
- except subprocess.CalledProcessError as err:
- bot.say('Fehler {0} bei exec-on-peer({1}): {2}'.format(
- err.returncode,
- target_alias,
- err.output
- ))
- @willie.module.commands('forget-peer-sshhostkey')
- def ffpb_clearsshhostkey(bot, trigger):
- """Forget given node's SSH host key"""
- target_name = trigger.group(2) or ''
- if len(target_name.strip()) == 0:
- bot.say('Lasst uns einfach mal ' + trigger.nick + ' löschen!')
- bot.say('Tipp: !forget-peer-sshhostkey <peer>')
- return
- # identify requested node or bail out
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return
- # check ACL
- if not playitsafe(bot, trigger, via_channel=True, node=node):
- return
- # derive target from node's MAC
- node_mac = node.get('mac')
- target = mac2ipv6(node_mac, 'fdca:ffee:ff12:132:')
- target_alias = node["hostname"]
- # assemble command
- cmd = [
- 'ssh-keygen',
- '-f', '.ssh/known_hosts',
- '-R',
- target,
- ]
- try:
- # call command
- result = subprocess.check_output(
- cmd,
- stderr=subprocess.STDOUT,
- shell=False,
- )
- bot.reply('Ich mache den Weg frei ... also den zu "' + target + '" per SSH zumindest.')
- except subprocess.CalledProcessError as err:
- bot.say('Fehler {0} bei remove-sshhostkey-of-peer({1}): {2}'.format(
- err.returncode,
- target_alias,
- err.output
- ))
|