Compare commits

..
9 Commits
9 changed files with 295 additions and 275 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
BIN = ./.venv/bin/
PYTHON = ./.venv/bin/python
PIP = ./.venv/bin/pip
APP_NAME=boeh
APP_NAME=rwebgui
all: install build
@@ -24,5 +24,5 @@ build:
$(PYTHON) -m build
run:
$(BIN)$(APP_NAME) --port=3028
$(BIN)$(APP_NAME).serve --port=3080
+109 -3
View File
@@ -1,4 +1,110 @@
# Boeh
# Backend Web Services
## Description
Matrix bot written in Python that says boeh everytime that Joe talks. He knows why.
## Example programs
This is all you need to write an application that does post to GPT and returns the result in the textarea. No javascript needed! Only HTML and equal naming of field names. The prompt-id of the html element should match the field name server side.
### Server side sourc
```python
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))
```
### HTML Source
```html
<div>
<textarea id="prompt" type="text" value=""></textarea>
<textarea id="answer" type="text" value=""></textarea>
<input id="submit" type="button" value="Submit" />
</div>
```
## Component using it's own service that frequently repeats.
Every method prefixed with `task_` will have it's own thread. Background tasks!
In this example, this counter (a text field, defined by HTML) has two background workers.
An task_increment that does +1 every second using pure python. Another task that only does something
every ten seconds.
### Server side source
```python
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)
```
### HTML page
```
<input id="teller1" type="text" value="[some text]" />
```
## Interactive
Here's an example of an interactive calculator that calculates your expression after typing f. No javascript need and no polling. It' a textbox, but if you would change it to a textarea, it doesn't matter. As long if it has the right javascript events.
### Eval server side execution
```python
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:
print(value)
return value
```
### HTML
```html
<input id="eval_box" type="text" value="wiii" />
```
## Random string stream from the Demo
This is the code of that random ascii banner.
```python
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)
```
-57
View File
@@ -1,57 +0,0 @@
from nio import AsyncClient, RoomMessageText
import random
class BooeehBot:
def generate_boeh(self):
boeh = "b"
for _ in range(random.randint(1, 10)):
boeh += "o"
for _ in range(random.randint(1, 5)):
boeh += "e"
for _ in range(random.randint(1, 3)):
boeh += "e"
return boeh
def __init__(self, url, username, password):
self.url = url
self.username = username
self.password = password
self.client = AsyncClient(url, username)
async def login(self):
try:
response = await self.client.login(self.password)
print(f"Logged in. Serving {self.username}.")
return response
except Exception as e:
print(f"Login error: {e}")
return None
async def handle_message(self, room, event):
specific_user_id = "@joewilliams007:matrix.org"
if isinstance(event, RoomMessageText):
if event.sender == specific_user_id:
response_text = self.generate_boeh()
try:
await self.client.room_send(
room.room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": response_text},
)
print(f"Response to {event.sender}: " + response_text)
except Exception as e:
print(f"Failed to send message: {e}")
async def start(self):
login_response = await self.login()
if not login_response:
return
self.client.add_event_callback(self.handle_message, RoomMessageText)
await self.client.sync_forever(timeout=30000)
async def stop(self):
await self.client.close()
-23
View File
@@ -1,23 +0,0 @@
import asyncio
from boeh import BooeehBot, env
async def main_async():
url = "https://matrix.org"
username = "@retoor2:matrix.org"
password = env.secret4
bot = BooeehBot(url, username, password)
try:
await bot.start()
except KeyboardInterrupt:
await bot.stop()
def main():
asyncio.run(main_async())
if __name__ == "__main__":
main()
-29
View File
@@ -1,29 +0,0 @@
import base64
import os
secret = None
secret2 = None
secret3 = None
secret4 = None
if __name__ == "__main__":
secret = input("Type secret: ")
print(base64.b64encode(secret.encode()).decode())
else:
try:
secret = base64.b64decode(os.getenv("SECRET", "").encode()).decode()
except:
pass
try:
secret2 = base64.b64decode(os.getenv("SECRET2", "").encode()).decode()
except:
pass
try:
secret3 = base64.b64decode(os.getenv("SECRET3", "").encode()).decode()
except:
pass
try:
secret4 = base64.b64decode(os.getenv("SECRET4", "").encode()).decode()
except:
pass
+33 -4
View File
@@ -1,8 +1,37 @@
from rwebgui.app import Application
from aiohttp import web
import asyncio
# Written by retoor@molodetz.nl
# This script initializes a web application using aiohttp and runs it asynchronously with a thread pool executor.
# Imports aiohttp for web server functionality and concurrent.futures for handling asynchronous execution.
#
# MIT License
#
# Copyright (c) 2023 Future Contributor
#
# 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
from concurrent.futures import ThreadPoolExecutor as Executor
from aiohttp import web
from rwebgui.app import Application
def main():
@@ -10,7 +39,7 @@ def main():
executor = Executor(max_workers=20)
loop = asyncio.get_event_loop()
loop.set_default_executor(executor)
web.run_app(app, loop=loop)
web.run_app(app, loop=loop, port=3080)
if __name__ == "__main__":
+34 -33
View File
@@ -1,12 +1,14 @@
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
import pathlib
import time
import uuid
from aiohttp import web
from app.app import Application as BaseApplication
from rwebgui.component import Component
class EvalBox(Component):
@@ -18,42 +20,44 @@ class EvalBox(Component):
try:
result = eval(value)
value = value + "= " + str(result)
await self.set_attr("value",value)
await self.set_attr("value", value)
except:
pass
except AttributeError as ex:
except AttributeError:
print(value)
return value
class Button(Component):
async def on_click(self, event):
component = self.app.search
await component.set_attr("value","Woeiii")
await component.set_attr("value", "Woeiii")
class Button1(Component):
async def on_click(self,event):
async def on_click(self, event):
field = self.app.search
await field.toggle()
value = await field.get_style("display","block")
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 self.set_attr("innerHTML", random_data)
await asyncio.sleep(0.01)
class Counter(Component):
async def task_test(self):
@@ -61,8 +65,6 @@ class Counter(Component):
await asyncio.sleep(10)
print("Slow task")
async def task_increment(self):
if not self.value:
self.value = 0
@@ -72,31 +74,36 @@ class Counter(Component):
except:
self.value = 0
self.value += 1
await self.set_attr("value",self.value)
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)
print("GOGOG", event, data)
return await super().trigger(id_, event, data)
async def on_click(self,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}
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):
def __init__(self, app, id_, description=None, ws=None):
self.time_start = time.time()
self.bytes_received = 0
super().__init__(app, id_, description, ws)
@@ -106,7 +113,7 @@ class SpeedMeter(Component):
bytes_received = self.bytes_received
self.bytes_received = 0
await self.set_attr("value","{} kb/s".format(bytes_received / 1000))
await self.set_attr("value", f"{bytes_received / 1000} kb/s")
await asyncio.sleep(1)
async def trigger(self, id_, event, data):
@@ -142,31 +149,25 @@ class Application(BaseApplication):
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_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']
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,{})
return await self.render_template("index.html", request, {})
+76 -71
View File
@@ -1,18 +1,41 @@
import uuid
import json
import time
import aiohttp
# Written by retoor@molodetz.nl
# This module defines a Component class that facilitates WebSocket communication and management of various tasks in an asynchronous environment. It allows dynamic creation of child components and interaction through callbacks and events.
# Imports used: aiohttp (external library for client-server communication)
# 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 asyncio
import time
import uuid
from aiohttp import web
class Component:
@classmethod
def define(cls):
return cls
def __init__(self,app, id_, description=None,ws: web.WebSocketResponse=None):
def __init__(self, app, id_, description=None, ws: web.WebSocketResponse = None):
self.id = id_
self.ws = ws
self.app = app
@@ -21,19 +44,19 @@ class Component:
self._callbacks = {}
self.value = None
self._running = False
if not hasattr(self,"Children"):
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 )
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)
@@ -42,7 +65,7 @@ class Component:
@classmethod
def to_json(cls):
obj = cls.__dict__ .copy()
obj = cls.__dict__.copy()
return obj
@classmethod
@@ -57,21 +80,21 @@ class Component:
@property
def tasks(self):
tasks_ = [getattr(self, name) for name in dir(self) if name.startswith("task_") and hasattr(self, name)]
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())
tasks_ += child.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
@@ -80,54 +103,50 @@ class Component:
def callbacks(self):
return hasattr(self.app, "callbacks") and self.app.callbacks or self._callbacks
async def trigger(self,id_, event,data):
async def trigger(self, id_, event, data):
if self.id == id_:
method_name = "on_"+event
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)
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):
async def call(self, method, args=None, id_=None, callback=True):
while not self.running:
await asyncio.sleep(0.1)
if not args:
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({
await self.ws.send_json(
{
"event_id": event_id,
"event": "call",
"id": id_ and id_ or self.id,
"method": method,
"args": args,
"callback": callback
})
"callback": callback,
}
)
if callback:
response = await self.communicate(event_id=event_id)
return response['result']
#print("GLUKT")
#return response['result']
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)
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)
result = await self.call("setAttr", [self.id, key, value], callback=False)
return result
async def get(self, id_):
@@ -139,20 +158,19 @@ class Component:
return child
async def set_data(self, key, value):
result = await self.call("setData", [self.id, key,value], callback=False)
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)
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)
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:
@@ -160,13 +178,10 @@ class Component:
await self.set_style("display", value)
async def get_style(self, key, default=None):
result = await self.call("getStyle", [self.id,key], default)
result = await self.call("getStyle", [self.id, key], default)
return result or default
async def on_keyup(self,event):
async def on_keyup(self, event):
value = await self.get_attr("value")
if self.value != value:
if hasattr(self, "on_change"):
@@ -174,11 +189,10 @@ class Component:
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())
tasks += child.tasks
return tasks
async def set_running(self):
@@ -196,42 +210,33 @@ class Component:
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 = {"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']
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
+8 -20
View File
@@ -20,7 +20,6 @@ const allEvents = [
'dragenter', 'dragleave', 'touchstart', 'touchmove', 'touchend',
'touchcancel', 'pointerdown', 'pointerup', 'pointermove', 'pointerover',
'pointerout', 'pointerenter', 'pointerleave', 'wheel'/*'scroll',*/
// Add more as needed
];
const props = [
'data',
@@ -104,20 +103,15 @@ const callback = (mutationList, observer) => {
}
}
};
// 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"];
class RWebGuiApp extends HTMLElement {
_ready = false
_uuid = null
ws = null
@@ -134,11 +128,16 @@ class MyCustomElement extends HTMLElement {
return this.app._ready && this.app.connected
}
get url() {
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
return `${protocol}://${window.location.host}/ws/${this.uuid}`
}
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}`)
this.ws = new WebSocket(this.url)
const me = this
this.ws.onopen = ()=>{
me.connected = true;
@@ -252,7 +251,6 @@ class MyCustomElement extends HTMLElement {
}
connectedCallback() {
console.log("Custom element added to page.");
this.rWebGui()
@@ -270,25 +268,15 @@ class MyCustomElement extends HTMLElement {
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);
customElements.define("rwebgui-app",RWebGuiApp);
/*
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM fully loaded and parsed");
document.querySelectorAll("*").forEach(child => {
child.rWebGui()
})