diff --git a/.gitignore b/.gitignore index 4512867..6766675 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ __pycache__/** .env +cache.py +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/client.py b/client.py index e69de29..49b4993 100644 --- a/client.py +++ b/client.py @@ -0,0 +1,24 @@ +import asyncio +import firepup650 as fp + +rInput = fp.replitInput +fp.replitCursor = fp.bcolors.REPLIT + "> " + fp.bcolors.RESET + + +async def client(host, port): + reader, writer = await asyncio.open_connection(host, port) + + print(f"Send: {message!r}") + writer.write(message.encode()) + await writer.drain() + + data = await reader.read(100) + print(f"Received: {data.decode()!r}") + + print("Close the connection") + writer.close() + await writer.wait_closed() + + +host, port = (rInput("Hostname"), rInput("Port (default is 65048)")) +asyncio.run(tcp_echo_client("Hello World!")) diff --git a/logs.py b/logs.py new file mode 100644 index 0000000..0e57c8d --- /dev/null +++ b/logs.py @@ -0,0 +1,40 @@ +#!/usr/bin/python3 +from datetime import datetime as dt +from sys import stdout, stderr +from typing import Union + + +def log( + message: str, + level: str = "LOG", + origin: str = None, + time: Union[dt, str] = "now", +) -> bytes: + if level in ["EXIT", "CRASH", "FATAL", "ERROR"]: + stream = stderr + else: + stream = stdout + if time == "now": + dtime = dt.now() + elif type(time) == str: + raise ValueError('Only "now" is an accepted string argument for time') + elif type(time) == dt: + dtime = time + else: + raise ValueError("time must either be a string or a dt object") + time = dtime.strftime("%d-%m-%Y %H:%M:%S") + log = "" + if not "\n" in message: + log = f"[{level}]{'['+origin+']' if origin else ''}[{time}] {message}" + print( + f"[{level}]{'['+origin+']' if origin else ''}[{time}] {message}", + file=stream, + ) + else: + for line in message.split("\n"): + log = log + f"\n[{level}]{'['+origin+']' if origin else ''}[{time}] {line}" + print( + f"[{level}]{'['+origin+']' if origin else ''}[{time}] {line}", + file=stream, + ) + return (log[5:] + "\n").encode("utf8") diff --git a/pyproject.toml b/pyproject.toml index d30c0ba..99c1216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "talk" +name = "python-talk" version = "0.0.1" description = "A talk/talkd like program" authors = ["Firepup Sixfifty "] diff --git a/server.py b/server.py index e69de29..7e17f58 100644 --- a/server.py +++ b/server.py @@ -0,0 +1,154 @@ +import os, sys, asyncio, re, signal +from firepup650 import console +from logs import log + + +class Globals: ... + + +G = Globals() +G.uniqueClients = 0 +G.linked = [] +port = 65048 +G.msgs = [] +G.remoteID = "firepi" +G.event = asyncio.Event() +G.loop = asyncio.get_event_loop() +G.interruptCount = 0 +# Try to load a message log, if one exists +try: + G.msgs = __import__("cache").msgs + log(f"Got {len(G.msgs)} lines from message cache") +except ImportError: + log("No server message cache.", level="WARN") +except Exception: + log( + "Abnormal state! Failed to load cache even though the file exists!", + level="WARN", + ) +try: + for arg in sys.argv: + if arg.startswith("--port"): + port = int(arg.lstrip("-port=")) + elif arg.startswith("-p"): + port = int(arg.lstrip("-p=")) + elif arg in ["-n", "--no-cache"]: + log("Explicitly erasing cached messages") + G.msgs = [] + elif arg in ["-?", "-h", "--help"]: + print("TODO: Help menu soon") + exit(0) +except Exception: + sys.tracebacklimit = 0 + raise ValueError("Invalid arguments. Please refer to -? for usage.") from None + + +async def handle_client(reader, writer): + try: + global G + writer.write(b"Pleass identify yourself. Nick limit is 20 chars.\n") + await writer.drain() + name = repr((await reader.read(20)).decode("utf8").strip())[1:-1].replace( + "\\\\", "\\" + ) + if len(name) > 20: + name = name[ + :19 + ] # Really this is only possible if someone passes raw unicode as a nick, but let's clean it up anyways. + try: + await asyncio.wait_for( + reader.read(), 0.01 + ) # Silently consume the excess useename data + except asyncio.TimeoutError: + pass + if not name: + writer.write(b"Nice try. Actually set a nick.\n") + await writer.drain() + writer.close() + await writer.wait_closed() + return + if name in G.linked: + writer.write(f"Nick ({name}) in use\n".encode("utf8")) + await writer.drain() + writer.close() + await writer.wait_closed() + return + G.linked.extend([name]) + msgIndex = 0 + G.uniqueClients += 1 + G.msgs.extend([log(f"{name} has connected to the server.")]) + while 1: + try: + buffer = await asyncio.wait_for(reader.read(970), 0.1) + request = repr(buffer.decode("utf8").strip())[1:-1].replace( + "\\\\", "\\" + ) + response = None + if request.startswith("/mes "): + response = log(f"* {name}'s {request[4:]}") + elif request.startswith("/me "): + response = log(f"* {name} {request[3:]}") + elif request.startswith("/h"): + writer.write(b"TODO: Command listing\n") + await writer.drain() + elif request.startswith("/quit"): + break + elif request: + response = log(f" {name}: {request}") + if response: + G.msgs.extend([response]) + except asyncio.TimeoutError: + pass + if msgIndex < len(G.msgs): + writer.writelines(G.msgs[msgIndex:]) + msgIndex = len(G.msgs) + await writer.drain() + writer.close() + await writer.wait_closed() + G.msgs.extend([log(f"{name} has disconnected from the server.")]) + G.linked.remove(name) + except ConnectionResetError: + G.msgs.extend([log(f"{name} has disconnected from the server.")]) + G.linked.remove(name) + + +async def run_server(port): + global G + server = await asyncio.start_server(handle_client, "0.0.0.0", port) + log(f"Listening on port {port}...") + G.msgs.extend([log("Server startup")]) + crash = False + try: + log("Waiting on the Interrupt Event to be set...") + await G.event.wait() + log("Interrupt Event has been set, shutting down normally") + except Exception: + crash = True + G.msgs.extend([log("Server crash", level="FATAL")[1:]]) + log("Shutting down from Exception") + # TODO: Add format_exc here + finally: + if not crash: + G.msgs.extend([log("Server shutdown")]) + log("Kicking all clients as we go down") + server.close() + # server.abort_clients() + with open("cache.py", "w") as cache: + cache.write(f"msgs = {G.msgs}\n") + log("Saved logs, exiting now.") + + +class ServerInterruptException(KeyboardInterrupt): ... + + +def interruptCatch(s, f): + global G + print() + G.loop.call_soon_threadsafe(G.event.set) + G.interruptCount += 1 + sys.tracebacklimit = 0 + raise ServerInterruptException from None + + +signal.signal(signal.SIGINT, interruptCatch) +asyncio.run(run_server(port))