This commit is contained in:
retoor 2024-12-29 16:46:30 +01:00
parent 91ac6a17e8
commit 64d898c31b
7 changed files with 764 additions and 4 deletions

View File

@ -1,7 +1,7 @@
[metadata]
name = boeh
name = rwebgui
version = 1.0.0
description = Service that says boeh when Joe talks.
description = RWebGui
author = retoor
author_email = retoor@molodetz.nl
license = MIT
@ -15,11 +15,10 @@ package_dir =
python_requires = >=3.7
install_requires =
app @ git+https://retoor.molodetz.nl/retoor/app
matrix-nio
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
boeh = boeh.__main__:main
rwebgui.serve = rwebgui.__main__:main

0
src/rwebgui/__init__.py Normal file
View File

17
src/rwebgui/__main__.py Normal file
View File

@ -0,0 +1,17 @@
from rwebgui.app import Application
from aiohttp import web
import asyncio
from concurrent.futures import ThreadPoolExecutor as Executor
def main():
app = Application()
executor = Executor(max_workers=20)
loop = asyncio.get_event_loop()
loop.set_default_executor(executor)
web.run_app(app, loop=loop)
if __name__ == "__main__":
main()

172
src/rwebgui/app.py Normal file
View File

@ -0,0 +1,172 @@
import pathlib
from aiohttp import web
import uuid
from app.app import Application as BaseApplication
from rwebgui.component import Component
import traceback
import time
import asyncio
import json
class EvalBox(Component):
async def on_change(self, value):
try:
if value and value.strip().endswith("="):
value = value.strip()[:-1]
try:
result = eval(value)
value = value + "= " + str(result)
await self.set_attr("value",value)
except:
pass
except AttributeError as ex:
print(value)
return value
class Button(Component):
async def on_click(self, event):
component = self.app.search
await component.set_attr("value","Woeiii")
class Button1(Component):
async def on_click(self,event):
field = self.app.search
await field.toggle()
value = await field.get_style("display","block")
await self.set_attr("innerText", value)
class RandomString(Component):
async def task_random(self):
import random
rand_bytes = [random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(15)]
random_data = "".join(rand_bytes)
while True:
remember = random_data[0]
random_data = random_data[1:] + remember
await self.set_attr("innerHTML",random_data)
await asyncio.sleep(0.01)
class Counter(Component):
async def task_test(self):
while True:
await asyncio.sleep(10)
print("Slow task")
async def task_increment(self):
if not self.value:
self.value = 0
while True:
try:
self.value = int(self.value)
except:
self.value = 0
self.value += 1
await self.set_attr("value",self.value)
await asyncio.sleep(1)
class GPT(Component):
class Children:
prompt = Component
answer = Component
class submit(Component):
async def trigger(self, id_, event, data):
print("GOGOG",event,data)
return await super().trigger(id_, event, data)
async def on_click(self,data):
from xmlrpc.client import ServerProxy
client = ServerProxy("https://api.molodetz.nl/rpc")
prompt = await self.app.prompt.get_attr("value")
print(prompt)
exit(0)
await self.answer.set_attr("value",client.gpt4o(prompt))
return {"event_id":data['event_id'],"success":True}
class SpeedMeter(Component):
def __init__(self, app, id_, description=None, ws = None):
self.time_start = time.time()
self.bytes_received = 0
super().__init__(app, id_, description, ws)
async def task_update(self):
while True:
bytes_received = self.bytes_received
self.bytes_received = 0
await self.set_attr("value","{} kb/s".format(bytes_received / 1000))
await asyncio.sleep(1)
async def trigger(self, id_, event, data):
super().trigger(id_, event, data)
print("JAA")
self.bytes_received += len(json.dumps(data))
class App(Component):
class Children:
eval_box = EvalBox
search = Component
teller1 = Counter
teller2 = Counter
teller3 = Counter
teller4 = Counter
teller5 = Counter
teller6 = Counter
teller7 = Counter
link1 = Button
random1 = RandomString
speed = SpeedMeter
toggle = Button1
gpt = GPT
_service = None
class Application(BaseApplication):
def __init__(self):
self.location = pathlib.Path(__file__).parent
self.location_static = self.location.joinpath("static")
self.template_path = self.location.joinpath("templates")
super().__init__(template_path=self.template_path)
self.router.add_static('/static', self.location_static)
self.router.add_get("/", self.index_handler)
self.router.add_get("/ws/{uuid}", self.websocket_handler)
async def websocket_handler(self, request):
# Extract the UUID from the route
uuid_value = request.match_info['uuid']
# Validate if it's a valid UUID
try:
uuid_obj = uuid.UUID(uuid_value)
except ValueError:
return web.Response(text="Invalid UUID", status=400)
# Upgrade the connection to WebSocket
ws = web.WebSocketResponse()
await ws.prepare(request)
print(f"WebSocket connection established with UUID: {uuid_obj}")
component = App(self, "app", ws=ws)
await component.service()
return ws
async def index_handler(self, request):
return await self.render_template("index.html",request,{})

239
src/rwebgui/component.py Normal file
View File

@ -0,0 +1,239 @@
import uuid
import json
import time
import aiohttp
import asyncio
from aiohttp import web
class Component:
@classmethod
def define(cls):
return cls
def __init__(self,app, id_, description=None,ws: web.WebSocketResponse=None):
self.id = id_
self.ws = ws
self.app = app
self.description = description
self.children = []
self._callbacks = {}
self.value = None
self._running = False
if not hasattr(self,"Children"):
return
for name in dir(self.Children):
if name.startswith("__"):
continue
obj = getattr(self.Children, name)
instance = obj(app=self.app,id_=name,ws=ws )
self.add_child(instance)
instance.app = self
instance.ws = self.ws
setattr(self, name, instance)
@classmethod
def from_json(cls, json):
obj = cls(None, None)
obj.__dict__ = json
return obj
@classmethod
def to_json(cls):
obj = cls.__dict__ .copy()
return obj
@classmethod
def clone(cls):
return cls.from_json(cls.to_json())
@property
def running(self):
if not hasattr(self.app, "_running"):
return self._running
return self.app._running
@property
def tasks(self):
tasks_ = [getattr(self, name) for name in dir(self) if name.startswith("task_") and hasattr(self, name)]
for child in self.children:
tasks_ += child.tasks#.extend(await child.get_tasks())
return tasks_
async def communicate(self, event_id=None):
async for msg in self.ws:
if msg.type == web.WSMsgType.TEXT:
# Echo the message back to the client
#print(f"Received message: {msg.data}")
data = msg.json()
if not event_id:
pass
#return data
else:
if data.get("event_id") == event_id:
return data
@property
def callbacks(self):
return hasattr(self.app, "callbacks") and self.app.callbacks or self._callbacks
async def trigger(self,id_, event,data):
if self.id == id_:
method_name = "on_"+event
if hasattr(self, method_name):
method = getattr(self, method_name)
await method(data)
print("JAAJ")
for child in self.children:
await child.trigger(id_,event,data)
async def register_callback(self, event_id, callback):
self.callbacks[event_id] = callback
async def call(self, method, args=None,id_=None, callback=True):
while not self.running:
await asyncio.sleep(0.1)
if not args:
args= []
event_id = str(uuid.uuid4())
loop = asyncio.get_running_loop()
future = loop.create_future()
self.callbacks[event_id] = lambda data: future.set_result(data)
await self.ws.send_json({
"event_id": event_id,
"event": "call",
"id": id_ and id_ or self.id,
"method": method,
"args": args,
"callback": callback
})
if callback:
response = await self.communicate(event_id=event_id)
return response['result']
#print("GLUKT")
#return response['result']
return True
#return await future
async def get_attr(self, key, default=None):
result = await self.call("getAttr", [self.id, key],True)
return result or default
async def set_attr(self, key, value):
result = await self.call("setAttr", [self.id,key,value],callback=False)
return result
async def get(self, id_):
if self.id == id_:
return self
for child in self.children:
child = await child.get(id_)
if child:
return child
async def set_data(self, key, value):
result = await self.call("setData", [self.id, key,value], callback=False)
return result
async def get_data(self, key, default=None):
result = await self.call("getData", [self.id,key], default,True)
return result or default
async def set_style(self, key, value):
result = await self.call("setStyle", [self.id, key,value], callback=False)
return result
async def toggle(self):
value = await self.get_style("display", "block")
if value == "none":
value = ""
else:
value = "none"
await self.set_style("display", value)
async def get_style(self, key, default=None):
result = await self.call("getStyle", [self.id,key], default)
return result or default
async def on_keyup(self,event):
value = await self.get_attr("value")
if self.value != value:
if hasattr(self, "on_change"):
value = await self.on_change(value)
self.value = value
return self.value
async def get_tasks(self):
tasks = self.tasks
for child in self.children:
tasks += child.tasks#.extend(await child.get_tasks())
return tasks
async def set_running(self):
self._running = True
async def get_message(self):
async for msg in self.ws:
return msg
async def service(self):
tasks = self.tasks
tasks.append(self.set_running)
async def events():
try:
async for msg in self.ws:
if msg.type == web.WSMsgType.TEXT:
# Echo the message back to the client
#print(f"Received message: {msg.data}")
data = msg.json()
response = {"event_id":data['event_id'],"success":True}
response['time_start'] = time.time()
if self.callbacks.get(data['event_id']):
self.callbacks[data['event_id']](data['result'])
elif data.get('data') and not data['data'].get('id'):
response['handled'] = False
elif data.get('data'):
response['handled'] = True
response['data'] = await self.trigger(data['data']['id'], data['event'],data['data'])
response['cancel'] = True
response['time_end'] = time.time()
response['time_duration'] = response['time_end'] - response['time_start']
await self.ws.send_json(response)
#await ws.send_str(f"Echo: {msg.data}")
elif msg.type == web.WSMsgType.ERROR:
print(f"WebSocket error: {self.ws.exception()}")
except Exception as ex:
print(ex)
pass
#async def the_task():
# while True:
# time.sleep(1)
#while True:
tasks.append(events)
await asyncio.gather(*[task() for task in tasks])
#await asyncio.create_task(asyncio.gather(*[task() for task in tasks]))
#await tasks()
print("AFTERR")
def add_child(self, child):
child.app = self.app
child.ws = self.ws
self.children.append(child)

View File

@ -0,0 +1,295 @@
function elToObject(el){
obj = {}
if(el.targetElement)
el = el.targetElement
el.getAttributeNames().forEach(name => {
obj[name] = el.getAttribute(name)
if(el[name])
obj[name] = el[name]
})
return obj
}
function wrapElement(app, el){
const allEvents = [
'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseenter',
'mouseleave', 'mouseover', 'mouseout', 'keydown', 'keyup', 'keypress',
'focus', 'blur', 'change', 'input', 'submit', 'reset', 'resize',
'contextmenu', 'drag', 'drop', 'dragstart', 'dragend', 'dragover',
'dragenter', 'dragleave', 'touchstart', 'touchmove', 'touchend',
'touchcancel', 'pointerdown', 'pointerup', 'pointermove', 'pointerover',
'pointerout', 'pointerenter', 'pointerleave', 'wheel'/*'scroll',*/
// Add more as needed
];
const props = [
'data',
'id',
'isTrusted',
'altKey',
'ctrlKey',
'layerX',
'layerY',
'movementX',
'movementY',
'offsetX',
'offsetY',
'pageX',
'pageY',
'screenX',
'screenY',
'shiftKey',
'metaKey',
'value',
'code',
'keyCode',
'key'
]
el.app = app
allEvents.forEach(event => {
el.addEventListener(event, async(e) => {
if(el.app.suppress)
return
obj = {}
obj["id"] = el.id ? el.id : el._uuid
obj["uuid"] = el._uuid
obj["event"] = event
obj['attrs'] = elToObject(el)
obj['data'] = {}
props.forEach(prop => {
if(e[prop] != undefined){
obj['data'][prop] = e[prop]
}
if(e["targetElement"] && e["targetElement"][prop] != undefined){
obj['data'][prop] = e["targetElement"][prop]
}
})
//obj["data"] = JSON.stringify(e)
obj["nr"] = el.app.inc()
response = await el.app.emit(event, obj);
},false)
})
}
HTMLElement.prototype.rWebGui = function(){
const rWebGui = this
const config = { attributes: true, childList: true, subtree: true };
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(child => {
wrapElement(rWebGui, child)
if(!child._uuid){
child._uuid = rWebGui.createUUID()
}
})
} else if (mutation.type === "attributes") {
obj = {}
obj["uuid"] = mutation.target._uuid
obj[mutation.attributeName] = mutation.target.getAttribute(mutation.attributeName)
rWebGui.emit("attributeChanged", obj)
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(rWebGui, config);
// Later, you can stop observing
//observer.disconnect();
}
class MyCustomElement extends HTMLElement {
static observedAttributes = ["color", "size"];
_ready = false
_uuid = null
ws = null
_inc = 0;
connected = false
callbacks = {}
suppress = false
inc() {
this._inc++;
return this._inc
}
get isReady() {
return this.app._ready && this.app.connected
}
constructor() {
// Always call super first in constructor
super();
if(!this.parent || !this.parent.app){
this.ws = new WebSocket(`ws://${window.location.host}/ws/${this.uuid}`)
const me = this
this.ws.onopen = ()=>{
me.connected = true;
}
this.ws.onmessage = (e)=>{
const data = JSON.parse(e.data)
if(data.event_id){
if(me.callbacks[data.event_id])
{
me.callbacks[data.event_id](data)
}else if(data.event == "call"){
const method = me[data.method]
if(!method){
data['success'] = false;
data['result'] = null;
}else{
let response = method(...data.args)
data['result'] = response
data['success'] = true
}
if(data['callback'])
me.ws.send(JSON.stringify(
data
))
}else {
if(data.event == "set_attr"){
const el = document.getElementById(data['id'])
if(el){
data['data'].forEach(attr=>{
el.setAttribute(attr['name'], attr['value'])
el[attr['name']] = attr['value']
})}
}
}
}
}
}
}
getAttr(id,key){
const el = document.getElementById(id)
if (el[key] != undefined)
return el[key]
return el.getAttribute(key)
}
getData(id,key){
const el = document.getElementById(id)
return el.dataset[key]
}
setData(id,key,value){
const el = document.getElementById(id)
el.dataset[key] = value
return true
}
setStyle(id, key, value){
const el = document.getElementById(id)
el.style[key] = value
}
getStyle(id, key){
return document.getElementById(id).style[key]
}
setAttr(id, key, value) {
try{
document.getElementById(id).setAttribute(key, value)
document.getElementById(id)[key] = value
}catch(e){
console.error("Element not found:", key)
console.error("Failed to set value:", value)
}
return true
}
get isApp(){
return !this.parent || !this.parent.app
}
emit(event, data) {
if(!this.app.isReady)
return false;
const me = this
return new Promise(resolve => {
data["event_id"] = me.inc()
me.callbacks[data["event_id"]] = resolve
me.app.ws.send(JSON.stringify({"uuid":obj.uuid,"event_id":data["event_id"], "event": event, "data": data}))
}).then(res => {
return res
})
}
get uuid() {
if(!this._uuid){
this._uuid = this.createUUID()
}
return this._uuid
}
createUUID(){
const uuid = crypto.randomUUID();
return uuid
}
get app() {
if(!this.parent)
return this
if(!this.parent.app)
return this
return this.parent.app
}
connectedCallback() {
console.log("Custom element added to page.");
this.rWebGui()
if(!this.isApp){
this._uuid = this.generateUUID()
this._ready = true;
return;
}
// this.querySelectorAll("*").forEach(child => {
// child.rWebGui()
// })
/// this.dataset.uuid = this.generateUUID();
this._ready = true
}
disconnectedCallback() {
console.log("Custom element removed from page.");
}
adoptedCallback() {
console.log("Custom element moved to new page.");
}
attributeChangedCallback(name, oldValue, newValue) {
this.emit("attributeChanged", {aa:123})
console.log(`Attribute ${name} has changed.`);
}
}
customElements.define("rwebgui-app", MyCustomElement);
/*
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM fully loaded and parsed");
document.querySelectorAll("*").forEach(child => {
child.rWebGui()
})
})*/;

View File

@ -0,0 +1,38 @@
<html>
<head>
<script src="/static/rwebgui.js"></script>
</head>
<body>
<rwebgui-app id="app">
<h1>I do stuff</h1>
<h2 id="header2">I do stuff</h1>
<h3>I do stuff</h1>
<h4 id="header4">I do stuff</h1>
<input id="eval_box" type="text" value="wiii" />
<input id="search" type="text" value="wiii" />
<input id="teller1" type="text" value="wiii" />
<input id="teller2" type="text" value="wiii" />
<input id="teller3" type="text" value="wiii" />
<input id="teller4" type="text" value="wiii" />
<input id="teller5" type="text" value="wiii" />
<input id="teller6" type="text" value="wiii" />
<input id="teller7" type="text" value="wiii" />
<a href="#" id="link1">I do stuff</a>
<a href="#" id="toggle">I do stuff</a>
<input id="speed" type="text" value="wiii" />
<div id="random1"></div>
<div >
<textarea id="prompt" type="text" value=""></textarea>
<textarea id="answer" type="text" value=""></textarea>
<input id="submit" type="button" value="Submit" />
</div>
</rwebgui-app>
</body>
</html>