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