Updated Web backend now with multi-user, misc data and endpoints improvements

This commit is contained in:
2024-08-12 02:03:47 +02:00
parent 6ebc68127e
commit 5ba0df43c4
16 changed files with 341 additions and 180 deletions

View File

@ -29,9 +29,11 @@ class Room(Entity):
Db.create_tables([EntitySettings, User, Room], safe=True) Db.create_tables([EntitySettings, User, Room], safe=True)
class UserSettingsData(): class UserSettingsData():
def __new__(cls, user_id:str) -> SafeNamespace|None: def __new__(cls, user_id:str=None) -> SafeNamespace:
settings = None
try: try:
return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get()) settings = EntitySettings.select().join(User).where(User.id == user_id).dicts().get()
except EntitySettings.DoesNotExist: except EntitySettings.DoesNotExist:
return None pass
return SafeNamespace(**(settings or {}), _exists=bool(settings))

View File

@ -16,7 +16,7 @@ import mastodon
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from magic import Magic from magic import Magic
def MastodonMain() -> bool: def MastodonMain(path:str) -> bool:
if not (MastodonUrl and MastodonToken): if not (MastodonUrl and MastodonToken):
return False return False
Mastodon = mastodon.Mastodon(api_base_url=MastodonUrl, access_token=MastodonToken) Mastodon = mastodon.Mastodon(api_base_url=MastodonUrl, access_token=MastodonToken)
@ -40,7 +40,7 @@ def MastodonMakeInputMessageData(status:dict) -> InputMessageData:
id = ("mastodon:" + strip_url_scheme(status["account"]["uri"])), id = ("mastodon:" + strip_url_scheme(status["account"]["uri"])),
name = status["account"]["display_name"], name = status["account"]["display_name"],
) )
data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace()) data.user.settings = UserSettingsData(data.user.id)
return data return data
def MastodonHandler(event, Mastodon): def MastodonHandler(event, Mastodon):

View File

@ -25,7 +25,7 @@ import queue
MatrixClient = None MatrixClient = None
MatrixQueue = queue.Queue() MatrixQueue = queue.Queue()
def MatrixMain() -> bool: def MatrixMain(path:str) -> bool:
if not (MatrixUrl and MatrixUsername and (MatrixPassword or MatrixToken)): if not (MatrixUrl and MatrixUsername and (MatrixPassword or MatrixToken)):
return False return False
def upgrade_username(new:str): def upgrade_username(new:str):
@ -73,7 +73,7 @@ def MatrixMakeInputMessageData(room:nio.MatrixRoom, event:nio.RoomMessage) -> In
_, _, server_name, media_id = mxc_url.split('/') _, _, server_name, media_id = mxc_url.split('/')
data.media["url"] = ("https://" + server_name + nio.Api.download(server_name, media_id)[1]) data.media["url"] = ("https://" + server_name + nio.Api.download(server_name, media_id)[1])
data.command = TextCommandData(data.text_plain, "matrix") data.command = TextCommandData(data.text_plain, "matrix")
data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace()) data.user.settings = UserSettingsData(data.user.id)
return data return data
async def MatrixInviteHandler(room:nio.MatrixRoom, event:nio.InviteEvent) -> None: async def MatrixInviteHandler(room:nio.MatrixRoom, event:nio.InviteEvent) -> None:

View File

@ -20,7 +20,7 @@ from telegram.ext import CommandHandler, MessageHandler, Filters, CallbackContex
TelegramClient = None TelegramClient = None
def TelegramMain() -> bool: def TelegramMain(path:str) -> bool:
if not TelegramToken: if not TelegramToken:
return False return False
global TelegramClient global TelegramClient
@ -55,7 +55,7 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
), ),
) )
data.command = TextCommandData(data.text_plain, "telegram") data.command = TextCommandData(data.text_plain, "telegram")
data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace()) data.user.settings = UserSettingsData(data.user.id)
linked = TelegramLinker(data) linked = TelegramLinker(data)
data.message_url = linked.message data.message_url = linked.message
data.room.url = linked.room data.room.url = linked.room

View File

@ -8,78 +8,191 @@
WebConfig = { WebConfig = {
"host": ("0.0.0.0", 30264), "host": ("0.0.0.0", 30264),
"url": "https://windog.octt.eu.org", "url": "https://windog.octt.eu.org",
"anti_drop_interval": 15,
} }
""" # end windog config # """ """ # end windog config # """
import queue import queue
from http.server import BaseHTTPRequestHandler from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from LibWinDog.Platforms.Web.multithread_http_server import MultiThreadHttpServer from threading import Thread
from uuid6 import uuid7
WebQueues = {} WebQueues = {}
web_css_style = web_js_script = None
default_css = """<style> web_html_prefix = (lambda document_class='', head_extra='': (f'''<!DOCTYPE html><html class="{document_class}"><head>
* { box-sizing: border-box; } <meta charset="UTF-8"/>
iframe { width: 100%; height: 3em; top: 0; position: sticky; } <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
textarea { width: calc(100% - 3em); height: 2em; } <title>WinDog 🐶️</title>
input { width: 2em; } <link rel="stylesheet" href="/windog.css"/>
</style>""" <script src="/windog.js">''' + '' + f'''</script>
{head_extra}</head><body>'''))
class WebServerClass(BaseHTTPRequestHandler): class WebServerClass(BaseHTTPRequestHandler):
def do_GET(self): def parse_path(self):
if self.path == '/': path = self.path.strip('/').lower()
uuid = str(time.time()) try:
WebQueues[uuid] = queue.Queue() query = path.split('?')[1].split('&')
except Exception:
query = []
path = path.split('?')[0]
return (path, path.split('/'), query)
def do_redirect(self, path:str):
self.send_response(302) self.send_response(302)
self.send_header("Location", f"/{uuid}") self.send_header("Location", path)
self.end_headers() self.end_headers()
return
uuid = self.path.split('/')[-1] def send_text_content(self, text:str, mime:str, infinite:bool=False):
if self.path.startswith("/form/"):
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/html; charset=UTF-8") self.send_header("Content-Type", f"{mime}; charset=UTF-8")
self.end_headers() self.end_headers()
self.wfile.write(f'{default_css}<form method="POST" action="/form/{uuid}"><textarea name="text"></textarea><input type="submit" value="📤️"/></form>'.encode()) self.wfile.write(text.encode())
return if infinite:
while True:
time.sleep(9**9)
def init_new_room(self, room_id:str=None):
if not room_id:
room_id = str(uuid7().hex)
WebQueues[room_id] = {}#{"0": queue.Queue()}
#WebPushEvent(room_id, ".start", self.headers)
#Thread(target=lambda:WebAntiDropEnqueue(room_id)).start()
self.do_redirect(f"/{room_id}")
def handle_room_chat(self, room_id:str, user_id:str, is_redirected:bool=False):
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/html; charset=UTF-8") self.send_header("Content-Type", "text/html; charset=UTF-8")
self.send_header("Content-Encoding", "chunked") self.send_header("Content-Encoding", "chunked")
self.end_headers() self.end_headers()
self.wfile.write(f'{default_css}<h3><a href="/">WinDog</a></h3><iframe src="/form/{uuid}"></iframe>'.encode()) target = f"/{room_id}/{user_id}?page-target=1#load-target"
while True: # this apparently makes us lose threads and the web bot becomes unusable, we should handle dropped connections if not is_redirected:
return self.wfile.write(f'''{web_html_prefix(head_extra=f'<meta http-equiv="refresh" content="0; url={target}">')}
<h3><a href="/" target="_parent">WinDog 🐶️</a></h3>
<p>Initializing... <a href="{target}">Click here</a> if you are not automatically redirected.</p>'''.encode())
self.wfile.write(f'''{web_html_prefix()}
<h3><a href="/" target="_parent">WinDog 🐶️</a></h3>
<div class="sticky-box">
<p id="load-target" style="display: none;"><span style="color: red;">Background loading seems to have stopped...</span> Please open a new chat or <a href="{target}">reload this one</a> if you can't send new messages.</p>
<div class="input-frame"><iframe src="/form/{room_id}/{user_id}"></iframe></div>
</div>
<div style="display: flex; flex-direction: column-reverse;">'''.encode())
while True:
# TODO this apparently makes us lose threads, we should handle dropped connections?
try: try:
self.wfile.write(("<p>" + WebQueues[uuid].get(block=False).text_html + "</p>").encode()) self.wfile.write(WebMakeMessageHtml(WebQueues[room_id][user_id].get(block=False), user_id).encode())
except queue.Empty: except queue.Empty:
time.sleep(0.01) time.sleep(0.01)
def do_POST(self): def send_form_html(self, room_id:str, user_id:str):
uuid = self.path.split('/')[-1] self.send_text_content((f'''{web_html_prefix("form no-margin")}
text = urlparse.unquote_plus(self.rfile.read(int(self.headers["Content-Length"])).decode().split('=')[1]) <!--<link rel="stylesheet" href="/on-connection-dropped.css"/>-->
self.send_response(302) <!--<p class="on-connection-dropped">Connection dropped! Please <a href="/" target="_parent">open a new chat</a>.</p>-->
self.send_header("Location", f"/form/{uuid}") <form method="POST" action="/form/{room_id}/{user_id}" onsubmit="''' + '''(function(event){
self.end_headers() var textEl = event.target.querySelector('[name=\\'text\\']');
data = WebMakeInputMessageData(text, uuid) textEl.value = textEl.value.trim();
OnInputMessageParsed(data) window.parent.inputFrameResize();
call_endpoint(EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data) if (!textEl.value) {
event.preventDefault();
}
})(event);''' + '''">
<textarea name="text" required="true" autofocus="true" placeholder="Type something..." onkeydown="''' + '''(function(event){
var submitOnEnterSimple = !event.shiftKey;
var submitOnEnterCombo = (event.shiftKey || event.ctrlKey);
if ((event.keyCode === 13) && submitOnEnterCombo) {
event.preventDefault();
event.target.parentElement.querySelector('input[type=\\'submit\\']').click();
}
})(event);''' + '''" oninput="window.parent.inputFrameResize();"></textarea>
<!--<input type="text" name="text" required="true" autofocus="true" placeholder="Type something..."/>-->
<input type="submit" value="📤️"/>
</form>'''), "text/html")
def WebMakeInputMessageData(text:str, uuid:str) -> InputMessageData: def do_GET(self):
return InputMessageData( room_id = user_id = None
path, fields, query = self.parse_path()
if not path:
self.init_new_room()
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[0] == "form":
self.send_form_html(*fields[1:3])
else:
room_id, user_id = (fields + [None])[0:2]
if room_id not in WebQueues:
self.init_new_room(room_id if (len(room_id) >= 32) else None)
if user_id:
if user_id not in WebQueues[room_id]:
WebQueues[room_id][user_id] = queue.Queue()
WebPushEvent(room_id, user_id, ".start", self.headers)
Thread(target=lambda:WebAntiDropEnqueue(room_id, user_id)).start()
self.handle_room_chat(room_id, user_id, ("page-target=1" in query))
else:
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()
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)
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))
data = InputMessageData(
text_plain = text, text_plain = text,
text_html = html_escape(text),
command = TextCommandData(text, "web"), command = TextCommandData(text, "web"),
room = SafeNamespace( room = SafeNamespace(
id = f"web:{uuid}", id = f"web:{room_id}",
), ),
user = UserData( user = UserData(
settings = SafeNamespace(), id = f"web:{user_id}",
name = ("User" + str(abs(hash('\n'.join([f"{key}: {headers[key]}" for key in headers if key.lower() in ["accept", "accept-language", "accept-encoding", "dnt", "user-agent"]]))) % (10**8))),
settings = UserSettingsData(),
), ),
) )
OnInputMessageParsed(data)
WebSender(context, ObjectUnion(data, {"from_user": True}))
call_endpoint(context, data)
def WebMain() -> None: def WebMakeMessageHtml(message:MessageData, for_user_id:str):
server = MultiThreadHttpServer(WebConfig["host"], 32, WebServerClass) if not (message.text_html or message.media):
server.start(background=True) return "<!---->"
user_id = (message.user and message.user.id and message.user.id.split(':')[1])
# NOTE: this doesn't handle tags with attributes!
if message.text_html and (f"{message.text_html}<".split('>')[0].split('<')[1] not in ['p', 'pre']):
message.text_html = f'<p>{message.text_html}</p>'
return (f'<div class="message {"from-self" if (user_id == for_user_id) else ""} {f"color-{(int(user_id, 16) % 6) + 1}" if (user_id and (user_id != for_user_id)) else ""}">'
+ f'<span class="name">{(message.user and message.user.name) or "WinDog"}</span>'
+ (message.text_html.replace('\n', "<br />") if message.text_html else '')
+ (''.join([f'<p><img src="{medium.url}"/></p>' for medium in message.media]) if message.media else '')
+ '</div>')
def WebMain(path:str) -> bool:
global web_css_style, web_js_script
web_css_style = open(f"{path}/windog.css", 'r').read()
web_js_script = open(f"{path}/windog.js", 'r').read()
Thread(target=lambda:ThreadingHTTPServer(WebConfig["host"], WebServerClass).serve_forever()).start()
return True
def WebSender(context:EventContext, data:OutputMessageData) -> None: def WebSender(context:EventContext, data:OutputMessageData) -> None:
WebQueues[context.event.room_id].put(data) #WebQueues[context.event.room_id][context.event.user_id].put(data)
for user_id in (room := WebQueues[context.event.room_id]):
room[user_id].put(data)
# prevent browser from closing connection after ~1 minute of inactivity, by continuously sending empty, invisible messages
# TODO maybe there should exist only a single thread with this function handling all queues? otherwise we're probably wasting many threads!
def WebAntiDropEnqueue(room_id:str, user_id:str):
while True:
WebQueues[room_id][user_id].put(OutputMessageData())
time.sleep(WebConfig["anti_drop_interval"])
RegisterPlatform(name="Web", main=WebMain, sender=WebSender) RegisterPlatform(name="Web", main=WebMain, sender=WebSender)

View File

@ -1,119 +0,0 @@
#!/usr/bin/env python
"""
MIT License
Copyright (c) 2018 Ortis (cao.ortis.org@gmail.com)
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 socket
import threading
import time
from http.server import HTTPServer
import logging
class MultiThreadHttpServer:
def __init__(self, host, parallelism, http_handler_class, request_callback=None, log=None):
"""
:param host: host to bind. example: '127.0.0.1:80'
:param parallelism: number of thread listener and backlog
:param http_handler_class: the handler class extending BaseHTTPRequestHandler
:param request_callback: callback on incoming request. This method can be accede in the HTTPHandler instance.
Example: self.server.request_callback(
'GET', # specify http method
self # pass the HTTPHandler instance
)
"""
self.host = host
self.parallelism = parallelism
self.http_handler_class = http_handler_class
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.request_callback = request_callback
self.connection_handlers = []
self.stop_requested = False
self.log = log
def start(self, background=False):
self.socket.bind(self.host)
self.socket.listen(self.parallelism)
if self.log is not None:
self.log.debug("Creating "+str(self.parallelism)+" connection handler")
for i in range(self.parallelism):
ch = ConnectionHandler(self.socket, self.http_handler_class, self.request_callback)
ch.start()
self.connection_handlers.append(ch)
if background:
if self.log is not None:
self.log.debug("Serving (background thread)")
threading.Thread(target=self.__serve).start()
else:
if self.log is not None:
self.log.debug("Serving (current thread)")
self.__serve()
def stop(self):
self.stop_requested = True
for ch in self.connection_handlers:
ch.stop()
def __serve(self):
"""
Serve until stop() is called. Blocking method
:return:
"""
while not self.stop_requested:
time.sleep(1)
class ConnectionHandler(threading.Thread, HTTPServer):
def __init__(self, sock, http_handler_class, request_callback=None):
HTTPServer.__init__(self, sock.getsockname(), http_handler_class, False)
self.socket = sock
self.server_bind = self.server_close = lambda self: None
self.HTTPHandler = http_handler_class
self.request_callback = request_callback
threading.Thread.__init__(self)
self.daemon = True
self.stop_requested = False
def stop(self):
self.stop_requested = True
def run(self):
""" Each thread process request forever"""
self.serve_forever()
def serve_forever(self):
""" Handle requests until stopped """
while not self.stop_requested:
self.handle_request()
print("Finish" + str(threading.current_thread()))

View File

@ -0,0 +1 @@
uuid6

View File

@ -0,0 +1,121 @@
:root {
--bg-color: #fff;
--bg-color-2: #ddd;
--fg-color: #000;
}
* {
box-sizing: border-box;
}
html.no-margin > body {
margin: 0;
}
body {
color: var(--fg-color);
background-color: var(--bg-color);
}
html.wrapper > body {
overflow: hidden;
}
html.wrapper > body > iframe {
width: 100vw;
height: 100vh;
border: none;
}
form > * {
min-height: 2em;
height: 98vh; height: calc(100vh - 1em);
}
form > input[type="text"], form > textarea {
width: 93%; width: calc(100% - 4em);
resize: none;
}
form > input[type="submit"] {
width: 3em;
float: right;
}
/* .on-connection-dropped {
color: red;
margin: 0;
display: none;
} */
.sticky-box {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--bg-color);
}
.input-frame {
overflow: auto;
height: 4em; height: calc(4em + 4px);
min-height: 4em; min-height: calc(4em + 4px);
resize: vertical;
border-bottom: 2px dotted var(--fg-color);
padding-top: 1em;
margin-bottom: 1em;
}
.input-frame > iframe {
width: 100%;
height: 100%; height: calc(100% - 1em);
border: none;
}
.message {
margin: 0;
padding-left: 1em;
padding-right: 1em;
text-align: left;
background: linear-gradient(to left, var(--bg-color), var(--bg-color-2));
}
.message.from-self {
text-align: right;
background: linear-gradient(to left, var(--bg-color-2), var(--bg-color));
}
.message.blue, .message.color-1 {
background: linear-gradient(to left, var(--bg-color), #ccf);
}
.message.red, .message.color-2 {
background: linear-gradient(to left, var(--bg-color), #fcc);
}
.message.green, .message.color-3 {
background: linear-gradient(to left, var(--bg-color), #dfd);
}
.message.purple, .message.color-4 {
background: linear-gradient(to left, var(--bg-color), #fdf);
}
.message.cyan, .message.color-5 {
background: linear-gradient(to left, var(--bg-color), #dff);
}
.message.yellow, .message.color-6 {
background: linear-gradient(to left, var(--bg-color), #ffd);
}
/* .message:last-child { position: sticky; bottom: 0; } */
/* TODO .message:last-child-minus-1 { position: sticky; bottom: 0; } */
.message * {
max-width: 100%;
width: auto;
height: auto;
}
.message p {
word-break: break-word;
}
.message pre {
overflow-x: auto;
padding-bottom: 1em;
}
.message img,
.message video {
max-height: 80vh; max-height: calc(90vh - 6em);
}
.message > .name {
display: inline-block;
padding: 1em;
padding-bottom: 0;
font-style: italic;
font-size: small;
}
#load-target:target {
display: revert !important;
padding-top: 1em;
}

View File

@ -0,0 +1,30 @@
window.inputFrameResize = (function(height){
var frameEl = document.querySelector('.input-frame');
var frameWindow = frameEl.querySelector('iframe').contentWindow;
var textEl = frameWindow.document.querySelector('form > [name="text"]');
textEl.style.minHeight = 0;
frameEl.style.height = '1em';
//if (textEl.scrollHeight > frameWindow.document.documentElement.clientHeight) {
if (textEl.scrollHeight / parseInt(getComputedStyle(textEl).height.slice(0, -2)) < 5) {
frameEl.style.height = ('calc(3em + ' + (textEl.scrollHeight + 4) + 'px)');
frameEl.dataset.scrollHeightOld = textEl.scrollHeight;
} else {
frameEl.style.height = ('calc(3em + ' + (parseInt(frameEl.dataset.scrollHeightOld) + 4) + 'px)');
}
textEl.style.minHeight = null;
/* if (!frameEl.dataset.height) {
frameEl.dataset.height = 0;
}
if (frameEl.dataset.height > )
frameEl.style.height = ('calc(4em + ' + height + 'px)');
frameEl.dataset.height = height;
*/
});
if (document.documentElement.className.split(' ').includes('form')) {
var intervalFocus = setInterval(function(){
try {
document.querySelector('form > [name="text"]').focus();
clearInterval(intervalFocus);
} catch(err) {}
}, 100);
}

View File

@ -6,12 +6,21 @@
from types import SimpleNamespace from types import SimpleNamespace
class DictNamespace(SimpleNamespace): class DictNamespace(SimpleNamespace):
def __init__(self, **kwargs):
for key in kwargs:
if type(kwargs[key]) == dict:
kwargs[key] = self.__class__(**kwargs[key])
return super().__init__(**kwargs)
def __iter__(self): def __iter__(self):
return self.__dict__.__iter__() return self.__dict__.__iter__()
def __getitem__(self, key): def __getitem__(self, key):
return self.__getattribute__(key) return self.__getattribute__(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
return self.__setattr__(key, value) return self.__setattr__(key, value)
#def __setattr__(self, key, value):
#if type(value) == dict:
#value = self.__class__(**value)
#return super().__setattr__(key, value)
class SafeNamespace(DictNamespace): class SafeNamespace(DictNamespace):
def __getattribute__(self, key): def __getattribute__(self, key):

View File

@ -18,11 +18,13 @@ UserSettingsLimits = {
def cConfig(context:EventContext, data:InputMessageData): def cConfig(context:EventContext, data:InputMessageData):
language = data.user.settings.language language = data.user.settings.language
if not (settings := UserSettingsData(data.user.id)): if (key := data.command.arguments.get):
key = key.lower()
if (not key) or (key not in UserSettingsLimits):
return send_status_400(context, language)
if not (settings := UserSettingsData(data.user.id))._exists:
User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute() User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute()
settings = UserSettingsData(data.user.id) settings = UserSettingsData(data.user.id)
if not (key := data.command.arguments.get) or (key not in UserSettingsLimits):
return send_status_400(context, language)
if (value := data.command.body): if (value := data.command.body):
if len(value) > UserSettingsLimits[key]: if len(value) > UserSettingsLimits[key]:
return send_status(context, 500, language) return send_status(context, 500, language)
@ -52,7 +54,7 @@ def cPing(context:EventContext, data:InputMessageData):
RegisterModule(name="Base", endpoints=[ RegisterModule(name="Base", endpoints=[
SafeNamespace(names=["source"], handler=cSource), SafeNamespace(names=["source"], handler=cSource),
SafeNamespace(names=["config"], handler=cConfig, body=False, arguments={ SafeNamespace(names=["config", "settings"], handler=cConfig, body=False, arguments={
"get": True, "get": True,
}), }),
#SafeNamespace(names=["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr), #SafeNamespace(names=["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr),

View File

@ -7,7 +7,7 @@ def cEcho(context:EventContext, data:InputMessageData):
if not (text := data.command.body): if not (text := data.command.body):
return send_message(context, { return send_message(context, {
"text_html": context.endpoint.get_string("empty", data.user.settings.language)}) "text_html": context.endpoint.get_string("empty", data.user.settings.language)})
prefix = f'<a href="{data.message_url}">🗣️</a> ' prefix = f'<a href="{data.message_url or ""}">🗣️</a> '
if len(data.command.tokens) == 2: # text is a single word if len(data.command.tokens) == 2: # text is a single word
nonascii = True nonascii = True
for char in data.command.tokens[1]: for char in data.command.tokens[1]:

View File

@ -20,7 +20,8 @@ def cHelp(context:EventContext, data:InputMessageData) -> None:
text += (f"\n\n{module}" + (f": {summary}" if summary else '')) text += (f"\n\n{module}" + (f": {summary}" if summary else ''))
for endpoint in endpoints: for endpoint in endpoints:
summary = Modules[module].get_string(f"endpoints.{endpoint.names[0]}.summary", language) summary = Modules[module].get_string(f"endpoints.{endpoint.names[0]}.summary", language)
text += (f"\n* {prefix}{', {prefix}'.join(endpoint.names)}" + (f": {summary}" if summary else '')) text += (f"\n* {prefix}{f', {prefix}'.join(endpoint.names)}"
+ (f": {summary}" if summary else ''))
text = text.strip() text = text.strip()
return send_message(context, {"text_html": text}) return send_message(context, {"text_html": text})

View File

@ -10,6 +10,7 @@ MicrosoftBingSettings = {}
""" # end windog config # """ """ # end windog config # """
from urlextract import URLExtract from urlextract import URLExtract
from urllib import parse as urlparse
from urllib.request import urlopen, Request from urllib.request import urlopen, Request
def RandomHexString(length:int) -> str: def RandomHexString(length:int) -> str:

View File

@ -29,7 +29,7 @@ def cExec(context:EventContext, data:InputMessageData):
def cRestart(context:EventContext, data:InputMessageData): def cRestart(context:EventContext, data:InputMessageData):
if (data.user.id not in AdminIds) and (data.user.tag not in AdminIds): if (data.user.id not in AdminIds) and (data.user.tag not in AdminIds):
return send_message(context, {"text_plain": "Permission denied."}) return send_status(context, 403, data.user.settings.language)
open("./.WinDog.Restart.lock", 'w').close() open("./.WinDog.Restart.lock", 'w').close()
return send_message(context, {"text_plain": "Bot restart queued."}) return send_message(context, {"text_plain": "Bot restart queued."})

View File

@ -328,7 +328,7 @@ def app_main() -> None:
#SetupDb() #SetupDb()
app_log(f"📨️ Initializing Platforms... ", newline=False) app_log(f"📨️ Initializing Platforms... ", newline=False)
for platform in Platforms.values(): for platform in Platforms.values():
if platform.main(): if platform.main(f"./LibWinDog/Platforms/{platform.name}"):
app_log(f"{platform.name}, ", inline=True) app_log(f"{platform.name}, ", inline=True)
app_log("...Done. ✅️", inline=True, newline=True) app_log("...Done. ✅️", inline=True, newline=True)
app_log("🐶️ WinDog Ready!") app_log("🐶️ WinDog Ready!")