#!/usr/bin/env python3 import socket import sys import select import time import struct import getpass import asyncio import prompt_toolkit, prompt_toolkit.widgets, prompt_toolkit.layout.containers, prompt_toolkit.application, prompt_toolkit.layout.layout, prompt_toolkit.key_binding, prompt_toolkit.document import collections import random import netifaces maddr = ("ff15::aeae", 44718) def configure_multicast(maddr, ifn): mip, port = maddr haddr = socket.getaddrinfo("::", port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1] maddr = socket.getaddrinfo(mip, port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1] sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if hasattr(socket, "SO_REUSEPORT"): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1) sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 5) ifn = struct.pack("I", ifn) sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifn) group = socket.inet_pton(socket.AF_INET6, mip) + ifn sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group) sock.bind(haddr) sock.setblocking(False) return sock, maddr ta = prompt_toolkit.widgets.TextArea() async def do_inputs(inq, out_callback): output = prompt_toolkit.widgets.TextArea() inp = prompt_toolkit.widgets.TextArea(height=1, prompt="[] ", multiline=False, wrap_lines=False) def accept(buffer): asyncio.get_event_loop().create_task(out_callback(buffer.text)) inp.accept_handler = accept container = prompt_toolkit.layout.containers.HSplit([ output, inp ]) kb = prompt_toolkit.key_binding.KeyBindings() @kb.add("c-c") @kb.add("c-q") def _(event): sys.exit(0) event.app.exit() async def queue_watcher(): while True: text = output.text + await inq.get() + "\n" if text.count("\n") > 100: text = "\n".join(text.split("\n")[1:]) output.buffer.document = prompt_toolkit.document.Document( text=text, cursor_position=len(text) - 1 ) asyncio.get_event_loop().create_task(queue_watcher()) await prompt_toolkit.Application( layout=prompt_toolkit.layout.layout.Layout(container, focused_element=inp), mouse_support=False, full_screen=False, key_bindings=kb ).run_async() loop = asyncio.new_event_loop() TYPE_PING = 0 TYPE_MESSAGE = 1 local_id = random.randint(0, 0xFFFF) def shorthex(x): return "{:04x}".format(x) def encode_packet(ty, nick, content): return struct.pack("!BH16s", ty, local_id, nick.encode("utf-8")) + content.encode("utf-8") def decode_packet(pkt): ty, local_id, nick = struct.unpack("!BH16s", pkt[:19]) content = pkt[19:] return ty, local_id, nick.rstrip(b"\0").decode("utf-8"), content.decode("utf-8") Peer = collections.namedtuple("Peer", ["nick", "timeout_counter"]) async def run(): try: interface = sys.argv[1] addrs = [] except: for x in netifaces.interfaces(): if "lo" not in x and netifaces.AF_INET6 in netifaces.ifaddresses(x): interface = x break if not interface: print("No valid network interface found") sys.exit(1) print("Using", interface) addrs = [a["addr"] for a in netifaces.ifaddresses(interface)[netifaces.AF_INET6]] ifn = socket.if_nametoindex(interface) sock, addr = configure_multicast(maddr, ifn) own_nick = f"{getpass.getuser()}@{socket.gethostname()}"[:16] peers = {} def cb(): buf, remote = sock.recvfrom(2048) ip = remote[0] try: ty, lid, nick, content = decode_packet(buf) if lid == local_id and (addrs == [] or ip in addrs): return peer_id = ip + "/" + shorthex(lid) try: peer = peers[peer_id] if peer.nick != nick: inq.put_nowait(f"! {peer.nick} ({peer_id}) is now {nick}") except KeyError: inq.put_nowait(f"! {nick} ({peer_id}) exists") peers[peer_id] = Peer(nick, 10) if ty == TYPE_MESSAGE: inq.put_nowait(f"{nick}: {content}") except Exception as e: inq.put_nowait(f"! Parse error {e} from {ip}") loop.add_reader(sock, cb) inq = asyncio.Queue() async def outq(s): if s.startswith("/nick "): newnick = s[6:] if len(newnick.encode("utf8")) > 16: inq.put_nowait("! Max nick length is 16 bytes") return inq.put_nowait(f"! You are now {newnick}") nonlocal own_nick own_nick = newnick return elif s.startswith("/peer"): p = [f"{repr(p.nick)} ({i})" for i, p in peers.items()] inq.put_nowait(f"! Peers: {', '.join(p)}") return inq.put_nowait(f"{own_nick}: {s}") sock.sendto(encode_packet(TYPE_MESSAGE, own_nick, s), addr) loop.create_task(do_inputs(inq, outq)) while True: sock.sendto(encode_packet(TYPE_PING, own_nick, ""), addr) rem_queue = [] for peer_id, peer in peers.items(): count = peer.timeout_counter - 1 if count == 0: rem_queue.append(peer_id) inq.put_nowait(f"! {peer.nick} ({peer_id}) no longer exists") else: peers[peer_id] = Peer(nick=peer.nick, timeout_counter=count) for r in rem_queue: del peers[r] await asyncio.sleep(1) if __name__ == "__main__": loop.run_until_complete(run())