mirror of
https://github.com/osmarks/random-stuff
synced 2024-12-27 10:30:35 +00:00
172 lines
5.7 KiB
Python
Executable File
172 lines
5.7 KiB
Python
Executable File
#!/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()) |