temporary move

This commit is contained in:
2025-01-29 16:36:18 +01:00
parent 561a915e30
commit d7c003c409
91 changed files with 0 additions and 366 deletions
+114
View File
@@ -0,0 +1,114 @@
import pathlib
from aiohttp import web
from aiohttp_session import (
get_session as session_get,
session_middleware,
setup as session_setup,
)
from aiohttp_session.cookie_storage import EncryptedCookieStorage
from app.app import Application as BaseApplication
from snek.docs.app import Application as DocsApplication
from snek.mapper import get_mappers
from snek.service import get_services
from snek.system import http
from snek.system.cache import Cache
from snek.system.markdown import MarkdownExtension
from snek.system.middleware import cors_middleware
from snek.view.about import AboutHTMLView, AboutMDView
from snek.view.docs import DocsHTMLView, DocsMDView
from snek.view.index import IndexView
from snek.view.login import LoginView
from snek.view.logout import LogoutView
from snek.view.register import RegisterView
from snek.view.rpc import RPCView
from snek.view.status import StatusView
from snek.view.web import WebView
# base64.urlsafe_b64encode(
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@web.middleware
async def session_middleware(request, handler):
setattr(request, "session", await session_get(request))
response = await handler(request)
return response
class Application(BaseApplication):
def __init__(self, *args, **kwargs):
middlewares = [
cors_middleware,
web.normalize_path_middleware(merge_slashes=True),
]
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
super().__init__(
middlewares=middlewares, template_path=self.template_path, *args, **kwargs
)
session_setup(self, EncryptedCookieStorage(SESSION_KEY))
self._middlewares.append(session_middleware)
self.jinja2_env.add_extension(MarkdownExtension)
self.setup_router()
self.cache = Cache(self)
self.services = get_services(app=self)
self.mappers = get_mappers(app=self)
def setup_router(self):
self.router.add_get("/", IndexView)
self.router.add_static(
"/",
pathlib.Path(__file__).parent.joinpath("static"),
name="static",
show_index=True,
)
self.router.add_view("/about.html", AboutHTMLView)
self.router.add_view("/about.md", AboutMDView)
self.router.add_view("/logout.json", LogoutView)
self.router.add_view("/logout.html", LogoutView)
self.router.add_view("/docs.html", DocsHTMLView)
self.router.add_view("/docs.md", DocsMDView)
self.router.add_view("/status.json", StatusView)
self.router.add_view("/web.html", WebView)
self.router.add_view("/login.html", LoginView)
self.router.add_view("/login.json", LoginView)
self.router.add_view("/register.html", RegisterView)
self.router.add_view("/register.json", RegisterView)
self.router.add_get("/http-get", self.handle_http_get)
self.router.add_get("/http-photo", self.handle_http_photo)
self.router.add_get("/rpc.ws", RPCView)
self.add_subapp(
"/docs",
DocsApplication(path=pathlib.Path(__file__).parent.joinpath("docs")),
)
async def handle_test(self, request):
return await self.render_template(
"test.html", request, context={"name": "retoor"}
)
async def handle_http_get(self, request: web.Request):
url = request.query.get("url")
content = await http.get(url)
return web.Response(body=content)
async def handle_http_photo(self, request):
url = request.query.get("url")
path = await http.create_site_photo(url)
return web.Response(
body=path.read_bytes(), headers={"Content-Type": "image/png"}
)
# @time_cache_async(60)
async def render_template(self, template, request, context=None):
return await super().render_template(template, request, context)
app = Application(db_path="sqlite:///snek.db")
if __name__ == "__main__":
web.run_app(app, port=8081, host="0.0.0.0")
View File
+51
View File
@@ -0,0 +1,51 @@
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
class AuthField(FormInputElement):
@property
async def errors(self):
result = await super().errors
if self.model.password.value and self.model.username.value:
if not await self.app.services.user.validate_login(
self.model.username.value, self.model.password.value
):
return ["Invalid username or password"]
return result
class LoginForm(Form):
title = HTMLElement(tag="h1", text="Login")
username = AuthField(
name="username",
required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_]+$",
place_holder="Username",
type="text",
)
password = AuthField(
name="password",
required=True,
regex=r"^[a-zA-Z0-9_.+-]{6,}",
type="password",
place_holder="Password",
)
action = FormButtonElement(
name="action", value="submit", text="Login", type="button"
)
@property
async def is_valid(self):
return all(
[
self["username"],
self["password"],
not await self.username.errors,
not await self.password.errors,
]
)
+44
View File
@@ -0,0 +1,44 @@
from snek.system.form import Form, FormButtonElement, FormInputElement, HTMLElement
class UsernameField(FormInputElement):
@property
async def errors(self):
result = await super().errors
if self.value and await self.app.services.user.count(username=self.value):
result.append("Username is not available.")
return result
class RegisterForm(Form):
title = HTMLElement(tag="h1", text="Register")
username = UsernameField(
name="username",
required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_]+$",
place_holder="Username",
type="text",
)
email = FormInputElement(
name="email",
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
place_holder="Email address",
type="email",
)
password = FormInputElement(
name="password",
required=True,
regex=r"^[a-zA-Z0-9_.+-]{6,}",
type="password",
place_holder="Password",
)
action = FormButtonElement(
name="action", value="submit", text="Register", type="button"
)
+3
View File
@@ -0,0 +1,3 @@
from snek.app import app
application = app
+25
View File
@@ -0,0 +1,25 @@
import functools
from snek.mapper.channel import ChannelMapper
from snek.mapper.channel_member import ChannelMemberMapper
from snek.mapper.channel_message import ChannelMessageMapper
from snek.mapper.notification import NotificationMapper
from snek.mapper.user import UserMapper
from snek.system.object import Object
@functools.cache
def get_mappers(app=None):
return Object(
**{
"user": UserMapper(app=app),
"channel_member": ChannelMemberMapper(app=app),
"channel": ChannelMapper(app=app),
"channel_message": ChannelMessageMapper(app=app),
"notification": NotificationMapper(app=app),
}
)
def get_mapper(name, app=None):
return get_mappers(app=app)[name]
+7
View File
@@ -0,0 +1,7 @@
from snek.model.channel import ChannelModel
from snek.system.mapper import BaseMapper
class ChannelMapper(BaseMapper):
table_name = "channel"
model_class = ChannelModel
+7
View File
@@ -0,0 +1,7 @@
from snek.model.channel_member import ChannelMemberModel
from snek.system.mapper import BaseMapper
class ChannelMemberMapper(BaseMapper):
table_name = "channel_member"
model_class = ChannelMemberModel
+7
View File
@@ -0,0 +1,7 @@
from snek.model.channel_message import ChannelMessageModel
from snek.system.mapper import BaseMapper
class ChannelMessageMapper(BaseMapper):
model_class = ChannelMessageModel
table_name = "channel_message"
+7
View File
@@ -0,0 +1,7 @@
from snek.model.notification import NotificationModel
from snek.system.mapper import BaseMapper
class NotificationMapper(BaseMapper):
table_name = "notification"
model_class = NotificationModel
+7
View File
@@ -0,0 +1,7 @@
from snek.model.user import UserModel
from snek.system.mapper import BaseMapper
class UserMapper(BaseMapper):
table_name = "user"
model_class = UserModel
+25
View File
@@ -0,0 +1,25 @@
import functools
from snek.model.channel import ChannelModel
from snek.model.channel_member import ChannelMemberModel
# from snek.model.channel_message import ChannelMessageModel
from snek.model.channel_message import ChannelMessageModel
from snek.model.user import UserModel
from snek.system.object import Object
@functools.cache
def get_models():
return Object(
**{
"user": UserModel,
"channel_member": ChannelMemberModel,
"channel": ChannelModel,
"channel_message": ChannelMessageModel,
}
)
def get_model(name):
return get_models()[name]
+11
View File
@@ -0,0 +1,11 @@
from snek.system.model import BaseModel, ModelField
class ChannelModel(BaseModel):
label = ModelField(name="label", required=True, kind=str)
description = ModelField(name="description", required=False, kind=str)
tag = ModelField(name="tag", required=False, kind=str)
created_by_uid = ModelField(name="created_by_uid", required=True, kind=str)
is_private = ModelField(name="is_private", required=True, kind=bool, value=False)
is_listed = ModelField(name="is_listed", required=True, kind=bool, value=True)
index = ModelField(name="index", required=True, kind=int, value=1000)
+15
View File
@@ -0,0 +1,15 @@
from snek.system.model import BaseModel, ModelField
class ChannelMemberModel(BaseModel):
label = ModelField(name="label", required=True, kind=str)
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
user_uid = ModelField(name="user_uid", required=True, kind=str)
is_moderator = ModelField(
name="is_moderator", required=True, kind=bool, value=False
)
is_read_only = ModelField(
name="is_read_only", required=True, kind=bool, value=False
)
is_muted = ModelField(name="is_muted", required=True, kind=bool, value=False)
is_banned = ModelField(name="is_banned", required=True, kind=bool, value=False)
+8
View File
@@ -0,0 +1,8 @@
from snek.system.model import BaseModel, ModelField
class ChannelMessageModel(BaseModel):
channel_uid = ModelField(name="channel_uid", required=True, kind=str)
user_uid = ModelField(name="user_uid", required=True, kind=str)
message = ModelField(name="message", required=True, kind=str)
html = ModelField(name="html", required=False, kind=str)
+9
View File
@@ -0,0 +1,9 @@
from snek.system.model import BaseModel, ModelField
class NotificationModel(BaseModel):
object_uid = ModelField(name="object_uid", required=True)
object_type = ModelField(name="object_type", required=True)
message = ModelField(name="message", required=True)
user_uid = ModelField(name="user_uid", required=True)
read_at = ModelField(name="is_read", required=True)
+31
View File
@@ -0,0 +1,31 @@
from snek.system.model import BaseModel, ModelField
class UserModel(BaseModel):
username = ModelField(
name="username",
required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_]+$",
)
nick = ModelField(
name="nick",
required=True,
min_length=2,
max_length=20,
regex=r"^[a-zA-Z0-9_]+$",
)
color = ModelField(
name ="color",
required=True,
regex=r"^#[0-9a-fA-F]{6}$",
kind=str
)
email = ModelField(
name="email",
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
)
password = ModelField(name="password", required=True, regex=r"^[a-zA-Z0-9_.+-]{6,}")
+31
View File
@@ -0,0 +1,31 @@
import functools
from snek.service.channel import ChannelService
from snek.service.channel_member import ChannelMemberService
from snek.service.channel_message import ChannelMessageService
from snek.service.chat import ChatService
from snek.service.notification import NotificationService
from snek.service.socket import SocketService
from snek.service.user import UserService
from snek.service.util import UtilService
from snek.system.object import Object
@functools.cache
def get_services(app):
return Object(
**{
"user": UserService(app=app),
"channel_member": ChannelMemberService(app=app),
"channel": ChannelService(app=app),
"channel_message": ChannelMessageService(app=app),
"chat": ChatService(app=app),
"socket": SocketService(app=app),
"notification": NotificationService(app=app),
"util": UtilService(app=app),
}
)
def get_service(name, app=None):
return get_services(app=app)[name]
+48
View File
@@ -0,0 +1,48 @@
from snek.system.service import BaseService
class ChannelService(BaseService):
mapper_name = "channel"
async def create(
self,
label,
created_by_uid,
description=None,
tag=None,
is_private=False,
is_listed=True,
):
if label[0] != "#" and is_listed:
label = f"#{label}"
count = await self.count(deleted_at=None)
if not tag and not count:
tag = "public"
model = await self.new()
model["label"] = label
model["description"] = description
model["tag"] = tag
model["created_by_uid"] = created_by_uid
model["is_private"] = is_private
model["is_listed"] = is_listed
if await self.save(model):
return model
raise Exception(f"Failed to create channel: {model.errors}.")
async def ensure_public_channel(self, created_by_uid):
model = await self.get(is_listed=True, tag="public")
is_moderator = False
if not model:
is_moderator = True
model = await self.create(
"public", created_by_uid=created_by_uid, is_listed=True, tag="public"
)
await self.app.services.channel_member.create(
model["uid"],
created_by_uid,
is_moderator=is_moderator,
is_read_only=False,
is_muted=False,
is_banned=False,
)
return model
+34
View File
@@ -0,0 +1,34 @@
from snek.system.service import BaseService
class ChannelMemberService(BaseService):
mapper_name = "channel_member"
async def create(
self,
channel_uid,
user_uid,
is_moderator=False,
is_read_only=False,
is_muted=False,
is_banned=False,
):
model = await self.get(channel_uid=channel_uid, user_uid=user_uid)
if model:
if model.is_banned.value:
return False
return model
model = await self.new()
channel = await self.services.channel.get(uid=channel_uid)
model["label"] = channel["label"]
model["channel_uid"] = channel_uid
model["user_uid"] = user_uid
model["is_moderator"] = is_moderator
model["is_read_only"] = is_read_only
model["is_muted"] = is_muted
model["is_banned"] = is_banned
print(model.record, flush=True)
if await self.save(model):
return model
raise Exception(f"Failed to create channel member: {model.errors}.")
+40
View File
@@ -0,0 +1,40 @@
from snek.system.service import BaseService
import jinja2
class ChannelMessageService(BaseService):
mapper_name = "channel_message"
async def create(self, channel_uid, user_uid, message):
model = await self.new()
model["channel_uid"] = channel_uid
model["user_uid"] = user_uid
model["message"] = message
context = {
}
record = model.record
context.update(record)
user = await self.app.services.user.get(uid=user_uid)
context.update(dict(
user_uid=user['uid'],
username=user['username'],
user_nick=user['nick']
))
try:
template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context)
except Exception as ex:
print(ex,flush=True)
print("RENDER",flush=True)
print("RECORD",context,flush=True)
print("AFTER RENDER",flush=True)
if await self.save(model):
return model
raise Exception(f"Failed to create channel message: {model.errors}.")
+30
View File
@@ -0,0 +1,30 @@
from snek.system.service import BaseService
class ChatService(BaseService):
async def send(self,user_uid, channel_uid, message):
channel_message = await self.services.channel_message.create(
channel_uid,
user_uid,
message
)
channel_message_uid = channel_message["uid"]
user = await self.services.user.get(uid=user_uid)
await self.services.notification.create_channel_message(channel_message_uid)
sent_to_count = await self.services.socket.broadcast(channel_uid, dict(
message=channel_message["message"],
html=channel_message["html"],
user_uid=user_uid,
color=user['color'],
channel_uid=channel_uid,
created_at=channel_message["created_at"],
updated_at=None,
uid=channel_message['uid'],
user_nick=user['nick']
))
return sent_to_count
+37
View File
@@ -0,0 +1,37 @@
from snek.system.service import BaseService
class NotificationService(BaseService):
mapper_name = "notification"
async def create(self, object_uid, object_type, user_uid, message):
model = await self.new()
model["object_uid"] = object_uid
model["object_type"] = object_type
model["user_uid"] = user_uid
model["message"] = message
if await self.save(model):
return model
raise Exception(f"Failed to create notification: {model.errors}.")
async def create_channel_message(self, channel_message_uid):
channel_message = await self.services.channel_message.get(
uid=channel_message_uid
)
user = await self.services.user.get(uid=channel_message["user_uid"])
async for channel_member in self.services.channel_member.find(
channel_uid=channel_message["channel_uid"],
is_banned=False,
is_muted=False,
deleted_at=None,
):
model = await self.new()
model["object_uid"] = channel_message_uid
model["object_type"] = "channel_message"
model["user_uid"] = channel_member["user_uid"]
model["message"] = (
f"New message from {user['nick']} in {channel_member['label']}."
)
if await self.save(model):
return model
raise Exception(f"Failed to create notification: {model.errors}.")
+40
View File
@@ -0,0 +1,40 @@
from snek.system.service import BaseService
class SocketService(BaseService):
def __init__(self, app):
super().__init__(app)
self.sockets = set()
self.subscriptions = {}
async def add(self, ws):
self.sockets.add(ws)
async def subscribe(self, ws, channel_uid):
if not channel_uid in self.subscriptions:
self.subscriptions[channel_uid] = set()
self.subscriptions[channel_uid].add(ws)
async def broadcast(self, channel_uid, message):
print("BROADCAT!",message)
count = 0
subscriptions = set(self.subscriptions.get(channel_uid,[]))
for ws in subscriptions:
try:
await ws.send_json(message)
except Exception as ex:
print(ex)
print("Deleting socket.")
self.subscriptions[channel_uid].remove(ws)
continue
count += 1
return count
async def delete(self, ws):
try:
self.sockets.remove(ws)
except IndexError:
pass
+38
View File
@@ -0,0 +1,38 @@
from snek.system import security
from snek.system.service import BaseService
class UserService(BaseService):
mapper_name = "user"
async def validate_login(self, username, password):
model = await self.get(username=username)
if not model:
return False
if not await security.verify(password, model["password"]):
return False
return True
async def save(self, user):
if not user['color']:
user['color'] = await self.services.util.random_light_hex_color()
return await super().save(user)
async def register(self, email, username, password):
if await self.exists(username=username):
raise Exception("User already exists.")
model = await self.new()
model["nick"] = username
model['color'] = await self.services.util.random_light_hex_color()
model.email.value = email
model.username.value = username
model.password.value = await security.hash(password)
if await self.save(model):
if model:
channel = await self.services.channel.ensure_public_channel(
model["uid"]
)
if not channel:
raise Exception("Failed to create public channel.")
return model
raise Exception(f"Failed to create user: {model.errors}.")
+15
View File
@@ -0,0 +1,15 @@
import random
from snek.system.service import BaseService
class UtilService(BaseService):
async def random_light_hex_color(self):
r = random.randint(128, 255)
g = random.randint(128, 255)
b = random.randint(128, 255)
return "#{:02x}{:02x}{:02x}".format(r, g, b)
+402
View File
@@ -0,0 +1,402 @@
/*class Message {
uid = null
author = null
avatar = null
text = null
time = null
constructor(uid,avatar,author,text,time){
this.uid = uid
this.avatar = avatar
this.author = author
this.text = text
this.time = time
}
get links() {
if(!this.text)
return []
let result = []
for(let part in this.text.split(/[,; ]/)){
if(part.startsWith("http") || part.startsWith("www.") || part.indexOf(".com") || part.indexOf(".net") || part.indexOf(".io") || part.indexOf(".nl")){
result.push(part)
}
}
return result
}
get mentions() {
if(!this.text)
return []
let result = []
for(let part in this.text.split(/[,; ]/)){
if(part.startsWith("@")){
result.push(part)
}
}
return result
}
}*/
class Messages {
}
class Room {
name = null
messages = []
constructor(name) {
this.name = name
}
setMessages(list) {
}
}
class InlineAppElement extends HTMLElement {
constructor() {
// this.
}
}
class Page {
elements = []
}
class RESTClient {
debug = false
async get(url, params) {
params = params ? params : {}
const encodedParams = new URLSearchParams(params);
if (encodedParams)
url += '?' + encodedParams
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json()
if (this.debug) {
console.debug({ url: url, params: params, result: result })
}
return result
}
async post(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json()
if (this.debug) {
console.debug({ url: url, params: params, result: result })
}
return result
}
}
const rest = new RESTClient()
class EventHandler {
constructor() {
this.subscribers = {}
}
addEventListener(type, handler) {
if (!this.subscribers[type])
this.subscribers[type] = []
this.subscribers[type].push(handler)
}
emit(type, ...data) {
if (this.subscribers[type])
this.subscribers[type].forEach(handler => handler(...data))
}
}
class Chat extends EventHandler {
constructor() {
super()
this._url = window.location.hostname == 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws'
this._socket = null
this._wait_connect = null
this._promises = {}
}
connect() {
if (this._wait_connect)
return this._wait_connect
const me = this
return new Promise(async (resolve, reject) => {
me._wait_connect = resolve
console.debug("Connecting..")
try {
me._socket = new WebSocket(me._url)
}catch(e){
console.warning(e)
setTimeout(()=>{
me.ensureConnection()
},1000)
}
me._socket.onconnect = () => {
me._connected()
me._wait_socket(me)
}
})
}
generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9); // Example: id-k5f9zq7
}
call(method, ...args) {
const me = this
return new Promise(async (resolve, reject) => {
try {
const command = { method: method, args: args, message_id: me.generateUniqueId() }
me._promises[command.message_id] = resolve
await me._socket.send(JSON.stringify(command))
} catch (e) {
reject(e)
}
})
}
_connected() {
const me = this
this._socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.message_id && me._promises[message.message_id]) {
me._promises[message.message_id](message)
delete me._promises[message.message_id]
} else {
me.emit("message", me, message)
}
//const room = this.rooms.find(room=>room.name == message.room)
//if(!room){
// this.rooms.push(new Room(message.room))
}
this._socket.onclose = (event) => {
me._wait_socket = null
me._socket = null
me.emit('close', me)
}
}
async privmsg(room, text) {
await rest.post("/api/privmsg", {
room: room,
text: text
})
}
}
class Socket extends EventHandler {
ws = null
isConnected = null
isConnecting = null
url = null
connectPromises = []
ensureTimer = null
constructor() {
super()
this.url = window.location.hostname == 'localhost' ? 'ws://localhost:8081/rpc.ws' : 'wss://' + window.location.hostname + '/rpc.ws'
this.ensureConnection()
}
_camelToSnake(str) {
return str
.replace(/([a-z])([A-Z])/g, '$1_$2')
.toLowerCase();
}
get client() {
const me = this
const proxy = new Proxy(
{},
{
get(target, prop) {
return (...args) => {
let functionName = me._camelToSnake(prop)
return me.call(functionName, ...args);
};
},
}
);
return proxy
}
ensureConnection() {
if(this.ensureTimer)
return this.connect()
const me = this
this.ensureTimer = setInterval(()=>{
if (me.isConnecting)
me.isConnecting = false
me.connect()
},5000)
return this.connect()
}
generateUniqueId() {
return 'id-' + Math.random().toString(36).substr(2, 9);
}
connect() {
const me = this
if (!this.isConnected && !this.isConnecting) {
this.isConnecting = true
} else if (this.isConnecting) {
return new Promise((resolve, reject) => {
me.connectPromises.push(resolve)
})
} else if (this.isConnected) {
return new Promise((resolve, reject) => {
resolve(me)
})
}
return new Promise((resolve, reject) => {
me.connectPromises.push(resolve)
console.debug("Connecting..")
const ws = new WebSocket(this.url)
ws.onopen = (event) => {
me.ws = ws
me.isConnected = true
me.isConnecting = false
ws.onmessage = (event) => {
me.onData(JSON.parse(event.data))
}
ws.onclose = (event) => {
me.onClose()
}
ws.onerror = (event)=>{
me.onClose()
}
me.connectPromises.forEach(resolve => {
resolve(me)
})
}
})
}
onData(data) {
if(data.success != undefined && !data.success){
console.error(data)
}
if (data.callId) {
this.emit(data.callId, data.data)
}
if (data.channel_uid) {
this.emit(data.channel_uid, data.data)
this.emit("channel-message", data)
}
}
async sendJson(data) {
return await this.connect().then((api) => {
api.ws.send(JSON.stringify(data))
})
}
async call(method, ...args) {
const call = {
callId: this.generateUniqueId(),
method: method,
args: args
}
const me = this
return new Promise(async (resolve, reject) => {
me.addEventListener(call.callId, (data) => {
resolve(data)
})
await me.sendJson(call)
})
}
onClose() {
console.info("Connection lost. Reconnecting.")
this.isConnected = false
this.isConnecting = false
this.ensureConnection().then(() => {
console.info("Reconnected.")
})
}
}
class App extends EventHandler {
rooms = []
rest = rest
ws = null
rpc = null
sounds = ["/audio/soundfx.d_beep3.mp3"]
playSound(soundIndex) {
if (!soundIndex)
soundIndex = 0
const player = new Audio(this.sounds[soundIndex]);
player.play()
.then(() => {
console.debug("Gave sound notification")
})
.catch((error) => {
console.error("Notification failed:", error);
});
}
constructor() {
super()
this.rooms.push(new Room("General"))
this.ws = new Socket()
this.rpc = this.ws.client
const me = this
this.ws.addEventListener("channel-message", (data) => {
me.emit(data.channel_uid, data)
})
}
async benchMark(times, message) {
if (!times)
times = 100
if (!message)
message = "Benchmark Message"
let promises = []
const me = this
for (let i = 0; i < times; i++) {
promises.push(this.rpc.getChannels().then(channels => {
channels.forEach(channel => {
me.rpc.sendMessage(channel.uid, `${message} ${i}`).then(data => {
})
})
}))
}
//return await Promise.all(promises)
}
}
const app = new App()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+238
View File
@@ -0,0 +1,238 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #e6e6e6;
line-height: 1.5;
display: flex;
flex-direction: column;
height: 100vh;
min-width: 100%;
}
main {
min-width: 100%;
}
header {
background-color: #0f0f0f;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header .logo {
color: #fff;
font-size: 1.5em;
font-weight: bold;
}
header nav a {
color: #aaa;
text-decoration: none;
margin-left: 15px;
font-size: 1em;
transition: color 0.3s;
}
header nav a:hover {
color: #fff;
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #121212;
padding: 20px;
overflow-y: auto;
border-right: 1px solid #333;
}
.sidebar h2 {
color: #f05a28;
font-size: 1.2em;
margin-bottom: 20px;
}
.sidebar ul {
list-style: none;
}
.sidebar ul li {
margin-bottom: 15px;
}
.sidebar ul li a {
color: #ccc;
text-decoration: none;
font-size: 1em;
transition: color 0.3s;
}
.sidebar ul li a:hover {
color: #fff;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background-color: #1a1a1a;
}
.chat-header {
padding: 10px 20px;
background-color: #0f0f0f;
border-bottom: 1px solid #333;
}
.chat-header h2 {
font-size: 1.2em;
color: #fff;
}
message-list {
flex: 1;;
height: 200px;
padding-bottom: 40px;
overflow-y: auto;
}
.chat-messages {
flex: 1;
padding: 10px;
height: 200px;
background: #1a1a1a;
}
.message-list-manager {
flex: 1;
overflow-y: auto;
background: #1a1a1a;
}
.chat-messages .message {
display: flex;
align-items: flex-start;
margin-bottom: 0px;
padding: 5px;
/*background: #222;*/
border-radius: 8px;
/* box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);*/
}
.chat-messages .message .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f05a28;
color: #fff;
font-size: 1em;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.chat-messages .message .message-content {
flex: 1;
}
.chat-messages .message .message-content .author {
font-weight: bold;
color: #f05a28;
margin-bottom: 3px;
}
.chat-messages .message .message-content .text {
margin-bottom: 5px;
color: #e6e6e6;
}
.chat-messages .message .message-content .time {
font-size: 0.8em;
color: #aaa;
}
.chat-input {
padding: 15px;
background-color: #121212;
display: flex;
align-items: center;
border-top: 1px solid #333;
}
.chat-input textarea {
flex: 1;
background-color: #1a1a1a;
color: white;
border: none;
padding: 10px;
border-radius: 5px;
resize: none;
}
.chat-input button {
background-color: #f05a28;
color: white;
border: none;
padding: 10px 15px;
margin-left: 10px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s;
}
.chat-input button:hover {
background-color: #e04924;
}
@media (max-width: 768px) {
.sidebar {
display: none;
}
.chat-area {
flex: 1;
}
}
.message {
.avatar {
opacity: 0;
}
.author {
display: none;
}
.time {
display: none;
}
}
.message.switch-user {
.avatar {
opacity: 1;
}
.author {
display: block;
}
}
.message:has(+ .message.switch-user), .message:last-child
{
.time {
display: block;
}
}
+50
View File
@@ -0,0 +1,50 @@
class ChatInputElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div');
this.shadowRoot.appendChild(this.component);
}
connectedCallback() {
const me = this
const link = document.createElement("link")
link.rel = 'stylesheet'
link.href = '/base.css'
this.component.appendChild(link)
this.container = document.createElement('div')
this.container.classList.add("chat-input")
this.container.innerHTML = `
<textarea placeholder="Type a message..." rows="2"></textarea>
<button>Send</button>
`;
this.textBox = this.container.querySelector('textarea')
this.textBox.addEventListener('input', (e) => {
this.dispatchEvent(new CustomEvent("input", { detail: e.target.value, bubbles: true }))
const message = e.target.value;
const button = this.container.querySelector('button');
button.disabled = !message;
})
this.textBox.addEventListener('change', (e) => {
this.dispatchEvent(new CustomEvent("change", { detail: e.target.value, bubbles: true }))
console.error(e.target.value)
})
this.textBox.addEventListener('keyup', (e) => {
if (e.key == 'Enter' && !e.shiftKey) {
this.dispatchEvent(new CustomEvent("submit", { detail: e.target.value, bubbles: true }))
e.target.value = ''
}
})
this.container.querySelector('button').addEventListener('click', (e) => {
this.dispatchEvent(new CustomEvent("submit", { detail: me.textBox.value, bubbles: true }))
setTimeout(()=>{
me.textBox.value = ''
me.textBox.focus()
},200)
})
this.component.appendChild(this.container)
}
}
customElements.define('chat-input', ChatInputElement);
+68
View File
@@ -0,0 +1,68 @@
class ChatWindowElement extends HTMLElement {
receivedHistory = false
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.component = document.createElement('section');
this.shadowRoot.appendChild(this.component);
}
async connectedCallback() {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/base.css'
this.component.appendChild(link)
this.component.classList.add("chat-area")
this.container = document.createElement("section")
this.container.classList.add("chat-area")
this.container.classList.add("chat-window")
const chatHeader = document.createElement("div")
chatHeader.classList.add("chat-header")
const chatTitle = document.createElement('h2')
chatTitle.classList.add("chat-title")
chatTitle.innerText = "Loading..."
chatHeader.appendChild(chatTitle)
this.container.appendChild(chatHeader)
const channels = await app.rpc.getChannels()
const channel = channels[0]
chatTitle.innerText = channel.name
const channelElement = document.createElement('message-list')
channelElement.setAttribute("channel", channel.uid)
//channelElement.classList.add("chat-messages")
this.container.appendChild(channelElement)
const chatInput = document.createElement('chat-input')
chatInput.addEventListener("submit",(e)=>{
app.rpc.sendMessage(channel.uid,e.detail)
})
this.container.appendChild(chatInput)
this.component.appendChild(this.container)
const messages = await app.rpc.getMessages(channel.uid)
messages.forEach(message=>{
if(!message['user_nick'])
return
channelElement.addMessage(message)
})
const me = this
channelElement.addEventListener("message",(message)=>{
app.playSound(0)
message.detail.element.scrollIntoView()
})
}
}
customElements.define('chat-window', ChatWindowElement);
+68
View File
@@ -0,0 +1,68 @@
class FancyButton extends HTMLElement {
url = null
type="button"
value = null
constructor(){
super()
this.attachShadow({mode:'open'})
}
connectedCallback() {
this.container = document.createElement('span')
let size = this.getAttribute('size')
console.info({GG:size})
if(size == 'auto'){
size = '1%'
}else{
size = '33%'
}
this.styleElement = document.createElement("style")
this.styleElement.innerHTML = `
:root {
width:100%;
--width: 100%;
}
button {
width: var(--width);
min-width: ${size};
padding: 10px;
background-color: #f05a28;
border: none;
border-radius: 5px;
color: white;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
border: 1px solid #f05a28;
}
button:hover {
color: #EFEFEF;
background-color: #e04924;
border: 1px solid #efefef;
}
`
this.container.appendChild(this.styleElement)
this.buttonElement = document.createElement('button')
this.container.appendChild(this.buttonElement)
this.shadowRoot.appendChild(this.container)
this.url = this.getAttribute('url');
this.value = this.getAttribute('value')
const me = this
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")))
this.buttonElement.addEventListener("click",()=>{
if(me.url == "/back" || me.url == "/back/"){
window.history.back()
}else if(me.url){
window.location = me.url
}
})
}
}
customElements.define("fancy-button",FancyButton)
+100
View File
@@ -0,0 +1,100 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #e6e6e6;
line-height: 1.5;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
}
generic-form {
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: #000000;
}
.generic-form-container {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 400px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center;
}
.generic-form-container h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
}
input {
border: 10px solid #000000;
}
.generic-form-container generic-field {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #333;
border-radius: 5px;
background-color: #1a1a1a;
color: #e6e6e6;
font-size: 1em;
}
.generic-form-container button {
width: 100%;
padding: 10px;
background-color: #f05a28;
border: none;
border-radius: 5px;
color: white;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
}
.generic-form-container button:hover {
background-color: #e04924;
}
.generic-form-container a {
color: #f05a28;
text-decoration: none;
display: block;
margin-top: 15px;
font-size: 0.9em;
transition: color 0.3s;
}
.generic-form-container a:hover {
color: #e04924;
}
.error {
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
}
@media (max-width: 500px) {
.generic-form-container {
width: 90%;
}
}
+346
View File
@@ -0,0 +1,346 @@
class GenericField extends HTMLElement {
form = null
field = null
inputElement = null
footerElement = null
action = null
container = null
styleElement = null
name = null
get value() {
return this.inputElement.value
}
get type() {
return this.field.tag
}
set value(val) {
val = val == null ? '' : val
this.inputElement.value = val
this.inputElement.setAttribute("value", val)
}
setInvalid(){
this.inputElement.classList.add("error")
this.inputElement.classList.remove("valid")
}
setErrors(errors){
if(errors.length)
this.inputElement.setAttribute("title", errors[0])
else
this.inputElement.setAttribute("title","")
}
setValid(){
this.inputElement.classList.remove("error")
this.inputElement.classList.add("valid")
}
constructor() {
super()
this.attachShadow({mode:'open'})
this.container = document.createElement('div')
this.styleElement = document.createElement('style')
this.styleElement.innerHTML = `
h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
margin-top: 0px;
}
input {
width: 90%;
padding: 10px;
margin: 10px 0;
border: 1px solid #333;
border-radius: 5px;
background-color: #1a1a1a;
color: #e6e6e6;
font-size: 1em;
}
button {
width: 50%;
padding: 10px;
background-color: #f05a28;
border: none;
float: right;
margin-top: 10px;
margin-left: 10px;
margin-right: 10px;
border-radius: 5px;
color: white;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
clear: both;
}
button:hover {
background-color: #e04924;
}
a {
color: #f05a28;
text-decoration: none;
display: block;
margin-top: 15px;
font-size: 0.9em;
transition: color 0.3s;
}
a:hover {
color: #e04924;
}
.valid {
border: 1px solid green;
color:green;
font-size: 0.9em;
margin-top: 5px;
}
.error {
border: 3px solid red;
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
}
@media (max-width: 500px) {
input {
width: 90%;
}
}
`
this.container.appendChild(this.styleElement)
this.shadowRoot.appendChild(this.container)
}
connectedCallback(){
this.updateAttributes()
}
setAttribute(name,value){
this[name] = value
}
updateAttributes(){
if(this.inputElement == null && this.field){
this.inputElement = document.createElement(this.field.tag)
if(this.field.tag == 'button'){
if(this.field.value == "submit"){
}
this.action = this.field.value
}
this.inputElement.name = this.field.name
this.name = this.inputElement.name
const me = this
this.inputElement.addEventListener("keyup",(e)=>{
if(e.key == 'Enter'){
me.dispatchEvent(new Event("submit"))
}else if(me.field.value != e.target.value)
{
const event = new CustomEvent("change", {detail:me,bubbles:true})
me.dispatchEvent(event)
}
})
this.inputElement.addEventListener("click",(e)=>{
const event = new CustomEvent("click",{detail:me,bubbles:true})
me.dispatchEvent(event)
})
this.container.appendChild(this.inputElement)
}
if(!this.field){
return
}
this.inputElement.setAttribute("type",this.field.type == null ? 'input' : this.field.type)
this.inputElement.setAttribute("name",this.field.name == null ? '' : this.field.name)
if(this.field.text != null){
this.inputElement.innerText = this.field.text
}
if(this.field.html != null){
this.inputElement.innerHTML = this.field.html
}
if(this.field.class_name){
this.inputElement.classList.add(this.field.class_name)
}
this.inputElement.setAttribute("tabindex", this.field.index)
this.inputElement.classList.add(this.field.name)
this.value = this.field.value
let place_holder = null
if(this.field.place_holder)
place_holder = this.field.place_holder
if(this.field.required && place_holder){
place_holder = place_holder
}
if(place_holder)
this.field.place_holder = "* " + place_holder
this.inputElement.setAttribute("placeholder",place_holder)
if(this.field.required)
this.inputElement.setAttribute("required","required")
else
this.inputElement.removeAttribute("required")
if(!this.footerElement){
this.footerElement = document.createElement('div')
this.footerElement.style.clear = 'both'
this.container.appendChild(this.footerElement)
}
}
}
customElements.define('generic-field', GenericField);
class GenericForm extends HTMLElement {
fields = {}
form = {}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.styleElement = document.createElement("style")
this.styleElement.innerHTML = `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
width:90%
}
div {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 400px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center;
}
@media (max-width: 500px) {
width:100%;
height:100%;
form {
height:100%;
width: 100%;
width: 80%;
}
}`
this.container = document.createElement('div');
this.container.appendChild(this.styleElement)
this.container.classList.add("generic-form-container")
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
const url = this.getAttribute('url');
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
this.loadForm(fullUrl.toString());
} else {
this.container.textContent = "No URL provided!";
}
}
async loadForm(url) {
const me = this
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
}
me.form = await response.json();
let fields = Object.values(me.form.fields)
fields = fields.sort((a,b)=>{
console.info(a.index,b.index)
return a.index - b.index
})
fields.forEach(field=>{
const fieldElement = document.createElement('generic-field')
me.fields[field.name] = fieldElement
fieldElement.setAttribute("form", me)
fieldElement.setAttribute("field", field)
me.container.appendChild(fieldElement)
fieldElement.updateAttributes()
fieldElement.addEventListener("change",(e)=>{
me.form.fields[e.detail.name].value = e.detail.value
})
fieldElement.addEventListener("click",async (e)=>{
if(e.detail.type == "button"){
if(e.detail.value == "submit")
{
const isValid = await me.validate()
if(isValid){
const saveResult = await me.submit()
if(saveResult.redirect_url){
window.location.pathname = saveResult.redirect_url
}
}
}
}
})
})
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
async validate(){
const url = this.getAttribute("url")
const me = this
let response = await fetch(url,{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"action":"validate", "form":me.form})
});
const form = await response.json()
Object.values(form.fields).forEach(field=>{
if(!me.form.fields[field.name])
return
me.form.fields[field.name].is_valid = field.is_valid
if(!field.is_valid){
me.fields[field.name].setInvalid()
me.fields[field.name].setErrors(field.errors)
}else{
me.fields[field.name].setValid()
}
me.fields[field.name].setAttribute("field",field)
me.fields[field.name].updateAttributes()
})
Object.values(form.fields).forEach(field=>{
me.fields[field.name].setErrors(field.errors)
})
return form['is_valid']
}
async submit(){
const me = this
const url = me.getAttribute("url")
const response = await fetch(url,{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"action":"submit", "form":me.form})
});
return await response.json()
}
}
customElements.define('generic-form', GenericForm);
+6
View File
@@ -0,0 +1,6 @@
.html-frame {
width: 100px;
height: 50px;
overflow: hidden;
border: 1px solid black;
}
+47
View File
@@ -0,0 +1,47 @@
class HTMLFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.container = document.createElement('div');
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
this.container.classList.add("html_frame")
let url = this.getAttribute('url');
if(!url.startsWith("https")){
url = "https://" + url
}
if (url) {
let fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
this.loadAndRender(fullUrl.toString());
} else {
this.container.textContent = "No source URL!";
}
}
async loadAndRender(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
const html = await response.text();
if(url.endsWith(".md")){
const parent = this
const markdownElement = document.createElement('div')
markdownElement.innerHTML = html
this.outerHTML = html
}else{
this.container.innerHTML = html;
}
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
}
customElements.define('html-frame', HTMLFrame);
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Snek",
"description": "Danger noodle",
"display": "standalone",
"start_url": "/web.html",
"icons": [
{
"src": "/image/snek1.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
+39
View File
@@ -0,0 +1,39 @@
class HTMLFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.container = document.createElement('div');
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
this.container.classList.add("html_frame")
const url = this.getAttribute('url');
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
this.loadAndRender(fullUrl.toString());
} else {
this.container.textContent = "No source URL!";
}
}
async loadAndRender(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
const html = await response.text();
this.container.innerHTML = html;
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
}
customElements.define('markdown-frame', HTMLFrame);
@@ -0,0 +1,23 @@
class MessageListManagerElement extends HTMLElement {
constructor() {
super()
this.attachShadow({mode:'open'})
this.container = document.createElement("div")
this.shadowRoot.appendChild(this.container)
}
async connectedCallback() {
let channels = await app.rpc.getChannels()
const me = this
channels.forEach(channel=>{
const messageList = document.createElement("message-list")
messageList.setAttribute("channel",channel.uid)
me.container.appendChild(messageList)
})
}
}
customElements.define("message-list-manager",MessageListManagerElement)
+126
View File
@@ -0,0 +1,126 @@
class MessageListElement extends HTMLElement {
static get observedAttributes() {
return ["messages"];
}
messages = []
room = null
url = null
container = null
messageEventSchedule = null
observer = null
constructor() {
super()
this.attachShadow({ mode: 'open' });
this.component = document.createElement('div')
this.shadowRoot.appendChild(this.component )
}
linkifyText(text) {
const urlRegex = /https?:\/\/[^\s]+/g;
return text.replace(urlRegex, (url) => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});
}
createElement(message){
const element = document.createElement("div")
element.dataset.uid = message.uid
element.dataset.color = message.color
element.dataset.channel_uid = message.channel_uid
element.dataset.user_nick = message.user_nick
element.dataset.created_at = message.created_at
element.dataset.user_uid = message.user_uid
element.dataset.message = message.message
element.classList.add("message")
if(!this.messages.length){
element.classList.add("switch-user")
}else if (this.messages[this.messages.length-1].user_uid != message.user_uid){
element.classList.add("switch-user")
}
const avatar = document.createElement("div")
avatar.classList.add("avatar")
avatar.style.backgroundColor = message.color
avatar.style.color= "black"
avatar.innerText = message.user_nick[0]
const messageContent = document.createElement("div")
messageContent.classList.add("message-content")
const author = document.createElement("div")
author.classList.add("author")
author.style.color = message.color
author.textContent = message.user_nick
const text = document.createElement("div")
text.classList.add("text")
if(message.html)
text.innerHTML = this.linkifyText(message.html)
const time = document.createElement("div")
time.classList.add("time")
time.textContent = message.created_at
messageContent.appendChild(author)
messageContent.appendChild(text)
messageContent.appendChild(time)
element.appendChild(avatar)
element.appendChild(messageContent)
message.element = element
return element
}
addMessage(message){
const obj = new models.Message(
message.uid,
message.channel_uid,
message.user_uid,
message.user_nick,
message.color,
message.message,
message.html,
message.created_at,
message.updated_at
)
const element = this.createElement(obj)
this.messages.push(obj)
this.container.appendChild(element)
const me = this
this.messageEventSchedule.delay(() => {
me.dispatchEvent(new CustomEvent("message", {detail:obj,bubbles:true}))
})
return obj
}
scrollBottom(){
this.container.scrollTop = this.container.scrollHeight;
}
connectedCallback() {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/base.css'
this.component.appendChild(link)
this.component.classList.add("chat-messages")
this.container = document.createElement('div')
//this.container.classList.add("chat-messages")
this.component.appendChild(this.container)
this.messageEventSchedule = new Schedule(500)
this.messages = []
this.channel_uid = this.getAttribute("channel")
const me = this
app.addEventListener(this.channel_uid, (data) => {
me.addMessage(data)
})
this.dispatchEvent(new CustomEvent("rendered", {detail:this,bubbles:true}))
}
}
customElements.define('message-list', MessageListElement);
+26
View File
@@ -0,0 +1,26 @@
class MessageModel {
message = null
html = null
user_uid = null
channel_uid = null
created_at = null
updated_at = null
element = null
color = null
constructor(uid, channel_uid,user_uid,user_nick, color,message,html,created_at, updated_at){
this.uid = uid
this.message = message
this.html = html
this.user_uid = user_uid
this.user_nick = user_nick
this.color = color
this.channel_uid = channel_uid
this.created_at = created_at
this.updated_at = updated_at
}
}
const models = {
Message: MessageModel
}
+77
View File
@@ -0,0 +1,77 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.registration-container {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 400px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center;
left: calc(50%-200);
}
.registration-container h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
}
.registration-container input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #333;
border-radius: 5px;
background-color: #1a1a1a;
color: #e6e6e6;
font-size: 1em;
}
.registration-container button {
width: 100%;
padding: 10px;
background-color: #f05a28;
border: none;
border-radius: 5px;
color: white;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
}
.registration-container button:hover {
background-color: #e04924;
}
.registration-container a {
color: #f05a28;
text-decoration: none;
display: block;
margin-top: 15px;
font-size: 0.9em;
transition: color 0.3s;
}
.registration-container a:hover {
color: #e04924;
}
.error {
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
}
@media (max-width: 500px) {
.registration-container {
width: 90%;
}
}
+46
View File
@@ -0,0 +1,46 @@
class Schedule {
constructor(msDelay) {
if(!msDelay){
msDelay = 100
}
this.msDelay = msDelay
this._once = false
this.timeOutCount = 0;
this.timeOut = null
this.interval = null
}
cancelRepeat() {
clearInterval(this.interval)
this.interval = null
}
cancelDelay() {
clearTimeout(this.interval)
this.interval = null
}
repeat(func){
if(this.interval){
return false
}
this.interval = setInterval(()=>{
func()
}, this.msDelay)
}
delay(func) {
this.timeOutCount++
if(this.timeOut){
this.cancelDelay()
}
const me = this
this.timeOut = setTimeout(()=>{
clearTimeout(me.timeOut)
me.timeOut = null
func(me.timeOutCount)
me.cancelDelay()
me.timeOutCount = 0
}, this.msDelay)
}
}
+55
View File
@@ -0,0 +1,55 @@
* {
box-sizing: border-box;
}
.dialog {
background-color: #0f0f0f;
border-radius: 10px;
padding: 30px;
width: 800px;
margin: 30px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
@media screen and (max-width: 500px) {
.center {
width: 100%;
left: 0px;
}
.dialog {
width: 100%;
left: 0px;
}
}
h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
}
h2 {
font-size: 1.4em;
color: #f05a28;
margin-bottom: 20px;
}
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #e6e6e6;
line-height: 1.5;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
}
div {
text-align: left;
}
View File
View File
+113
View File
@@ -0,0 +1,113 @@
import functools
import json
from snek.system import security
cache = functools.cache
CACHE_MAX_ITEMS_DEFAULT = 5000
class Cache:
def __init__(self, app, max_items=CACHE_MAX_ITEMS_DEFAULT):
self.app = app
self.cache = {}
self.max_items = max_items
self.lru = []
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
async def get(self, args):
try:
self.lru.pop(self.lru.index(args))
except:
print("Cache miss!", args, flush=True)
return None
self.lru.insert(0, args)
while len(self.lru) > self.max_items:
self.cache.pop(self.lru[-1])
self.lru.pop()
print("Cache hit!", args, flush=True)
return self.cache[args]
def json_default(self, value):
# if hasattr(value, "to_json"):
# return value.to_json()
try:
return json.dumps(value.__dict__, default=str)
except:
return str(value)
async def create_cache_key(self, args, kwargs):
return await security.hash(
json.dumps(
{"args": args, "kwargs": kwargs},
sort_keys=True,
default=self.json_default,
)
)
async def set(self, args, result):
is_new = args not in self.cache
self.cache[args] = result
try:
self.lru.pop(self.lru.index(args))
except (ValueError, IndexError):
pass
self.lru.insert(0, args)
while len(self.lru) > self.max_items:
self.cache.pop(self.lru[-1])
self.lru.pop()
if is_new:
self.version += 1
print("Cache store! New version:", self.version, flush=True)
async def delete(self, args):
if args in self.cache:
try:
self.lru.pop(self.lru.index(args))
except IndexError:
pass
del self.cache[args]
def async_cache(self, func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
cache_key = await self.create_cache_key(args, kwargs)
cached = await self.get(cache_key)
if cached:
return cached
result = await func(*args, **kwargs)
await self.set(cache_key, result)
return result
return wrapper
def async_delete_cache(self, func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
cache_key = await self.create_cache_key(args, kwargs)
if cache_key in self.cache:
try:
self.lru.pop(self.lru.index(cache_key))
except IndexError:
pass
del self.cache[cache_key]
return await func(*args, **kwargs)
return wrapper
def async_cache(func):
cache = {}
@functools.wraps(func)
async def wrapper(*args):
if args in cache:
return cache[args]
result = await func(*args)
cache[args] = result
return result
return wrapper
+120
View File
@@ -0,0 +1,120 @@
# Written by retoor@molodetz.nl
# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data.
# This code uses the `snek.system.model` library for managing model fields.
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from snek.system import model
class HTMLElement(model.ModelField):
def __init__(
self,
id=None,
tag="div",
name=None,
html=None,
class_name=None,
text=None,
*args,
**kwargs,
):
self.tag = tag
self.text = text
self.id = id
self.class_name = class_name or name
self.html = html
super().__init__(name=name, *args, **kwargs)
async def to_json(self):
result = await super().to_json()
result["text"] = self.text
result["id"] = self.id
result["html"] = self.html
result["class_name"] = self.class_name
result["tag"] = self.tag
return result
class FormElement(HTMLElement):
pass
class FormInputElement(FormElement):
def __init__(self, type="text", place_holder=None, *args, **kwargs):
super().__init__(tag="input", *args, **kwargs)
self.place_holder = place_holder
self.type = type
async def to_json(self):
data = await super().to_json()
data["place_holder"] = self.place_holder
data["type"] = self.type
return data
class FormButtonElement(FormElement):
def __init__(self, tag="button", *args, **kwargs):
super().__init__(tag=tag, *args, **kwargs)
class Form(model.BaseModel):
@property
def html_elements(self):
return [element for element in self.fields if isinstance(element, HTMLElement)]
def set_user_data(self, data):
return super().set_user_data(data.get("fields"))
async def to_json(self, encode=False):
elements = await super().to_json()
html_elements = {}
for element in elements.keys():
if element == "is_valid":
# is_valid is async get property so we can't do getattr on it
continue
field = getattr(self, element)
if isinstance(field, HTMLElement):
try:
html_elements[element] = elements[element]
except KeyError:
pass
is_valid = all(field["is_valid"] for field in html_elements.values())
return {
"fields": html_elements,
"is_valid": is_valid,
"errors": await self.errors,
}
@property
async def errors(self):
result = []
for field in self.html_elements:
result += await field.errors
return result
@property
async def is_valid(self):
# This is not good, but timebox to resolve issue exceeded.
return False
+111
View File
@@ -0,0 +1,111 @@
# Written by retoor@molodetz.nl
# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content.
# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots.
# The MIT License (MIT)
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import pathlib
import uuid
import zlib
from urllib.parse import urljoin
import aiohttp
import imgkit
from app.cache import time_cache_async
from bs4 import BeautifulSoup
async def crc32(data):
try:
data = data.encode()
except:
pass
return "crc32" + str(zlib.crc32(data))
async def get_file(name, suffix=".cache"):
name = await crc32(name)
path = pathlib.Path(".").joinpath("cache")
if not path.exists():
path.mkdir(parents=True, exist_ok=True)
return path.joinpath(name + suffix)
async def public_touch(name=None):
path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
path.open("wb").close()
return path
async def create_site_photo(url):
loop = asyncio.get_event_loop()
if not url.startswith("https"):
url = "https://" + url
output_path = await get_file("site-screenshot-" + url, ".png")
if output_path.exists():
return output_path
output_path.touch()
def make_photo():
imgkit.from_url(url, output_path.absolute())
return output_path
return await loop.run_in_executor(None, make_photo)
async def repair_links(base_url, html_content):
soup = BeautifulSoup(html_content, "html.parser")
for tag in soup.find_all(["a", "img", "link"]):
if tag.has_attr("href") and not tag["href"].startswith("http"):
tag["href"] = urljoin(base_url, tag["href"])
if tag.has_attr("src") and not tag["src"].startswith("http"):
tag["src"] = urljoin(base_url, tag["src"])
return soup.prettify()
async def is_html_content(content: bytes):
try:
content = content.decode(errors="ignore")
except:
pass
marks = ["<html", "<img", "<p", "<span", "<div"]
try:
content = content.lower()
for mark in marks:
if mark in content:
return True
except Exception as ex:
print(ex)
return False
@time_cache_async(120)
async def get(url):
async with aiohttp.ClientSession() as session:
response = await session.get(url)
content = await response.text()
if await is_html_content(content):
content = (await repair_links(url, content)).encode()
return content
+70
View File
@@ -0,0 +1,70 @@
DEFAULT_LIMIT = 30
import typing
from snek.system.model import BaseModel
class BaseMapper:
model_class: BaseModel = None
default_limit: int = DEFAULT_LIMIT
table_name: str = None
def __init__(self, app):
self.app = app
self.default_limit = self.__class__.default_limit
@property
def db(self):
return self.app.db
async def new(self):
return self.model_class(mapper=self)
@property
def table(self):
return self.db[self.table_name]
async def get(self, uid: str = None, **kwargs) -> BaseModel:
if uid:
kwargs["uid"] = uid
record = self.table.find_one(**kwargs)
if not record:
return None
record = dict(record)
model = await self.new()
for key, value in record.items():
model[key] = value
return model
return await self.model_class.from_record(mapper=self, record=record)
async def exists(self, **kwargs):
return self.table.exists(**kwargs)
async def count(self, **kwargs) -> int:
return self.table.count(**kwargs)
async def save(self, model: BaseModel) -> bool:
if not model.record.get("uid"):
raise Exception(f"Attempt to save without uid: {model.record}.")
model.updated_at.update()
return self.table.upsert(model.record, ["uid"])
async def find(self, **kwargs) -> typing.AsyncGenerator:
if not kwargs.get("_limit"):
kwargs["_limit"] = self.default_limit
for record in self.table.find(**kwargs):
model = await self.new()
for key, value in record.items():
model[key] = value
yield model
async def query(self, sql, *args):
for record in self.db.query(sql, *args):
yield dict(record)
async def delete(self, kwargs=None) -> int:
if not kwargs or not isinstance(kwargs, dict):
raise Exception("Can't execute delete with no filter.")
return self.table.delete(**kwargs)
+81
View File
@@ -0,0 +1,81 @@
# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
from types import SimpleNamespace
from app.cache import time_cache_async
from mistune import HTMLRenderer, Markdown
from pygments import highlight
from pygments.formatters import html
from pygments.lexers import get_lexer_by_name
class MarkdownRenderer(HTMLRenderer):
_allow_harmful_protocols = True
def __init__(self, app, template):
self.template = template
self.app = app
self.env = self.app.jinja2_env
formatter = html.HtmlFormatter()
self.env.globals["highlight_styles"] = formatter.get_style_defs()
def _escape(self, str):
return str ##escape(str)
def block_code(self, code, lang=None, info=None):
if not lang:
lang = info
if not lang:
return f"<div>{code}</div>"
# return '\n<pre><code>%s</code></pre>\n' % escape(code)
lexer = get_lexer_by_name(lang, stripall=True)
formatter = html.HtmlFormatter(lineseparator="<br>")
return highlight(code, lexer, formatter)
def render(self):
markdown_string = self.app.template_path.joinpath(self.template).read_text()
renderer = MarkdownRenderer(self.app, self.template)
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)
def render_markdown_sync(app, markdown_string):
renderer = MarkdownRenderer(app, None)
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)
@time_cache_async(120)
async def render_markdown(app, markdown_string):
return render_markdown_sync(app, markdown_string)
from jinja2 import TemplateSyntaxError, nodes
from jinja2.ext import Extension
from jinja2.nodes import Const
# Source: https://ron.sh/how-to-write-a-jinja2-extension/
class MarkdownExtension(Extension):
tags = {"markdown"}
def __init__(self, environment):
self.app = SimpleNamespace(jinja2_env=environment)
super(MarkdownExtension, self).__init__(environment)
def parse(self, parser):
line_number = next(parser.stream).lineno
md_file = [Const("")]
body = ""
try:
md_file = [parser.parse_expression()]
except TemplateSyntaxError:
body = parser.parse_statements(["name:endmarkdown"], drop_needle=True)
return nodes.CallBlock(
self.call_method("_to_html", md_file), [], [], body
).set_lineno(line_number)
def _to_html(self, md_file, caller):
return render_markdown_sync(self.app, caller())
+43
View File
@@ -0,0 +1,43 @@
# Written by retoor@molodetz.nl
# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
# MIT License: This code is distributed under the MIT License.
from aiohttp import web
@web.middleware
async def no_cors_middleware(request, handler):
response = await handler(request)
response.headers.pop("Access-Control-Allow-Origin", None)
return response
@web.middleware
async def cors_allow_middleware(request, handler):
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "*"
return response
@web.middleware
async def cors_middleware(request, handler):
if request.method == "OPTIONS":
response = web.Response()
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = (
"GET, POST, PUT, DELETE, OPTIONS"
)
response.headers["Access-Control-Allow-Headers"] = "*"
return response
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "*"
return response
+373
View File
@@ -0,0 +1,373 @@
# Written by retoor@molodetz.nl
# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules.
# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects.
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import copy
import json
import re
import uuid
from collections import OrderedDict
from datetime import datetime, timezone
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
def now():
return str(datetime.now(timezone.utc))
def add_attrs(**kwargs):
def decorator(func):
for key, value in kwargs.items():
setattr(func, key, value)
return func
return decorator
def validate_attrs(
required=False, min_length=None, max_length=None, regex=None, **kwargs
):
def decorator(func):
return add_attrs(
required=required,
min_length=min_length,
max_length=max_length,
regex=regex,
**kwargs,
)(func)
class Validator:
_index = 0
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = json.loads(json.dumps(val, default=str))
@property
def initial_value(self):
return self.value
def custom_validation(self):
return True
def __init__(
self,
required=False,
min_num=None,
max_num=None,
min_length=None,
max_length=None,
regex=None,
value=None,
kind=None,
help_text=None,
app=None,
model=None,
**kwargs,
):
self.index = Validator._index
Validator._index += 1
self.app = app
self.model = model
self.required = required
self.min_num = min_num
self.max_num = max_num
self.min_length = min_length
self.max_length = max_length
self.regex = regex
self._value = None
self.value = value
self.kind = kind
self.help_text = help_text
self.__dict__.update(kwargs)
@property
async def errors(self):
error_list = []
if self.value is None and self.required:
error_list.append("Field is required.")
return error_list
if self.value is None:
return error_list
if self.kind in [int, float]:
if self.min_num is not None and self.value < self.min_num:
error_list.append(f"Field should be minimal {self.min_num}.")
if self.max_num is not None and self.value > self.max_num:
error_list.append(f"Field should be maximal {self.max_num}.")
if self.min_length is not None and len(self.value) < self.min_length:
error_list.append(
f"Field should be minimal {self.min_length} characters long."
)
if self.max_length is not None and len(self.value) > self.max_length:
error_list.append(
f"Field should be maximal {self.max_length} characters long."
)
if self.regex and self.value and not re.match(self.regex, self.value):
error_list.append("Invalid value.")
if self.kind and not isinstance(self.value, self.kind):
error_list.append(f"Invalid kind. It is supposed to be {self.kind}.")
return error_list
async def validate(self):
errors = await self.errors
if errors:
raise ValueError(f"Errors: {errors}.")
return True
@property
async def is_valid(self):
try:
await self.validate()
return True
except ValueError:
return False
async def to_json(self):
errors = await self.errors
is_valid = await self.is_valid
return {
"required": self.required,
"min_num": self.min_num,
"max_num": self.max_num,
"min_length": self.min_length,
"max_length": self.max_length,
"regex": self.regex,
"value": self.value,
"kind": str(self.kind),
"help_text": self.help_text,
"errors": errors,
"is_valid": is_valid,
"index": self.index,
}
class ModelField(Validator):
index = 1
def __init__(self, name=None, save=True, *args, **kwargs):
self.name = name
self.save = save
super().__init__(*args, **kwargs)
async def to_json(self):
result = await super().to_json()
result["name"] = self.name
return result
class CreatedField(ModelField):
@property
def initial_value(self):
return now()
def update(self):
if not self.value:
self.value = now()
class UpdatedField(ModelField):
def update(self):
self.value = now()
class DeletedField(ModelField):
def update(self):
self.value = now()
class UUIDField(ModelField):
@property
def value(self):
return str(self._value)
@value.setter
def value(self, val):
self._value = str(val)
@property
def initial_value(self):
return str(uuid.uuid4())
class BaseModel:
uid = UUIDField(name="uid", required=True)
created_at = CreatedField(
name="created_at",
required=True,
regex=TIMESTAMP_REGEX,
place_holder="Created at",
)
updated_at = UpdatedField(
name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at"
)
deleted_at = DeletedField(
name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at"
)
@classmethod
async def from_record(cls, record, mapper):
model = cls()
model.mapper = mapper
model.record = record
return model
@property
def mapper(self):
return self._mapper
@mapper.setter
def mapper(self, value):
self._mapper = value
@property
def record(self):
return {key: field.value for key, field in self.fields.items()}
@record.setter
def record(self, val):
for key, value in val.items():
field = self.fields.get(key)
if not field:
continue
self[key] = value
return self
def __init__(self, *args, **kwargs):
self._mapper = None
self.fields = {}
for key in dir(self.__class__):
obj = getattr(self.__class__, key)
if isinstance(obj, Validator):
self.__dict__[key] = copy.deepcopy(obj)
self.__dict__[key].value = kwargs.pop(
key, self.__dict__[key].initial_value
)
self.fields[key] = self.__dict__[key]
self.fields[key].model = self
self.fields[key].app = kwargs.get("app")
def __setitem__(self, key, value):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
obj.value = value
def __getattr__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
return obj.value
return obj
def set_user_data(self, data):
for key, value in data.items():
field = self.fields.get(key)
if not field:
continue
if value.get("name"):
value = value.get("value")
field.value = value
@property
async def is_valid(self):
return all([await field.is_valid for field in self.fields.values()])
def __getitem__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj, Validator):
return obj.value
def __setattr__(self, key, value):
obj = getattr(self, key)
if isinstance(obj, Validator):
obj.value = value
else:
self.__dict__[key] = value
@property
async def recordz(self):
obj = await self.to_json()
record = {}
for key, value in obj.items():
if not isinstance(value, dict) or "value" not in value:
continue
if getattr(self, key).save:
record[key] = value.get("value")
return record
async def to_json(self, encode=False):
model_data = OrderedDict(
{
"uid": self.uid.value,
"created_at": self.created_at.value,
"updated_at": self.updated_at.value,
"deleted_at": self.deleted_at.value,
"is_valid": await self.is_valid,
}
)
for key, value in self.fields.items():
if key == "record":
continue
value = self.__dict__[key]
if hasattr(value, "value"):
model_data[key] = await value.to_json()
if encode:
return json.dumps(model_data, indent=2)
return model_data
class FormElement(ModelField):
def __init__(self, place_holder=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.place_holder = place_holder
class FormElement(ModelField):
def __init__(self, place_holder=None, *args, **kwargs):
self.place_holder = place_holder
super().__init__(*args, **kwargs)
async def to_json(self):
data = await super().to_json()
data["name"] = self.name
data["place_holder"] = self.place_holder
return data
+13
View File
@@ -0,0 +1,13 @@
class Object:
def __init__(self, *args, **kwargs):
for arg in args:
if isinstance(arg, dict):
self.__dict__.update(arg)
self.__dict__.update(kwargs)
def __getitem__(self, key):
return self.__dict__[key]
def __setitem__(self, key, value):
self.__dict__[key] = value
+22
View File
@@ -0,0 +1,22 @@
import hashlib
DEFAULT_SALT = b"snekker-de-snek-"
async def hash(data, salt=DEFAULT_SALT):
try:
data = data.encode(errors="ignore")
except AttributeError:
pass
try:
salt = salt.encode(errors="ignore")
except AttributeError:
pass
salted = salt + data
obj = hashlib.sha256(salted)
return obj.hexdigest()
async def verify(string: str, hashed: str):
return await hash(string) == hashed
+67
View File
@@ -0,0 +1,67 @@
from snek.mapper import get_mapper
from snek.model.user import UserModel
from snek.system.mapper import BaseMapper
class BaseService:
mapper_name: BaseMapper = None
@property
def services(self):
return self.app.services
def __init__(self, app):
self.app = app
self.cache = app.cache
if self.mapper_name:
self.mapper = get_mapper(self.mapper_name, app=self.app)
else:
self.mapper = None
async def exists(self, uid=None, **kwargs):
if uid:
if not kwargs and await self.cache.get(uid):
return True
kwargs["uid"] = uid
return await self.count(**kwargs) > 0
async def count(self, **kwargs):
return await self.mapper.count(**kwargs)
async def new(self, **kwargs):
return await self.mapper.new()
async def query(self, sql, *args):
for record in self.app.db.query(sql, *args):
yield record
async def get(self, uid=None, **kwargs):
if uid:
if not kwargs:
result = await self.cache.get(uid)
if result:
return result
kwargs["uid"] = uid
result = await self.mapper.get(**kwargs)
if result:
await self.cache.set(result["uid"], result)
return result
async def save(self, model: UserModel):
# if model.is_valid: You Know why not
if await self.mapper.save(model):
await self.cache.set(model["uid"], model)
return True
errors = await model.errors
raise Exception(f"Couldn't save model. Errors: f{errors}")
async def find(self, **kwargs):
if not "_limit" in kwargs or int(kwargs.get("_limit")) > 30:
kwargs["_limit"] = 60
async for model in self.mapper.find(**kwargs):
yield model
async def delete(self, **kwargs):
return await self.mapper.delete(**kwargs)
+69
View File
@@ -0,0 +1,69 @@
from aiohttp import web
from snek.system.markdown import render_markdown
class BaseView(web.View):
login_required = False
async def _iter(self):
if self.login_required and not self.session.get("logged_in"):
return web.HTTPFound("/")
return await super()._iter()
@property
def app(self):
return self.request.app
@property
def db(self):
return self.app.db
@property
def services(self):
return self.app.services
async def json_response(self, data, **kwargs):
return web.json_response(data, **kwargs)
@property
def session(self):
return self.request.session
async def render_template(self, template_name, context=None):
if template_name.endswith(".md"):
response = await self.request.app.render_template(
template_name, self.request, context
)
body = await render_markdown(self.app, response.body.decode())
return web.Response(body=body, content_type="text/html")
return await self.request.app.render_template(
template_name, self.request, context
)
class BaseFormView(BaseView):
form = None
async def get(self):
form = self.form(app=self.app)
return await self.json_response(await form.to_json())
async def post(self):
form = self.form(app=self.app)
post = await self.request.json()
form.set_user_data(post["form"])
result = await form.to_json()
if post.get("action") == "validate":
# Pass
pass
if post.get("action") == "submit" and result["is_valid"]:
result = await self.submit(form)
return await self.json_response(result)
return await self.json_response(result)
async def submit(self, model=None):
print("Submit sucess")
+10
View File
@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block main %}
<div class="dialog">
<fancy-button size="auto" text="Back" url="/back"></fancy-button>
<html-frame url="/about.md"></html-frame>
</div>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
# Snek
## What is a snek?
A snek is a danger noodle.
## Design choices
I made several design choices:
- Implemented **the worst 3rd party markdown to html renderer ever**. See this nice *bullet list*.
- Only password requirement is thats it requires six characters. Users are responsibly for their own security. Snek is not so arrogant to determine if a password is strong enough. It's up to what user prefers. Snek does not have a forgot-my-password service tho.
- Email is not required for registration. Email is (maybe) used in future for resetting password.
- Database is SQLite by default. Postgres is also possible. In that case you have to change `db_path` prefix to `postgres:///` and add a postgres docker container to the docker setup.
- Homebrew made ORM framework based on dataset.
- Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.
- !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.
- No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<script src="/app.js"></script>
<script src="/message-list.js"></script>
<style>{{ highlight_styles }}</style>
<link rel="stylesheet" href="/style.css">
<script src="/fancy-button.js"></script>
<script src="/html-frame.js"></script>
<script src="/generic-form.js"></script>
<link rel="stylesheet" href="/html-frame.css"></script>
</head>
<body>
<header>
{% block header %}
{% endblock %}
</header>
<main>
{% block main %}
{% endblock %}
</main>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<script src="/fancy-button.js"></script>
<link rel="stylesheet" href="/base.css">
<link rel="stylesheet" href="/generic-form.css">
<script src="/html-frame.js"></script>
<script src="/generic-form.js"></script>
<link rel="stylesheet" href="/html-frame.css"></script>
<link rel="stylesheet" href="/register__.css">
</head>
<body>
<header>
{% block header %}
{% endblock %}
</header>
<main>
<aside class="sidebar">
{% block sidebar %}
{% endblock %}
</aside>
{% block main %}
{% endblock %}
</main>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block main %}
<div class="dialog">
<fancy-button size="auto" text="Back" url="/back"></fancy-button>
<html-frame url="/docs.md"></html-frame>
</div>
{% endblock %}
+54
View File
@@ -0,0 +1,54 @@
# API Documentation
Currently only some details about the internal API are available.
## How to create a user
```python
# Save user to the table named 'user'
# Password gets sha256 encrypted with default a salt string
# of the snek.system.security module.
new_user_object = await app.service.user.register(
username="retoor", password="retoorded"
)
```
## Encrypt string
```python
from snek.system import security
# Support for both utf and bytes.
var1 = security.encrypt("data")
var2 = security.encrypt(b"data")
# Is correct:
assert var1 == var2
```
## How to create a basic HTML / Markdown view
```python
from snek.system.view import BaseView
class IndexView(BaseView):
async def get(self):
# The render function supports markdown.
# It will render with syntax highlighting.
# Just use the .md file extension in the file name.
return await self.render("index.html")
```
## How to create a FormView
```python
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterFormView(BaseFormView):
form = RegisterForm
```
## How to register a class view
```python
app.routes.add_view("/your-page.html", YourViewClass)
```
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snek chat by Molodetz</title>
<link rel="stylesheet" href="generic-form.css">
<link rel="stylesheet" href="register__.css">
<script src="/fancy-button.js"></script>
</head>
<body>
<div class="registration-container">
<h1>Snek</h1>
<fancy-button url="/login.html" text="Login"></fancy-button>
<span style="padding:10px;">Or</span>
<fancy-button url="/register.html" text="Register"></fancy-button>
<a href="/about.html">Design choices</a>
<a href="/web.html">App preview</a>
<a href="/docs/docs/">API docs</a>
</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block main %}
<fancy-button url="/back" text="Back" size="auto"></fancy-button>
<generic-form class="center" url="/login.json"></generic-form>
{% endblock %}
+10
View File
@@ -0,0 +1,10 @@
<style>
{{highlight_styles}}
</style>
{#
<div data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}" data-message="{{message}}" class="message"><div class="avatar" style="background-color: {{color}}; color: black;">{{user_nick[0]}}</div><div class="message-content"><div class="author" style="color: {{color}};">{{user_nick}}</div><div class="text">
#}
{% markdown %}{% autoescape false %}{{ message }}{%endautoescape%}{% endmarkdown %}
{#}
</div><div class="time">{{created_at}}</div></div></div>
{#}
+7
View File
@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block main %}
<fancy-button url="/back" text="Back" size="auto"></fancy-button>
<generic-form class="center" url="/register.json"></generic-form>
{% endblock %}
+122
View File
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Form Component</title>
<style>
.form-container {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
.form-field {
margin-bottom: 15px;
}
.form-field label {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
.form-field input {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 3px;
}
.form-field .error {
color: red;
font-size: 0.9em;
margin-top: 5px;
}
</style>
</head>
<body>
<!-- Use the custom form component -->
<dynamic-form></dynamic-form>
<script>
class DynamicForm extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// Sample data for the form
const formData = {
form: {
uid: { required: true, value: "e13ad3b7-20b2-4c1a-b74e-b8c7d1abd107", html_type: "text", place_holder: "UID", is_valid: true },
created_at: { required: true, value: "2025-01-17 21:21:27.561769+00:00", html_type: "text", place_holder: "Created At", is_valid: true },
updated_at: { required: false, value: null, html_type: "text", place_holder: "Updated At", is_valid: true },
deleted_at: { required: false, value: null, html_type: "text", place_holder: "Deleted At", is_valid: true },
email: { required: true, value: null, html_type: "email", place_holder: "Email address", errors: ["Field is required."], is_valid: false },
password: { required: true, value: null, html_type: "password", place_holder: "Password", errors: ["Field is required."], is_valid: false },
username: { required: true, value: null, html_type: "text", place_holder: "Username", errors: ["Field is required."], is_valid: false }
}
};
// Render the form
this.render(formData);
}
render(data) {
const form = data.form;
const container = document.createElement('div');
container.className = 'form-container';
// Create a form element
const formElement = document.createElement('form');
// Generate form fields from the data
Object.entries(form).forEach(([fieldName, fieldData]) => {
const fieldContainer = document.createElement('div');
fieldContainer.className = 'form-field';
// Add label
const label = document.createElement('label');
label.textContent = fieldName.replace(/_/g, ' ').toUpperCase();
label.htmlFor = fieldName;
fieldContainer.appendChild(label);
// Add input field
const input = document.createElement('input');
input.type = fieldData.html_type || 'text';
input.name = fieldName;
input.value = fieldData.value || '';
input.placeholder = fieldData.place_holder || '';
input.required = fieldData.required || false;
// Append input to the container
fieldContainer.appendChild(input);
// Display validation errors
if (fieldData.errors && fieldData.errors.length > 0) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = fieldData.errors.join(', ');
fieldContainer.appendChild(errorDiv);
}
// Append field to the form
formElement.appendChild(fieldContainer);
});
// Add the form to the container
container.appendChild(formElement);
// Append the container to the shadow DOM
this.shadowRoot.appendChild(container);
}
}
// Define the custom element
customElements.define('dynamic-form', DynamicForm);
</script>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snek</title>
<script src="/html-frame.js"></script>
<script src="/schedule.js"></script>
<script src="/app.js"></script>
<script src="/models.js"></script>
<script src="/message-list.js"></script>
<script src="/message-list-manager.js"></script>
<script src="/chat-input.js"></script>
<script src="/chat-window.js"></script>
<link rel="stylesheet" href="/base.css">
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/png" href="/image/snek1.png" sizes="32x32">
</head>
<body>
<header>
<div class="logo">Snek</div>
<nav>
<a href="/web.html">Home</a>
<a style="display:none" id="install-button" href="#">Install</a>
<a href="#">Settings</a>
<a href="/logout.html">Logout</a>
</nav>
</header>
<main>
<aside class="sidebar">
<h2>Chat Rooms</h2>
<ul>
<li><a href="#">General</a></li>
<li><a href="#">Development</a></li>
<li><a href="#">Support</a></li>
<li><a href="#">Random</a></li>
</ul>
</aside>
<chat-window class="chat-area"></chat-window>
</main>
<script>
let installPrompt = null
window.addEventListener("beforeinstallprompt", async(event) => {
event.preventDefault();
installPrompt = event;
//document.addEventListener("DOMContentLoaded", () => {
const button = document.getElementById("install-button")
button.addEventListener("click", async ()=>{
const result = await installPrompt.prompt()
console.info(result.outcome)
})
button.style.display = 'block'
});
;
</script>
</body>
</html>
View File
+13
View File
@@ -0,0 +1,13 @@
from snek.system.view import BaseView
class AboutHTMLView(BaseView):
async def get(self):
return await self.render_template("about.html")
class AboutMDView(BaseView):
async def get(self):
return await self.render_template("about.md")
+13
View File
@@ -0,0 +1,13 @@
from snek.system.view import BaseView
class DocsHTMLView(BaseView):
async def get(self):
return await self.render_template("docs.html")
class DocsMDView(BaseView):
async def get(self):
return await self.render_template("docs.md")
+7
View File
@@ -0,0 +1,7 @@
from snek.system.view import BaseView
class IndexView(BaseView):
async def get(self):
return await self.render_template("index.html")
+27
View File
@@ -0,0 +1,27 @@
from aiohttp import web
from snek.form.login import LoginForm
from snek.system.view import BaseFormView
class LoginView(BaseFormView):
form = LoginForm
async def get(self):
if self.session.get("logged_in"):
return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"):
return await super().get()
return await self.render_template("login.html")
async def submit(self, form):
if await form.is_valid:
user = await self.services.user.get(username=form['username'],deleted_at=None)
# Migrate data
await self.services.user.save(user)
self.session["logged_in"] = True
self.session["username"] = user['username']
self.session["uid"] = user["uid"]
self.session["color"] = user["color"]
return {"redirect_url": "/web.html"}
return {"is_valid": False}
+14
View File
@@ -0,0 +1,14 @@
from snek.form.login import LoginForm
from snek.system.view import BaseFormView
class LoginFormView(BaseFormView):
form = LoginForm
async def submit(self, form):
if await form.is_valid:
self.session["logged_in"] = True
self.session["username"] = form.username.value
self.session["uid"] = form.uid.value
return {"redirect_url": "/web.html"}
return {"is_valid": False}
+27
View File
@@ -0,0 +1,27 @@
from aiohttp import web
from snek.system.view import BaseView
class LogoutView(BaseView):
redirect_url = "/"
login_required = True
async def get(self):
try:
del self.session["logged_in"]
del self.session["uid"]
del self.session["username"]
except KeyError:
pass
return web.HTTPFound(self.redirect_url)
async def post(self):
try:
del self.session["logged_in"]
del self.session["uid"]
del self.session["username"]
except KeyError:
pass
return await self.json_response({"redirect_url": self.redirect_url})
+26
View File
@@ -0,0 +1,26 @@
from aiohttp import web
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterView(BaseFormView):
form = RegisterForm
async def get(self):
if self.session.get("logged_in"):
return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"):
return await super().get()
return await self.render_template("register.html")
async def submit(self, form):
result = await self.app.services.user.register(
form.email.value, form.username.value, form.password.value
)
self.request.session["uid"] = result["uid"]
self.request.session["username"] = result["username"]
self.request.session["logged_in"] = True
self.request.session["color"] = result["color"]
return {"redirect_url": "/web.html"}
+16
View File
@@ -0,0 +1,16 @@
from snek.form.register import RegisterForm
from snek.system.view import BaseFormView
class RegisterFormView(BaseFormView):
form = RegisterForm
async def submit(self, form):
result = await self.app.services.user.register(
form.email.value, form.username.value, form.password.value
)
self.request.session["uid"] = result["uid"]
self.request.session["username"] = result["usernmae"]
self.request.session["logged_in"] = True
return {"redirect_url": "/web.html"}
+147
View File
@@ -0,0 +1,147 @@
from aiohttp import web
from snek.system.view import BaseView
class RPCView(BaseView):
class RPCApi:
def __init__(self,view, ws):
self.view = view
self.app = self.view.app
self.services = self.app.services
self.ws = ws
@property
def user_uid(self):
return self.view.session.get("uid")
@property
def request(self):
return self.view.request
def _require_login(self):
if not self.is_logged_in:
raise Exception("Not logged in")
@property
def is_logged_in(self):
return self.view.session.get("logged_in", False)
async def login(self, username, password):
success = await self.services.user.validate_login(username, password)
if not success:
raise Exception("Invalid username or password")
user = await self.services.user.get(username=username)
self.view.session["uid"] = user["uid"]
self.view.session["logged_in"] = True
self.view.session["username"] = user["username"]
self.view.session["user_nick"] = user["nick"]
record = user.record
del record['password']
del record['deleted_at']
await self.services.socket.add(self.ws)
async for subscription in self.services.channel_member.find(user_uid=self.view.request.session.get("uid"),deleted_at=None,is_banned=False):
await self.services.socket.subscribe(self.ws,subscription["channel_uid"])
return record
async def get_user(self, user_uid):
self._require_login()
if not user_uid:
user_uid = self.user_uid
user = await self.services.user.get(uid=user_uid)
record = user.record
del record['password']
del record['deleted_at']
if not user_uid == user["uid"]:
del record['email']
return record
async def get_messages(self, channel_uid,offset=0):
self._require_login()
messages = []
async for message in self.services.channel_message.query("SELECT * FROM channel_message ORDER BY created_at DESC LIMIT 60"): #"SELECT uid, channel_uid, user_uid, message, created_at FROM channel_message WHERE channel_uid = :channel_uid ORDER BY created_at DESC LIMIT 30 OFFSET :offset",{"channel_uid":channel_uid,"offset":int(offset)}):
user = await self.services.user.get(uid=message["user_uid"])
if not user:
print("User not found!",flush= True)
continue
messages.insert(0,dict(
uid=message["uid"],
color=user['color'],
user_uid=message["user_uid"],
channel_uid=message["channel_uid"],
user_nick=user['nick'],
message=message["message"],
created_at=message["created_at"],
html=message['html']
))
return messages
async def get_channels(self):
self._require_login()
channels = []
async for subscription in self.services.channel_member.find(user_uid=self.user_uid,is_banned=False):
channels.append(dict(
name=subscription["label"],
uid=subscription["channel_uid"],
is_moderator=subscription["is_moderator"],
is_read_only=subscription["is_read_only"]
))
return channels
async def send_message(self, room, message):
self._require_login()
await self.services.chat.send(self.user_uid,room,message)
return True
async def echo(self,*args):
self._require_login()
return args
async def __call__(self, data):
try:
call_id = data.get("callId")
method_name = data.get("method")
if method_name.startswith("_"):
raise Exception("Not allowed")
args = data.get("args")
if hasattr(super(),method_name) or not hasattr(self,method_name):
return await self.ws.send_json({"callId":call_id,"data":"Not allowed"})
method = getattr(self,method_name.replace(".","_"),None)
if not method:
raise Exception("Method not found")
result = await method(*args)
await self.ws.send_json({"callId":call_id,"success":True,"data":result})
except Exception as ex:
await self.ws.send_json({"callId":call_id,"success":False,"data":str(ex)})
async def call_ping(self,callId,*args):
return {"pong": args}
async def get(self):
ws = web.WebSocketResponse()
await ws.prepare(self.request)
if self.request.session.get("logged_in") is True:
await self.services.socket.add(ws)
async for subscription in self.services.channel_member.find(user_uid=self.request.session.get("uid"),deleted_at=None,is_banned=False):
await self.services.socket.subscribe(ws,subscription["channel_uid"])
print("Subscribed for: ", subscription["label"],flush=True)
rpc = RPCView.RPCApi(self,ws)
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
await rpc(msg.json())
elif msg.type == web.WSMsgType.ERROR:
print(f"WebSocket exception {ws.exception()}")
await self.services.socket.delete(ws)
print("WebSocket connection closed")
return ws
+47
View File
@@ -0,0 +1,47 @@
from snek.system.view import BaseView
class StatusView(BaseView):
async def get(self):
memberships = []
user = {}
if self.session.get("uid"):
user = await self.app.services.user.get(uid=self.session.get("uid"))
if not user:
return await self.json_response({"error": "User not found"}, status=404)
async for model in self.app.services.channel_member.find(
user_uid=self.session.get("uid"), deleted_at=None, is_banned=False
):
channel = await self.app.services.channel.get(uid=model["channel_uid"])
memberships.append(
{
"name": channel["label"],
"description": model["description"],
"user_uid": model["user_uid"],
"is_moderator": model["is_moderator"],
"is_read_only": model["is_read_only"],
"is_muted": model["is_muted"],
"is_banned": model["is_banned"],
"channel_uid": model["channel_uid"],
"uid": model["uid"],
}
)
user = {
"username": user["username"],
"email": user["email"],
"nick": user["nick"],
"uid": user["uid"],
"color": user['color'],
"memberships": memberships,
}
return await self.json_response(
{
"user": user,
"cache": await self.app.cache.create_cache_key(
self.app.cache.cache, None
),
}
)
+9
View File
@@ -0,0 +1,9 @@
from snek.system.view import BaseView
class WebView(BaseView):
login_required = True
async def get(self):
return await self.render_template("web.html")