#!/usr/bin/env python3 libexec_dir = __LIBEXECDIR__ import enum import select import subprocess import sys import time class statuses(enum.Enum): available = 0 unavailable = 1 offline = 2 own_nick = None own_status = None default_target_mac = None peers = {} class Peer: def __init__(self, status, nick, lastseen): self.status = status self.nick = nick self.lastseen = lastseen def __repr__(self): r = 'Peer(%s, %s, %s)' % (repr(self.status), repr(self.nick), repr(self.lastseen)) if __name__ != '__main__': return '%s.%s' % (__name__, r) else: return r def writeall(f, b): written = 0 while written < len(b): written += f.write(b[written:]) def readall(f, length): read = bytearray() while len(read) < length: data = f.read(length - len(read)) if data == b'': raise ConnectionError('Could not satisfy read of %i bytes' % length) read.extend(data) return bytes(read) def readall_u16(f): u16_bytes = readall(f, 2) return (u16_bytes[0] << 8) | u16_bytes[1] def parse_mac(text): parts = text.split(':') if len(parts) != 6: raise ValueError('Invalid MAC format: %s' % text) try: parsed = bytes(int(field, 16) for field in parts) except ValueError: raise ValueError('Invalid MAC format %s' % text) return parsed def format_mac(mac): return ':'.join(mac[i:i+1].hex() for i in range(len(mac))) def format_status(status): if status == 0: return 'available' elif status == 1: return 'unavailable' elif status == 2: return 'offline' else: raise ValueError('Unknown status %i' % status) def send_message(backend, mac, message): encoded = message.encode('utf-8') writeall(backend, b'm' + mac + bytes([len(encoded) >> 8, len(encoded) & 0xff]) + encoded) def send_status_request(backend, mac): writeall(backend, b'r' + mac) def set_status_nick(backend, status, nick): encoded = nick.encode('utf-8') writeall(backend, b's' + bytes([status.value, len(encoded)]) + encoded) def handle_user_command(backend, line): global own_nick, own_status global default_target_mac if len(line) > 0 and line[0] == '/': command, _, rest = line.partition(' ') if command == '/': # Send quoted message if default_target_mac is None: print('--- Default target not set, set with /target') else: send_message(backend, default_target_mac, rest) elif command == '/msg': # Send message to target mac_str, _, message = rest.partition(' ') mac = parse_mac(mac_str) send_message(backend, mac, message) elif command == '/status': # Request status mac = parse_mac(rest) send_status_request(backend, mac) elif command == '/available' and rest == '': own_status = statuses.available set_status_nick(backend, own_status, own_nick) elif command == '/unavailable' and rest == '': own_status = statuses.unavailable set_status_nick(backend, own_status, own_nick) elif command == '/nick': own_nick = rest set_status_nick(backend, own_status, own_nick) elif command == '/target': default_target_mac = parse_mac(rest) else: # Display usage print('--- / ; /msg ; /status ; /available; /unavailable; /nick ; /target ') else: # Send message if default_target_mac is None: print('--- Default target not set, set with /target') else: send_message(backend, default_target_mac, line) def handle_status(mac, status, nick): global peers if mac not in peers: # Never seen before peers[mac] = Peer(nick = None, status = None, lastseen = None) peers[mac].lastseen = time.monotonic() if peers[mac].nick is not None and peers[mac].status != statuses.offline and nick != peers[mac].nick: print('=== ~%s -> ~%s [%s]' % (peers[mac].nick, nick, format_mac(mac))) peers[mac].nick = nick if status != peers[mac].status: if status == statuses.offline: print('<<< ~%s [%s]' % (nick, format_mac(mac))) elif peers[mac].status is None or peers[mac].status == statuses.offline: if status == statuses.available: print('>>> ~%s [%s]' % (nick, format_mac(mac))) else: print('>>> ~%s (%s) [%s]' % (nick, status.name, format_mac(mac))) else: print('=== ~%s (%s) [%s]' % (nick, status.name, format_mac(mac))) peers[mac].status = status def handle_message(mac, message): global peers if mac not in peers: nick = format_mac(mac) else: nick = peers[mac].nick # Ensure nicks are unique unique = True for peer_mac, peer in peers.items(): if peer_mac == mac: continue if peer.nick == nick: # Nick not unique unique = False break if unique: # Unique nicks: ~nick nick = '~' + nick else: # Non-unique nicks: [MAC]~nick nick = '[%s]~%s' % (format_mac(mac), nick) for line in message.split('\n'): print('<%s> %s' % (nick, ascii(line))) def eventloop(proc): # Create unbuffered version of stdin and close the old one as we # won't need it anymore unbuf_stdin = open(sys.stdin.buffer.fileno(), 'rb', buffering = 0) sys.stdin.close() # Set up a poll for inputs (but do output blockingly) poll = select.poll() poll.register(proc.stdout, select.POLLIN) poll.register(unbuf_stdin, select.POLLIN) input_buffer = bytearray() running = True while running: for fd, event in poll.poll(): if fd == proc.stdout.fileno() and event & select.POLLIN: event_type = readall(proc.stdout, 1) if event_type == b's': # Status source_mac = readall(proc.stdout, 6) status, = readall(proc.stdout, 1) nick_length, = readall(proc.stdout, 1) nick = readall(proc.stdout, nick_length,) handle_status(source_mac, statuses(status), nick.decode('utf-8')) elif event_type == b'i': # Msgid for message msgid = readall_u16(proc.stdout) print('(msgid: %i)' % msgid) #debg elif event_type == b'I': # Failed to get msgid for message print('(msgid fail)') #debg elif event_type == b'a': # ACK received source_mac = readall(proc.stdout, 6) msgid = readall_u16(proc.stdout) print('(ack: %s %i)' % (format_mac(source_mac), msgid)) #debg elif event_type == b'A': # ACK not received (and message send failed) print('(ack failed)') #debg elif event_type == b'm': # Message received source_mac = readall(proc.stdout, 6) message_length = readall_u16(proc.stdout) message = readall(proc.stdout, message_length) handle_message(source_mac, message.decode('utf-8')) else: raise ValueError('Unknown event type from backend: %s' % repr(event_type)) elif fd == proc.stdout.fileno() and event & select.POLLHUP: print('Backend exited') running = False elif fd == unbuf_stdin.fileno() and event & select.POLLIN: data = unbuf_stdin.read(1024) input_buffer.extend(data) while True: newline_location = input_buffer.find(b'\n') if newline_location == -1: break line, _, input_buffer = input_buffer.partition(b'\n') handle_user_command(proc.stdin, line.decode('utf-8')) if data == b'': # ^D writeall(proc.stdin, b'q') running = False else: raise Exception('Unreachable') def main(): global own_nick, own_status _, interface, own_nick = sys.argv proc = subprocess.Popen(['sudo', libexec_dir + '/ethermess-backend', interface], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = sys.stderr, bufsize = 0) # Tell the backend the status and nick own_status = statuses.available encoded = own_nick.encode('utf-8') writeall(proc.stdin, bytes([own_status.value, len(encoded)]) + encoded) # Read our MAC mac = readall(proc.stdout, 6) print('--- MAC: %s' % format_mac(mac)) eventloop(proc) proc.wait() if __name__ == '__main__': main()