mirror of
https://gitlab.com/octospacc/WinDog.git
synced 2025-02-08 23:58:51 +01:00
Start work on RPC HTTP API, add call endpoint
This commit is contained in:
parent
9ccc1027f7
commit
90fbf5033c
@ -11,12 +11,16 @@ WebConfig = {
|
||||
"anti_drop_interval": 15,
|
||||
}
|
||||
|
||||
WebTokens = {}
|
||||
|
||||
""" # end windog config # """
|
||||
|
||||
import queue
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from threading import Thread
|
||||
from uuid6 import uuid7
|
||||
#from secrets import token_urlsafe
|
||||
from urllib.parse import parse_qsl
|
||||
|
||||
WebQueues = {}
|
||||
web_css_style = web_js_script = None
|
||||
@ -25,18 +29,21 @@ web_html_prefix = (lambda document_class='', head_extra='': (f'''<!DOCTYPE html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>WinDog 🐶️</title>
|
||||
<link rel="stylesheet" href="/windog.css"/>
|
||||
<script src="/windog.js">''' + '' + f'''</script>
|
||||
<script src="/windog.js"></script>
|
||||
{head_extra}</head><body>'''))
|
||||
|
||||
class WebServerClass(BaseHTTPRequestHandler):
|
||||
def parse_path(self):
|
||||
path = self.path.strip('/').lower()
|
||||
try:
|
||||
query = path.split('?')[1].split('&')
|
||||
query = path.split('?')[1]
|
||||
params = dict(parse_qsl(query))
|
||||
query = query.split('&')
|
||||
except Exception:
|
||||
query = []
|
||||
params = {}
|
||||
path = path.split('?')[0]
|
||||
return (path, path.split('/'), query)
|
||||
return (path, path.split('/'), query, params)
|
||||
|
||||
def do_redirect(self, path:str):
|
||||
self.send_response(302)
|
||||
@ -111,17 +118,19 @@ class WebServerClass(BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self):
|
||||
room_id = user_id = None
|
||||
path, fields, query = self.parse_path()
|
||||
path, fields, query, params = self.parse_path()
|
||||
if not path:
|
||||
self.init_new_room()
|
||||
elif path == "favicon.ico":
|
||||
self.send_response(404)
|
||||
elif path == "windog.css":
|
||||
self.send_text_content(web_css_style, "text/css")
|
||||
#elif path == "on-connection-dropped.css":
|
||||
# self.send_text_content('.on-connection-dropped { display: revert; } form { display: none; }', "text/css", infinite=True)
|
||||
elif path == "windog.js":
|
||||
self.send_text_content(web_js_script, "application/javascript")
|
||||
elif fields[0] == "api":
|
||||
... # TODO rest API
|
||||
elif fields[:2] == ["api", "v1"]:
|
||||
self.handle_api("GET", fields[2:], params)
|
||||
elif fields[0] == "form":
|
||||
self.send_form_html(*fields[1:3])
|
||||
else:
|
||||
@ -138,11 +147,43 @@ class WebServerClass(BaseHTTPRequestHandler):
|
||||
self.send_text_content(f'''{web_html_prefix("wrapper no-margin")}<iframe src="/{room_id}/{uuid7().hex}#load-target"></iframe>''', "text/html")
|
||||
|
||||
def do_POST(self):
|
||||
path, fields, query = self.parse_path()
|
||||
path, fields, query, params = self.parse_path()
|
||||
if path and fields[0] == 'form':
|
||||
room_id, user_id = fields[1:3]
|
||||
self.send_form_html(room_id, user_id)
|
||||
WebPushEvent(room_id, user_id, urlparse.unquote_plus(self.rfile.read(int(self.headers["Content-Length"])).decode().split('=')[1]), self.headers)
|
||||
elif fields[:2] == ["api", "v1"]:
|
||||
self.handle_api("POST", fields[2:], params)
|
||||
|
||||
def parse_command_arguments_web(self, params:dict):
|
||||
args = SafeNamespace()
|
||||
for key in params:
|
||||
if key.startswith('_'):
|
||||
args[key[1:]] = params[key]
|
||||
return args
|
||||
|
||||
def handle_api(self, verb:str, fields:list, params:dict):
|
||||
fields.append('')
|
||||
match (method := fields[0].lower()):
|
||||
case "call" if (text := obj_get(params, "text")) or (endpoint := obj_get(params, "endpoint")):
|
||||
result = call_endpoint(EventContext(), InputMessageData(
|
||||
command = TextCommandData(text) if text else SafeNamespace(
|
||||
name = endpoint,
|
||||
arguments = self.parse_command_arguments_web(params),
|
||||
body = obj_get(params, 'body')
|
||||
),
|
||||
user = UserData(
|
||||
settings = UserSettingsData(),
|
||||
),
|
||||
))
|
||||
if result:
|
||||
self.send_text_content(data_to_json(result), "application/json")
|
||||
return
|
||||
#case "getmessage":
|
||||
#case "sendmessage":
|
||||
#case "endpoints":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def WebPushEvent(room_id:str, user_id:str, text:str, headers:dict[str:str]):
|
||||
context = EventContext(platform="web", event=SafeNamespace(room_id=room_id, user_id=user_id))
|
||||
|
@ -3,18 +3,7 @@
|
||||
# Licensed under AGPLv3 by OctoSpacc #
|
||||
# ==================================== #
|
||||
|
||||
from json import dumps as json_dumps, loads as json_loads
|
||||
|
||||
def dict_filter_meta(dikt:dict):
|
||||
remove = []
|
||||
for key in dikt:
|
||||
if key.startswith('_'):
|
||||
remove.append(key)
|
||||
elif type(obj := dikt[key]) == dict:
|
||||
dikt[key] = dict_filter_meta(obj)
|
||||
for key in remove:
|
||||
dikt.pop(key)
|
||||
return dikt
|
||||
from json import dumps as json_dumps
|
||||
|
||||
# TODO: assume current room when none specified
|
||||
def get_message_wrapper(context:EventContext, data:InputMessageData):
|
||||
@ -25,8 +14,7 @@ def get_message_wrapper(context:EventContext, data:InputMessageData):
|
||||
def cDump(context:EventContext, data:InputMessageData):
|
||||
if not (message := (data.quoted or get_message_wrapper(context, data))):
|
||||
return send_status_400(context, data.user.settings.language)
|
||||
data = dict_filter_meta(json_loads(json_dumps(message, default=(lambda obj: (obj.__dict__ if not callable(obj) else None)))))
|
||||
text = json_dumps(data, indent=" ")
|
||||
text = data_to_json(message, indent=" ")
|
||||
return send_message(context, {"text_html": f'<pre>{html_escape(text)}</pre>'})
|
||||
|
||||
def cGetMessage(context:EventContext, data:InputMessageData):
|
||||
|
28
WinDog.py
28
WinDog.py
@ -44,6 +44,22 @@ def get_exception_text(full:bool=False):
|
||||
def good_yaml_load(text:str):
|
||||
return yaml_load(text.replace("\t", " "), Loader=yaml_BaseLoader)
|
||||
|
||||
def data_to_dict(data:object):
|
||||
def dict_filter_meta(dikt:dict):
|
||||
remove = []
|
||||
for key in dikt:
|
||||
if key.startswith('_'):
|
||||
remove.append(key)
|
||||
elif type(obj := dikt[key]) == dict:
|
||||
dikt[key] = dict_filter_meta(obj)
|
||||
for key in remove:
|
||||
dikt.pop(key)
|
||||
return dikt
|
||||
return dict_filter_meta(json.loads(json.dumps(data, default=(lambda obj: (obj.__dict__ if not callable(obj) else None)))))
|
||||
|
||||
def data_to_json(data:object, **jsonargs):
|
||||
return json.dumps(data_to_dict(data), **jsonargs)
|
||||
|
||||
def get_string(bank:dict, query:str, lang:str=None) -> str|list[str]|None:
|
||||
if type(result := obj_get(bank, query)) != str:
|
||||
if not (result := obj_get(bank, f"{query}.{lang or DefaultLanguage}")):
|
||||
@ -103,7 +119,7 @@ def parse_command_arguments(command, endpoint, count:int=None):
|
||||
index += 1
|
||||
return [arguments, body]
|
||||
|
||||
def TextCommandData(text:str, platform:str) -> CommandData|None:
|
||||
def TextCommandData(text:str, platform:str=None) -> CommandData|None:
|
||||
if not text:
|
||||
return None
|
||||
text = text.strip()
|
||||
@ -116,7 +132,7 @@ def TextCommandData(text:str, platform:str) -> CommandData|None:
|
||||
command.tokens = text.split()
|
||||
command.prefix = command.tokens[0][0]
|
||||
command.name, command_target = (command.tokens[0][1:].lower().split('@') + [''])[:2]
|
||||
if command_target and not (command_target == call_or_return(Platforms[platform].agent_info).tag.lower()):
|
||||
if command_target and platform and not (command_target == call_or_return(Platforms[platform].agent_info).tag.lower()):
|
||||
return None
|
||||
command.body = text[len(command.tokens[0]):].strip()
|
||||
if not (endpoint := obj_get(Endpoints, command.name)):
|
||||
@ -192,7 +208,7 @@ def send_status(context:EventContext, code:int, lang:str=None, extra:str=None, p
|
||||
return send_message(context, {"text_html": (
|
||||
(((f'{global_string(f"statuses.{code}.icon")} {global_string("error") if code >= 400 else ""}'.strip()
|
||||
+ f' {code}: {global_string(f"statuses.{code}.title")}. {summary_text if summary else ""}').strip()) if preamble else '')
|
||||
+ '\n\n' + (extra or "")).strip()})
|
||||
+ '\n\n' + (extra or "")).strip()}, status=code)
|
||||
|
||||
def send_status_400(context:EventContext, lang:str=None, extra:str=None):
|
||||
return send_status(context, 400, lang,
|
||||
@ -223,7 +239,7 @@ def get_message(context:EventContext, data:InputMessageData) -> InputMessageData
|
||||
"room": {"id": data.room.id, "url": linked.room},
|
||||
"message_url": linked.message})
|
||||
|
||||
def send_message(context:EventContext, data:OutputMessageData, *, from_sent:bool=False):
|
||||
def send_message(context:EventContext, data:OutputMessageData, *, from_sent:bool=False, status:int=200):
|
||||
context = ObjectClone(context)
|
||||
data = (OutputMessageData(**data) if type(data) == dict else data)
|
||||
if data.text_html and not data.text_plain:
|
||||
@ -243,7 +259,7 @@ def send_message(context:EventContext, data:OutputMessageData, *, from_sent:bool
|
||||
if data.ReplyTo: # TODO decide if this has to be this way
|
||||
data.ReplyTo = ':'.join(data.ReplyTo.split(':')[1:])
|
||||
if context.platform not in Platforms:
|
||||
return None
|
||||
return ObjectUnion(data, {"status": status})
|
||||
platform = Platforms[context.platform]
|
||||
if (not context.manager) and (manager := platform.manager_class):
|
||||
context.manager = call_or_return(manager)
|
||||
@ -301,7 +317,7 @@ def call_endpoint(context:EventContext, data:InputMessageData):
|
||||
context.data = data
|
||||
context.module = endpoint.module
|
||||
context.endpoint = endpoint
|
||||
if callable(agent_info := Platforms[context.platform].agent_info):
|
||||
if context.platform and callable(agent_info := Platforms[context.platform].agent_info):
|
||||
Platforms[context.platform].agent_info = agent_info()
|
||||
return endpoint.handler(context, data)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user