now we need that api to exchange p2p connection info
(and yes - doesn’t scale because everyone connects to everyone)
EDIT:
maybe for the interested reader … the used python code it reads/writes to a local file rendezvous.info - inserts its own address/removes it on ctrl+c … if we now replace the file with a scratchpad on autonomi we should have the first p2p live chat solely based on autonomi …
import socket
import threading
import time
import random
import os
from datetime import datetime, timezone
from blessed import Terminal
import fire
RENDEZVOUS_FILE = "rendezvous.info"
term = Terminal()
def get_external_address(udp_socket: socket.socket, server_ip: str, server_port: int) -> tuple[str, int]:
"""
Contacts the server to retrieve the external IP and port.
"""
udp_socket.sendto(b"whoami", (server_ip, server_port))
udp_socket.settimeout(2)
try:
response, _ = udp_socket.recvfrom(1024)
external_ip, external_port = response.decode("utf-8").split(":")
return external_ip, int(external_port)
except socket.timeout:
print("No response from the server.")
return None, None
def update_rendezvous_file(ip: str, port: int, name: str) -> list[tuple[str, int, str]]:
"""
Updates the rendezvous.info file with the new IP/port/name and returns the list of participants.
"""
if not os.path.exists(RENDEZVOUS_FILE):
open(RENDEZVOUS_FILE, "w").close()
participants = []
with open(RENDEZVOUS_FILE, "r") as f:
for line in f:
p_ip, p_port, p_name = line.strip().split(":")
participants.append((p_ip, int(p_port), p_name))
# Add own address if not already present
if (ip, port, name) not in participants:
with open(RENDEZVOUS_FILE, "a") as f:
f.write(f"{ip}:{port}:{name}\n")
# Remove own address from the participants list
participants = [p for p in participants if not (p[0] == ip and p[1] == port)]
return participants
def remove_self_from_rendezvous(ip: str, port: int, name: str):
"""
Removes the user's contact information from rendezvous.info.
"""
if not os.path.exists(RENDEZVOUS_FILE):
return
with open(RENDEZVOUS_FILE, "r") as f:
lines = f.readlines()
with open(RENDEZVOUS_FILE, "w") as f:
for line in lines:
if line.strip() != f"{ip}:{port}:{name}":
f.write(line)
def watch_rendezvous_file(update_event):
"""
Watches the rendezvous.info file for changes and triggers an update event.
"""
last_modified = os.path.getmtime(RENDEZVOUS_FILE)
while True:
time.sleep(1)
try:
current_modified = os.path.getmtime(RENDEZVOUS_FILE)
if current_modified != last_modified:
last_modified = current_modified
update_event.set()
except FileNotFoundError:
continue
def send_message(socket: socket.socket, message: str, recipient: tuple[str, int], name: str):
"""
Sends a message with a random ID, UTC timestamp, and sender name to a recipient.
"""
msg_id = random.randint(1000, 9999)
utc_time = datetime.now(timezone.utc).isoformat()
packet = f"{msg_id}|{utc_time}|{name}|{message}".encode("utf-8")
socket.sendto(packet, recipient)
def render_chat(chat_log: list[str], input_prompt: str):
"""
Renders the chat window and input prompt.
"""
with term.location(0, 0):
print(term.clear, end="")
for line in chat_log[-(term.height - 2):]:
print(line)
print(term.move_xy(0, term.height - 1) + input_prompt, end="", flush=True)
def main(local_port: int, name: str, local_ip: str = None,
discovery_ip: str = "185.198.27.221", discovery_port: int = 59595):
"""
Starts the P2P chat client.
Args:
local_port (int): The local port to use.
name (str): The name of the participant.
local_ip (str): The local IP address. If set, no discovery server is used.
discovery_ip (str): The IP of the discovery server.
discovery_port (int): The port of the discovery server.
"""
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(("0.0.0.0", local_port))
if local_ip:
external_ip, external_port = local_ip, local_port
else:
external_ip, external_port = get_external_address(udp_socket, discovery_ip, discovery_port)
if not external_ip or not external_port:
print("Error: Could not retrieve external address.")
return
participants = update_rendezvous_file(external_ip, external_port, name)
print(f"Participants in the network: {participants}")
chat_log = []
running = True
input_prompt = ">>> "
input_lock = threading.Lock()
update_event = threading.Event()
def receive_messages():
while running:
try:
data, addr = udp_socket.recvfrom(1024)
msg_id, utc_time, sender_name, message = data.decode("utf-8").split("|", 3)
if message == "heartbeat":
continue # Ignore heartbeats
local_time = datetime.fromisoformat(utc_time).astimezone().strftime("%Y-%m-%d %H:%M:%S")
with input_lock:
chat_log.append(f"[{local_time}] {sender_name}: {message}")
except Exception:
continue
def heartbeat():
while running:
for participant in participants:
send_message(udp_socket, "heartbeat", (participant[0], participant[1]), name)
time.sleep(2)
def auto_render():
while running:
with input_lock:
render_chat(chat_log, input_prompt)
time.sleep(0.1)
def reload_participants():
while running:
update_event.wait()
if not running:
break
with input_lock:
participants.clear()
participants.extend(update_rendezvous_file(external_ip, external_port, name))
update_event.clear()
threading.Thread(target=receive_messages, daemon=True).start()
threading.Thread(target=heartbeat, daemon=True).start()
threading.Thread(target=auto_render, daemon=True).start()
threading.Thread(target=watch_rendezvous_file, args=(update_event,), daemon=True).start()
threading.Thread(target=reload_participants, daemon=True).start()
try:
with term.cbreak(), term.hidden_cursor():
while running:
user_input = term.inkey(timeout=0.1)
if user_input.name == "KEY_ENTER":
with input_lock:
if input_prompt.strip() != ">>> ":
for participant in participants:
send_message(udp_socket, input_prompt[4:], (participant[0], participant[1]), name)
local_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
chat_log.append(f"[{local_time}] {name}: {input_prompt[4:]}")
input_prompt = ">>> "
elif user_input.name == "KEY_BACKSPACE":
with input_lock:
input_prompt = input_prompt[:-1] if len(input_prompt) > 4 else input_prompt
elif user_input:
with input_lock:
input_prompt += user_input
except KeyboardInterrupt:
print("\nExiting chat...")
remove_self_from_rendezvous(external_ip, external_port, name)
running = False
finally:
udp_socket.close()
if __name__ == "__main__":
fire.Fire(main)
if you start it with local ip info it will use that (well … it would need to be a global ip actually xD … anyway)
python p2p_chat.py --local_port=12345 --local_ip=192.168.0.100 --name="Alice"
if you don’t pass the ip info it queries it from an external server
python p2p_chat.py --local_port=12345 --name="Alice"
it uses heartbeating to keep the hole-punched communication channel open and just fires to all other participants.
the local file rendezvous.info must have lines like:
77.12.239.17:13131:somerandomdude
that identify the other communication partners