Initial commit

This commit is contained in:
SolidLiquid
2025-06-05 20:15:38 +12:00
commit fa1cc5035e
16 changed files with 581 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

118
.gitignore vendored Normal file
View File

@@ -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/

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# Echo-MVP
This is a "minimal" chatroom made in Python. A full version will come later.

69
main.py Normal file
View File

@@ -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/<room_code>')
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/<code>/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)

20
requirements.txt Normal file
View File

@@ -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

BIN
resource/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -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;
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="icon" href="data:,">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% include 'navbar.html' %}
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,63 @@
{% extends 'base.html' %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Hello, hello, hello...</h1>
<p>Yeah, it's early.</p>
<div class="room-join-box">
<input type="text" id="username-input" class="room-input" placeholder="Your name">
</div>
<div class="room-join-box">
<input type="text" id="room-code-input" class="room-input" placeholder="Enter Room Code" maxlength="10">
<button class="room-button" id="join-button">Join</button>
</div>
<div class="room-join-box">
<button class="room-button" id="create-button">Create New Room</button>
</div>
<script>
function getUsername() {
const username = document.getElementById('username-input').value.trim();
if (!username) {
alert("Please enter your name.");
throw new Error("Username required");
}
localStorage.setItem('username', username);
return username;
}
document.getElementById('join-button').addEventListener('click', () => {
try {
getUsername();
const code = document.getElementById('room-code-input').value.trim();
if (code) {
window.location.href = `/room/${code}`;
} else {
alert("Enter a room code.");
}
} catch (_) {}
});
document.getElementById('create-button').addEventListener('click', async () => {
try {
getUsername();
const res = await fetch('/api/room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await res.json();
if (data.code) {
window.location.href = `/room/${data.code}`;
} else {
alert("Failed to create room.");
}
} catch (_) {}
});
</script>
</script>
{% endblock %}

View File

@@ -0,0 +1,5 @@
<div class="navbar">
<a class="navname">Echo</a>
<a href="/">Home</a>
</div>

View File

View File

@@ -0,0 +1,66 @@
{% extends 'room_base.html' %}
{% block title %}Room {{ room_code }}{% endblock %}
{% block content %}
<h1>Room {{ room_code }}</h1>
<div id="chat-box"></div>
<div class="chat-input-container">
<input type="text" id="chat-input" placeholder="Say something..." />
<button id="send-button">Send</button>
</div>
<script>
const socket = io();
const room = "{{ room_code }}";
const name = localStorage.getItem('username') || 'Anonymous';
socket.emit('join', { room, sender: name });
// Fetch message history
fetch(`/api/room/${room}/messages`)
.then(res => res.json())
.then(data => {
const chatBox = document.getElementById('chat-box');
if (data && Array.isArray(data)) {
data.forEach(msg => {
const msgElem = document.createElement('p');
msgElem.textContent = `${msg.sender}: ${msg.text}`;
chatBox.appendChild(msgElem);
});
chatBox.scrollTop = chatBox.scrollHeight;
}
});
// Message listener
socket.on('message', (data) => {
const chatBox = document.getElementById('chat-box');
const msgElem = document.createElement('p');
msgElem.textContent = `${data.sender}: ${data.text}`;
if (data.sender === 'System') {
msgElem.style.fontStyle = 'italic';
msgElem.style.color = 'gray';
}
chatBox.appendChild(msgElem);
chatBox.scrollTop = chatBox.scrollHeight;
});
// Send message
document.getElementById('send-button').onclick = () => {
const text = document.getElementById('chat-input').value;
const sender = localStorage.getItem('username') || 'Anonymous';
if (text.trim()) {
socket.emit('message', {
room,
text,
sender
});
document.getElementById('chat-input').value = '';
}
};
</script>
{% endblock %}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="icon" href="data:,">
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% include 'navbar.html' %}
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>

20
util/room/data.py Normal file
View File

@@ -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

6
util/room/logic.py Normal file
View File

@@ -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))

17
util/room/registry.py Normal file
View File

@@ -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]