mirror of
https://gitlab.com/octospacc/WinDog.git
synced 2025-06-05 22:09:20 +02:00
Updated Web backend now with multi-user, misc data and endpoints improvements
This commit is contained in:
@ -29,9 +29,11 @@ class Room(Entity):
|
||||
Db.create_tables([EntitySettings, User, Room], safe=True)
|
||||
|
||||
class UserSettingsData():
|
||||
def __new__(cls, user_id:str) -> SafeNamespace|None:
|
||||
def __new__(cls, user_id:str=None) -> SafeNamespace:
|
||||
settings = None
|
||||
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:
|
||||
return None
|
||||
pass
|
||||
return SafeNamespace(**(settings or {}), _exists=bool(settings))
|
||||
|
||||
|
@ -16,7 +16,7 @@ import mastodon
|
||||
from bs4 import BeautifulSoup
|
||||
from magic import Magic
|
||||
|
||||
def MastodonMain() -> bool:
|
||||
def MastodonMain(path:str) -> bool:
|
||||
if not (MastodonUrl and MastodonToken):
|
||||
return False
|
||||
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"])),
|
||||
name = status["account"]["display_name"],
|
||||
)
|
||||
data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace())
|
||||
data.user.settings = UserSettingsData(data.user.id)
|
||||
return data
|
||||
|
||||
def MastodonHandler(event, Mastodon):
|
||||
|
@ -25,7 +25,7 @@ import queue
|
||||
MatrixClient = None
|
||||
MatrixQueue = queue.Queue()
|
||||
|
||||
def MatrixMain() -> bool:
|
||||
def MatrixMain(path:str) -> bool:
|
||||
if not (MatrixUrl and MatrixUsername and (MatrixPassword or MatrixToken)):
|
||||
return False
|
||||
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('/')
|
||||
data.media["url"] = ("https://" + server_name + nio.Api.download(server_name, media_id)[1])
|
||||
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
|
||||
|
||||
async def MatrixInviteHandler(room:nio.MatrixRoom, event:nio.InviteEvent) -> None:
|
||||
|
@ -20,7 +20,7 @@ from telegram.ext import CommandHandler, MessageHandler, Filters, CallbackContex
|
||||
|
||||
TelegramClient = None
|
||||
|
||||
def TelegramMain() -> bool:
|
||||
def TelegramMain(path:str) -> bool:
|
||||
if not TelegramToken:
|
||||
return False
|
||||
global TelegramClient
|
||||
@ -55,7 +55,7 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
|
||||
),
|
||||
)
|
||||
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)
|
||||
data.message_url = linked.message
|
||||
data.room.url = linked.room
|
||||
|
@ -8,78 +8,191 @@
|
||||
WebConfig = {
|
||||
"host": ("0.0.0.0", 30264),
|
||||
"url": "https://windog.octt.eu.org",
|
||||
"anti_drop_interval": 15,
|
||||
}
|
||||
|
||||
""" # end windog config # """
|
||||
|
||||
import queue
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from LibWinDog.Platforms.Web.multithread_http_server import MultiThreadHttpServer
|
||||
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||
from threading import Thread
|
||||
from uuid6 import uuid7
|
||||
|
||||
WebQueues = {}
|
||||
|
||||
default_css = """<style>
|
||||
* { box-sizing: border-box; }
|
||||
iframe { width: 100%; height: 3em; top: 0; position: sticky; }
|
||||
textarea { width: calc(100% - 3em); height: 2em; }
|
||||
input { width: 2em; }
|
||||
</style>"""
|
||||
web_css_style = web_js_script = None
|
||||
web_html_prefix = (lambda document_class='', head_extra='': (f'''<!DOCTYPE html><html class="{document_class}"><head>
|
||||
<meta charset="UTF-8"/>
|
||||
<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>
|
||||
{head_extra}</head><body>'''))
|
||||
|
||||
class WebServerClass(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == '/':
|
||||
uuid = str(time.time())
|
||||
WebQueues[uuid] = queue.Queue()
|
||||
def parse_path(self):
|
||||
path = self.path.strip('/').lower()
|
||||
try:
|
||||
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_header("Location", f"/{uuid}")
|
||||
self.send_header("Location", path)
|
||||
self.end_headers()
|
||||
return
|
||||
uuid = self.path.split('/')[-1]
|
||||
if self.path.startswith("/form/"):
|
||||
|
||||
def send_text_content(self, text:str, mime:str, infinite:bool=False):
|
||||
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.wfile.write(f'{default_css}<form method="POST" action="/form/{uuid}"><textarea name="text"></textarea><input type="submit" value="📤️"/></form>'.encode())
|
||||
return
|
||||
self.wfile.write(text.encode())
|
||||
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_header("Content-Type", "text/html; charset=UTF-8")
|
||||
self.send_header("Content-Encoding", "chunked")
|
||||
self.end_headers()
|
||||
self.wfile.write(f'{default_css}<h3><a href="/">WinDog</a></h3><iframe src="/form/{uuid}"></iframe>'.encode())
|
||||
while True: # this apparently makes us lose threads and the web bot becomes unusable, we should handle dropped connections
|
||||
target = f"/{room_id}/{user_id}?page-target=1#load-target"
|
||||
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:
|
||||
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:
|
||||
time.sleep(0.01)
|
||||
|
||||
def do_POST(self):
|
||||
uuid = self.path.split('/')[-1]
|
||||
text = urlparse.unquote_plus(self.rfile.read(int(self.headers["Content-Length"])).decode().split('=')[1])
|
||||
self.send_response(302)
|
||||
self.send_header("Location", f"/form/{uuid}")
|
||||
self.end_headers()
|
||||
data = WebMakeInputMessageData(text, uuid)
|
||||
OnInputMessageParsed(data)
|
||||
call_endpoint(EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data)
|
||||
def send_form_html(self, room_id:str, user_id:str):
|
||||
self.send_text_content((f'''{web_html_prefix("form no-margin")}
|
||||
<!--<link rel="stylesheet" href="/on-connection-dropped.css"/>-->
|
||||
<!--<p class="on-connection-dropped">Connection dropped! Please <a href="/" target="_parent">open a new chat</a>.</p>-->
|
||||
<form method="POST" action="/form/{room_id}/{user_id}" onsubmit="''' + '''(function(event){
|
||||
var textEl = event.target.querySelector('[name=\\'text\\']');
|
||||
textEl.value = textEl.value.trim();
|
||||
window.parent.inputFrameResize();
|
||||
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:
|
||||
return InputMessageData(
|
||||
def do_GET(self):
|
||||
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_html = html_escape(text),
|
||||
command = TextCommandData(text, "web"),
|
||||
room = SafeNamespace(
|
||||
id = f"web:{uuid}",
|
||||
id = f"web:{room_id}",
|
||||
),
|
||||
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:
|
||||
server = MultiThreadHttpServer(WebConfig["host"], 32, WebServerClass)
|
||||
server.start(background=True)
|
||||
def WebMakeMessageHtml(message:MessageData, for_user_id:str):
|
||||
if not (message.text_html or message.media):
|
||||
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:
|
||||
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)
|
||||
|
||||
|
@ -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()))
|
||||
|
1
LibWinDog/Platforms/Web/requirements.txt
Normal file
1
LibWinDog/Platforms/Web/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
uuid6
|
121
LibWinDog/Platforms/Web/windog.css
Normal file
121
LibWinDog/Platforms/Web/windog.css
Normal 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;
|
||||
}
|
30
LibWinDog/Platforms/Web/windog.js
Normal file
30
LibWinDog/Platforms/Web/windog.js
Normal 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);
|
||||
}
|
@ -6,12 +6,21 @@
|
||||
from types import 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):
|
||||
return self.__dict__.__iter__()
|
||||
def __getitem__(self, key):
|
||||
return self.__getattribute__(key)
|
||||
def __setitem__(self, 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):
|
||||
def __getattribute__(self, key):
|
||||
|
@ -18,11 +18,13 @@ UserSettingsLimits = {
|
||||
|
||||
def cConfig(context:EventContext, data:InputMessageData):
|
||||
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()
|
||||
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 len(value) > UserSettingsLimits[key]:
|
||||
return send_status(context, 500, language)
|
||||
@ -52,7 +54,7 @@ def cPing(context:EventContext, data:InputMessageData):
|
||||
|
||||
RegisterModule(name="Base", endpoints=[
|
||||
SafeNamespace(names=["source"], handler=cSource),
|
||||
SafeNamespace(names=["config"], handler=cConfig, body=False, arguments={
|
||||
SafeNamespace(names=["config", "settings"], handler=cConfig, body=False, arguments={
|
||||
"get": True,
|
||||
}),
|
||||
#SafeNamespace(names=["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr),
|
||||
|
@ -7,7 +7,7 @@ def cEcho(context:EventContext, data:InputMessageData):
|
||||
if not (text := data.command.body):
|
||||
return send_message(context, {
|
||||
"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
|
||||
nonascii = True
|
||||
for char in data.command.tokens[1]:
|
||||
|
@ -20,7 +20,8 @@ def cHelp(context:EventContext, data:InputMessageData) -> None:
|
||||
text += (f"\n\n{module}" + (f": {summary}" if summary else ''))
|
||||
for endpoint in endpoints:
|
||||
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()
|
||||
return send_message(context, {"text_html": text})
|
||||
|
||||
|
@ -10,6 +10,7 @@ MicrosoftBingSettings = {}
|
||||
""" # end windog config # """
|
||||
|
||||
from urlextract import URLExtract
|
||||
from urllib import parse as urlparse
|
||||
from urllib.request import urlopen, Request
|
||||
|
||||
def RandomHexString(length:int) -> str:
|
||||
|
@ -29,7 +29,7 @@ def cExec(context:EventContext, data:InputMessageData):
|
||||
|
||||
def cRestart(context:EventContext, data:InputMessageData):
|
||||
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()
|
||||
return send_message(context, {"text_plain": "Bot restart queued."})
|
||||
|
||||
|
@ -328,7 +328,7 @@ def app_main() -> None:
|
||||
#SetupDb()
|
||||
app_log(f"📨️ Initializing Platforms... ", newline=False)
|
||||
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("...Done. ✅️", inline=True, newline=True)
|
||||
app_log("🐶️ WinDog Ready!")
|
||||
|
Reference in New Issue
Block a user