Initial commit.
This commit is contained in:
commit
40b8bd42fd
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.vscode
|
||||||
|
rchat.db*
|
||||||
|
config.py
|
||||||
|
.venv
|
||||||
|
.history
|
||||||
|
__pycache__
|
28
Makefile
Normal file
28
Makefile
Normal 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
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
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
BIN
dist/rchat-1.0.0.tar.gz
vendored
Normal file
Binary file not shown.
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
28
setup.cfg
Normal file
28
setup.cfg
Normal 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
|
14
src/rchat.egg-info/PKG-INFO
Normal file
14
src/rchat.egg-info/PKG-INFO
Normal 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
|
12
src/rchat.egg-info/SOURCES.txt
Normal file
12
src/rchat.egg-info/SOURCES.txt
Normal 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
|
1
src/rchat.egg-info/dependency_links.txt
Normal file
1
src/rchat.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
2
src/rchat.egg-info/entry_points.txt
Normal file
2
src/rchat.egg-info/entry_points.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
rchat.serve = rchat.__main__:main
|
5
src/rchat.egg-info/requires.txt
Normal file
5
src/rchat.egg-info/requires.txt
Normal 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
|
1
src/rchat.egg-info/top_level.txt
Normal file
1
src/rchat.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
rchat
|
5
src/rchat/__init__.py
Normal file
5
src/rchat/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
14
src/rchat/__main__.py
Normal file
14
src/rchat/__main__.py
Normal 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
134
src/rchat/app.py
Normal 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
3
src/rchat/faker.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
fake = Faker()
|
204
src/rchat/static/index.html
Normal file
204
src/rchat/static/index.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user