commit fa1cc5035ef5101dfaadf23902effce34dc984d3 Author: SolidLiquid Date: Thu Jun 5 20:15:38 2025 +1200 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..e155daa Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fd7d36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +## Generated by ChatGPT +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +pipenv.lock + +# poetry +poetry.lock + +# virtualenv +venv/ +ENV/ +env/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c6a7c3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Echo-MVP + +This is a "minimal" chatroom made in Python. A full version will come later. \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..974cd2d --- /dev/null +++ b/main.py @@ -0,0 +1,69 @@ +from flask_socketio import SocketIO, emit, join_room +from flask import Flask, render_template, redirect, request, url_for, jsonify + +from util.room.registry import RoomRegistry +from util.room.logic import generate_room_code + +ROOMS = set() + +app = Flask( + __name__, + template_folder="resource/template", + static_folder="resource/static" +) +socketio = SocketIO(app) + +registry = RoomRegistry() + +@app.route('/') +def index(): + return render_template("home.html") + +@app.route('/room/') +def room(room_code): + return render_template('room.html', room_code=room_code) + +@socketio.on('message') +def handle_message(data): + room_code = data['room'] + sender = data['sender'] + text = data['text'] + + room = registry.get_room(room_code) + if room: + room.add_message(sender, text) + emit('message', {'sender': sender, 'text': text}, to=room_code) + + +@app.route('/api/room', methods=['POST']) +def create_room(): + data = request.json + code = generate_room_code() + password = data.get("password") + room = registry.create_room(code, password) + return jsonify({"code": room.code}) + +@app.route('/api/room//messages') +def get_messages(code): + room = registry.get_room(code) + if not room: + return jsonify({"error": "Room not found"}), 404 + return jsonify(room.messages) + + +@socketio.on('join') +def handle_join(data): + room = data['room'] + sender = data.get('sender', 'Anonymous') + join_room(room) + + emit('message', { + 'sender': 'System', + 'text': f'{sender} has joined the room.' + }, to=room) + + + +if __name__ == '__main__': + socketio.run(app, debug=False, host='0.0.0.0', port=8080) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d5cb5a2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +bidict==0.23.1 +blinker==1.9.0 +click==8.1.7 +dnspython==2.7.0 +eventlet==0.40.0 +Flask==3.1.0 +Flask-SocketIO==5.5.1 +greenlet==3.2.2 +gunicorn==23.0.0 +h11==0.16.0 +itsdangerous==2.2.0 +Jinja2==3.1.4 +jsonify==0.5 +MarkupSafe==3.0.2 +packaging==25.0 +python-engineio==4.12.2 +python-socketio==5.13.0 +simple-websocket==1.1.0 +Werkzeug==3.1.3 +wsproto==1.2.0 diff --git a/resource/.DS_Store b/resource/.DS_Store new file mode 100644 index 0000000..f649f70 Binary files /dev/null and b/resource/.DS_Store differ diff --git a/resource/static/css/styles.css b/resource/static/css/styles.css new file mode 100644 index 0000000..b287c65 --- /dev/null +++ b/resource/static/css/styles.css @@ -0,0 +1,156 @@ +:root { + --primary-color: rgb(243, 243, 243); + --secondary-color: rgb(7, 190, 187); + --tertiary-color: rgb(250, 250, 250); + --text-color: rgb(62, 62, 62); + --text-highlight-color: white; +} + +/* Navigation */ +.navbar { + position: fixed; + top:0px; + left:0px; + right:0px; + background-color: var(--tertiary-color); + border-bottom: 1px solid var(--secondary-color); /* Changed color to lightsteelblue */ + overflow: hidden; + z-index: 1000; +} + +.navbar a { + float: left; + display: block; + text-align: center; + padding: 8px 16px; + text-decoration: none; + transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; /* Only transition the background-color */ + color: var(--text-color); /* Set default text color */ +} + +.navbar a:hover { + background-color: var(--secondary-color); + color: var(--text-highlight-color); /* Change text color on hover for better contrast */ + padding: 8px 20px; /* Increase padding on hover */ +} + +.navname{ + font-weight: bolder; + user-select: none; +} + +.navname a{ + background-color: var(--primary-color) !important; +} + +/* Body */ +body { + background-color: var(--primary-color); + margin: 0px; + color: var(--text-color); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* Container */ +.container { + padding: 35px 10px; + margin: auto; + max-width: 800px; +} + +.room-join-box { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.room-input { + padding: 8px 12px; + border: 1px solid var(--secondary-color); + border-radius: 5px; + background-color: var(--tertiary-color); + color: var(--text-color); + font-size: 16px; + outline: none; + transition: padding 0.15s ease, border-color 0.15s ease; + flex: 1; +} + +.room-input:focus { + border-color: var(--secondary-color); + padding-right: 14px; +} + +.room-button { + padding: 8px 16px; + background-color: var(--tertiary-color); + border: 1px solid var(--secondary-color); + border-radius: 5px; + color: var(--text-color); + font-size: 16px; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; +} + +.room-button:hover { + background-color: var(--secondary-color); + color: var(--text-highlight-color); + padding-left: 18px; + padding-right: 18px; +} + +#chat-box { + flex: 1; /* This is the key: allows chat-box to grow and fill vertical space */ + overflow-y: auto; /* Adds a scrollbar when content overflows vertically */ + padding: 10px; + border: 1px solid var(--secondary-color); /* Visual border for chat box */ + border-radius: 5px; + margin-bottom: 10px; /* Space between chat box and input */ + background-color: var(--tertiary-color); /* Example background for messages */ + color: var(--text-color); + display: flex; + flex-direction: column; +} + +#chat-box p { + margin: 0 0 5px 0; /* Adjust message spacing */ + line-height: 1.4; + word-wrap: break-word; /* Ensures long words break and wrap */ +} + +.chat-input-container { + display: flex; + width: 100%; + gap: 10px; +} + +#chat-input { + padding: 8px 12px; + border: 1px solid var(--secondary-color); + border-radius: 5px; + background-color: var(--tertiary-color); + color: var(--text-color); + font-size: 16px; + outline: none; + transition: padding 0.15s ease, border-color 0.15s ease; + flex: 1; /* Makes the input take up available horizontal space */ +} + +#send-button { + padding: 8px 16px; + background-color: var(--tertiary-color); + border: 1px solid var(--secondary-color); + border-radius: 5px; + color: var(--text-color); + font-size: 16px; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease, padding 0.15s ease; + flex-shrink: 0; /* Prevents the button from shrinking */ +} + +#send-button:hover { + background-color: var(--secondary-color); + color: var(--text-highlight-color); + padding-left: 18px; + padding-right: 18px; +} \ No newline at end of file diff --git a/resource/template/base.html b/resource/template/base.html new file mode 100644 index 0000000..e041998 --- /dev/null +++ b/resource/template/base.html @@ -0,0 +1,18 @@ + + + + + + + + {% block title %}{% endblock %} + + + {% include 'navbar.html' %} + +
+ {% block content %} + {% endblock %} +
+ + diff --git a/resource/template/home.html b/resource/template/home.html new file mode 100644 index 0000000..095ab74 --- /dev/null +++ b/resource/template/home.html @@ -0,0 +1,63 @@ +{% extends 'base.html' %} +{% block title %}Home{% endblock %} +{% block content %} +

Hello, hello, hello...

+

Yeah, it's early.

+
+ +
+ +
+ + +
+ +
+ +
+ + + + + + +{% endblock %} diff --git a/resource/template/navbar.html b/resource/template/navbar.html new file mode 100644 index 0000000..82cec73 --- /dev/null +++ b/resource/template/navbar.html @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/resource/template/options.html b/resource/template/options.html new file mode 100644 index 0000000..e69de29 diff --git a/resource/template/room.html b/resource/template/room.html new file mode 100644 index 0000000..2adf1d6 --- /dev/null +++ b/resource/template/room.html @@ -0,0 +1,66 @@ +{% extends 'room_base.html' %} +{% block title %}Room {{ room_code }}{% endblock %} +{% block content %} +

Room {{ room_code }}

+
+ +
+ + +
+ + +{% endblock %} diff --git a/resource/template/room_base.html b/resource/template/room_base.html new file mode 100644 index 0000000..1892e81 --- /dev/null +++ b/resource/template/room_base.html @@ -0,0 +1,20 @@ + + + + + + + + + + {% block title %}{% endblock %} + + + {% include 'navbar.html' %} + +
+ {% block content %} + {% endblock %} +
+ + diff --git a/util/room/data.py b/util/room/data.py new file mode 100644 index 0000000..b41631a --- /dev/null +++ b/util/room/data.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass, field +from typing import List, Dict +from datetime import datetime + +@dataclass +class Room: + code: str + created_at: datetime = field(default_factory=datetime.utcnow) + messages: List[Dict] = field(default_factory=list) # Could include sender, timestamp, etc. + password: str = None # Optional + + def add_message(self, sender: str, text: str): + self.messages.append({ + "sender": sender, + "text": text, + "timestamp": datetime.now().isoformat() + }) + + def check_password(self, attempt: str) -> bool: + return self.password is None or self.password == attempt diff --git a/util/room/logic.py b/util/room/logic.py new file mode 100644 index 0000000..6b014bb --- /dev/null +++ b/util/room/logic.py @@ -0,0 +1,6 @@ +import secrets +import string + +def generate_room_code(length=10): + chars = string.ascii_letters + string.digits + return ''.join(secrets.choice(chars) for _ in range(length)) diff --git a/util/room/registry.py b/util/room/registry.py new file mode 100644 index 0000000..4b02ea0 --- /dev/null +++ b/util/room/registry.py @@ -0,0 +1,17 @@ +from util.room.data import Room + +class RoomRegistry: + def __init__(self): + self.rooms: dict[str, Room] = {} + + def create_room(self, code: str, password: str = None) -> Room: + room = Room(code=code, password=password) + self.rooms[code] = room + return room + + def get_room(self, code: str) -> Room: + return self.rooms.get(code) + + def delete_room(self, code: str): + if code in self.rooms: + del self.rooms[code]