Initial commit.

This commit is contained in:
retoor 2024-12-05 19:34:58 +01:00
commit 40b8bd42fd
18 changed files with 460 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.vscode
rchat.db*
config.py
.venv
.history
__pycache__

28
Makefile Normal file
View File

@ -0,0 +1,28 @@
BIN = ./.venv/bin/
PYTHON = ./.venv/bin/python
PIP = ./.venv/bin/pip
APP_NAME=rchat
all: ensure_repo ensure_env format install build
ensure_repo:
-@git init
ensure_env:
-@python3 -m venv .venv
install:
$(PIP) install -e .
format:
$(PIP) install shed
. $(BIN)/activate && shed
build: format install
$(PIP) install build
$(PYTHON) -m build
run:
$(BIN)rchat.serve

BIN
dist/ragnar-1.3.37.tar.gz vendored Normal file

Binary file not shown.

BIN
dist/rchat-1.0.0-py3-none-any.whl vendored Normal file

Binary file not shown.

BIN
dist/rchat-1.0.0.tar.gz vendored Normal file

Binary file not shown.

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

28
setup.cfg Normal file
View File

@ -0,0 +1,28 @@
[metadata]
name = rchat
version = 1.0.0
description = rchat
author = retoor
author_email = retoor@molodetz.nl
license = MIT
long_description = file: README.md
long_description_content_type = text/markdown
[options]
packages = find:
package_dir =
= src
python_requires = >=3.7
install_requires =
aiohttp
faker
dataset
app @ git+https://retoor.molodetz.nl/retoor/app.git
mololog @ git+https://retoor.molodetz.nl/retoor/mololog.git
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
rchat.serve = rchat.__main__:main

View File

@ -0,0 +1,14 @@
Metadata-Version: 2.1
Name: rchat
Version: 1.0.0
Summary: rchat
Author: retoor
Author-email: retoor@molodetz.nl
License: MIT
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: aiohttp
Requires-Dist: faker
Requires-Dist: dataset
Requires-Dist: app@ git+https://retoor.molodetz.nl/retoor/app.git
Requires-Dist: mololog@ git+https://retoor.molodetz.nl/retoor/mololog.git

View File

@ -0,0 +1,12 @@
pyproject.toml
setup.cfg
src/rchat/__init__.py
src/rchat/__main__.py
src/rchat/app.py
src/rchat/faker.py
src/rchat.egg-info/PKG-INFO
src/rchat.egg-info/SOURCES.txt
src/rchat.egg-info/dependency_links.txt
src/rchat.egg-info/entry_points.txt
src/rchat.egg-info/requires.txt
src/rchat.egg-info/top_level.txt

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,2 @@
[console_scripts]
rchat.serve = rchat.__main__:main

View File

@ -0,0 +1,5 @@
aiohttp
faker
dataset
app@ git+https://retoor.molodetz.nl/retoor/app.git
mololog@ git+https://retoor.molodetz.nl/retoor/mololog.git

View File

@ -0,0 +1 @@
rchat

5
src/rchat/__init__.py Normal file
View File

@ -0,0 +1,5 @@
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)

14
src/rchat/__main__.py Normal file
View File

@ -0,0 +1,14 @@
from aiohttp import web
from rchat.app import create_app
def main():
app = create_app()
web.run_app(app, port=8080)
# Run the server
if __name__ == "__main__":
main()
# static/index.html

134
src/rchat/app.py Normal file
View File

@ -0,0 +1,134 @@
# server.py
import json
import pathlib
import uuid
import aiohttp
from aiohttp import web
from app.app import Application as BaseApplication
from mololog.client import patch
from rchat.faker import fake
patch("https://mololog.molodetz.nl/")
class Application(BaseApplication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.clients = {}
self.sessions = {}
self.base_folder = pathlib.Path(__file__).parent
self.static_folder = self.base_folder.joinpath("static")
self.router.add_get("/ws", self.websocket_handler)
self.router.add_get("/", self.index_handler)
self.router.add_static("/", path=self.static_folder, name="static")
async def index_handler(self, request):
content = self.static_folder.joinpath("index.html")
return web.Response(body=content.read_text(), content_type="text/html")
async def websocket_handler(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
session = request.session
if not 'username' in session:
session['username'] = fake.name().split(" ")[0]
session['uid'] = str(uuid.uuid4())
session['session_id'] = session['uid']
username = session["username"]
session_id = session["session_id"]
uid = session["uid"]
response = ws
response.set_cookie(
samesite=True,
name="rchat_session",
value=session_id,
max_age=365 * 24 * 60 * 60,
httponly=True,
)
try:
for message in await self.find("messages", {}):
await ws.send_json(message)
await ws.send_json(
{
"type": "system",
"message": f'Welcome, your name is {session["username"]}.',
}
)
await self.broadcast(
{"type": "system", "message": f"{username} joined the chat."}
)
self.clients[uid] = ws
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
try:
data = json.loads(msg.data)
message = data.get("message")
if message:
if message.startswith("/nick "):
original_name = session["username"]
session["username"] = message[len("/nick ") :]
await self.broadcast(
{
"type": "system",
"message": f'{original_name} is renamed to {session["username"]}.',
}
)
continue
await self.broadcast(
{
"type": "chat",
"sender": session["username"],
"message": data.get("message", ""),
}
)
except json.JSONDecodeError:
await ws.send_json(
{"type": "error", "message": "Invalid message format."}
)
elif msg.type == aiohttp.WSMsgType.ERROR:
print(
f"WebSocket connection closed with exception {ws.exception()}."
)
finally:
if uid in self.clients:
del self.clients[uid]
await self.broadcast(
{"type": "system", "message": f"{username} left the chat."},
exclude=uid,
)
return ws
async def broadcast(self, message, exclude=None):
await self.insert("messages", message)
for uid, client_ws in self.clients.items():
if uid == exclude:
continue
try:
await client_ws.send_json(message)
except Exception as e:
print(f"Error sending to client {uid}: {e}.")
def create_app():
app = Application()
return app

3
src/rchat/faker.py Normal file
View File

@ -0,0 +1,3 @@
from faker import Faker
fake = Faker()

204
src/rchat/static/index.html Normal file
View File

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html>
<head>
<title>rchat</title>
<style>
body {
display: flex;
flex-direction: column;
font-family: 'Courier New', Courier, monospace;
background-color: #000;
}
#chat-container {
width: 100%;
display: flex;
flex-direction: column;
overflow-y: hidden;
}
#messages {
flex-grow: 1;
display: flex;
flex-direction: column;
}
#message-form {
display: flex;
}
#message-input {
all: unset;
flex-grow: 1;
margin-right: 10px;
background-color: #000;
color: #fff;
border: none;
}
.message {
word-wrap: break-word;
}
.chat {
color: #ccc;
}
.system {
color: yellow;
font-style: italic;
}
.blue {
color: lightskyblue;
}
</style>
</head>
<body>
<div id="chat-container">
<div id="messages"></div>
<form id="message-form">
<input list="slash-commands" type="text" id="message-input" placeholder="Type a message...">
<datalist id="slash-commands">
<option value="/nick ">
</datalist>
</form>
</div>
<script>
class App {
ws = null
url = null
messageDiv = null
messageForm = null
messageInput = null
justLoaded = true
originalTitle = null
visible = true
stopNotify() {
if (this.notificationInterval) {
clearInterval(this.notificationInterval)
this.notificationInterval = null
}
}
startNotify() {
if (this.notificationInterval) {
return
}
console.info("START")
const me = this
this.notificationInterval = setInterval(() => {
if (document.title == me.originalTitle) {
document.title = "* " + me.originalTitle
} else {
document.title = me.originalTitle
}
if (me.visible) {
me.stopNotify()
}
}, 1000)
}
visibleChange() {
if (document.visibilityState == "visible") {
this.visible = true
} else {
this.visible = false
}
}
constructor() {
this.originalTitle = document.title
this.protocol = window.location.protocol == 'http:' ? 'ws:' : 'wss:'
this.url = this.protocol + '//' + window.location.host + '/ws'
this.ws = new WebSocket(this.url)
this.messagesDiv = document.getElementById('messages')
this.messageForm = document.getElementById('message-form')
this.messageInput = document.getElementById('message-input')
const me = this
document.addEventListener('click', () => {
if (document.getSelection().toString() == "")
me.messageInput.focus()
})
document.addEventListener("visibilitychange", () => {
me.visibleChange()
})
document.addEventListener('keydown', (event) => {
if(document.getSelection().toString() == "")
me.messageInput.focus()
})
this.ws.onopen = () => {
me.appendMessage('Connected.', 'system')
}
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
switch (data.type) {
case 'system':
me.appendMessage(data.message, 'system')
break
case 'chat':
me.appendMessage(`${data.sender}: ${data.message}`, 'chat')
break
case 'error':
me.appendMessage(data.message, 'error')
break
}
if (!me.visible)
me.startNotify()
}
this.messageForm.addEventListener('submit', (e) => {
e.preventDefault()
const message = me.messageInput.value.trim()
if (message) {
me.ws.send(JSON.stringify({ message }))
me.messageInput.value = ''
}
})
this.ws.onerror = (error) => {
console.error('WebSocket Error:', error)
me.appendMessage('Connection error', 'error')
}
this.ws.onclose = () => {
me.appendMessage('Disconnected from chat server', 'system')
}
}
appendMessage(message, type) {
message = ">>> " + message
const messageEl = document.createElement('div')
let parts = message.split("`")
let newMessage = ''
let part = 0
for (let i = 0; i < parts.length; i++) {
if (part == 0) {
newMessage += parts[i]
part++
} else if (part == 1) {
newMessage += '<span class="blue">'
newMessage += parts[i]
newMessage += '</span>'
part = 0
}
}
messageEl.innerHTML = newMessage
messageEl.classList.add("message")
messageEl.classList.add(type)
this.messagesDiv.appendChild(messageEl)
this.messageInput.scrollIntoView({ behavior: 'smooth' })
}
}
const app = new App()
</script>
</body>
</html>