mirror of
				https://gitlab.com/octospacc/WinDog.git
				synced 2025-06-05 22:09:20 +02:00 
			
		
		
		
	More work on bridging, first WIP Web backend
This commit is contained in:
		@@ -23,7 +23,7 @@ import nio
 | 
			
		||||
import queue
 | 
			
		||||
 | 
			
		||||
MatrixClient = None
 | 
			
		||||
MatrixQueue = []#queue.Queue()
 | 
			
		||||
MatrixQueue = queue.Queue()
 | 
			
		||||
 | 
			
		||||
def MatrixMain() -> bool:
 | 
			
		||||
	if not (MatrixUrl and MatrixUsername and (MatrixPassword or MatrixToken)):
 | 
			
		||||
@@ -33,11 +33,10 @@ def MatrixMain() -> bool:
 | 
			
		||||
		MatrixUsername = new
 | 
			
		||||
	async def queue_handler():
 | 
			
		||||
		asyncio.ensure_future(queue_handler())
 | 
			
		||||
		if not len(MatrixQueue):
 | 
			
		||||
			# avoid 100% CPU usage ☠️
 | 
			
		||||
			time.sleep(0.01)
 | 
			
		||||
		while len(MatrixQueue):
 | 
			
		||||
			MatrixSender(*MatrixQueue.pop(0))
 | 
			
		||||
		try:
 | 
			
		||||
			MatrixSender(*MatrixQueue.get(block=False))
 | 
			
		||||
		except queue.Empty:
 | 
			
		||||
			time.sleep(0.01) # avoid 100% CPU usage ☠️
 | 
			
		||||
	async def client_main() -> None:
 | 
			
		||||
		global MatrixClient
 | 
			
		||||
		MatrixClient = nio.AsyncClient(MatrixUrl, MatrixUsername)
 | 
			
		||||
@@ -69,6 +68,9 @@ def MatrixMakeInputMessageData(room:nio.MatrixRoom, event:nio.RoomMessage) -> In
 | 
			
		||||
			#name = , # TODO name must be get via a separate API request (and so maybe we should cache it)
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
	if (mxc_url := ObjGet(data, "media.url")) and mxc_url.startswith("mxc://"):
 | 
			
		||||
		_, _, server_name, media_id = mxc_url.split('/')
 | 
			
		||||
		data.media["url"] = ("https://" + server_name + nio.Api.download(server_name, media_id)[1])
 | 
			
		||||
	data.command = ParseCommand(data.text_plain)
 | 
			
		||||
	data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace())
 | 
			
		||||
	return data
 | 
			
		||||
@@ -85,7 +87,7 @@ def MatrixSender(context:EventContext, data:OutputMessageData):
 | 
			
		||||
	try:
 | 
			
		||||
		asyncio.get_event_loop()
 | 
			
		||||
	except RuntimeError:
 | 
			
		||||
		MatrixQueue.append((context, data))
 | 
			
		||||
		MatrixQueue.put((context, data))
 | 
			
		||||
		return None
 | 
			
		||||
	asyncio.create_task(context.manager.room_send(
 | 
			
		||||
		room_id=(data.room_id or ObjGet(context, "event.room.room_id")),
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,7 @@ def TelegramHandler(update:telegram.Update, context:CallbackContext=None) -> Non
 | 
			
		||||
 | 
			
		||||
def TelegramSender(context:EventContext, data:OutputMessageData):
 | 
			
		||||
	result = None
 | 
			
		||||
	# TODO clean this
 | 
			
		||||
	if data.room_id:
 | 
			
		||||
		result = context.manager.bot.send_message(data.room_id, text=data.text_plain)
 | 
			
		||||
	else:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
# ================================== #
 | 
			
		||||
# WinDog multi-purpose chatbot       #
 | 
			
		||||
# Licensed under AGPLv3 by OctoSpacc #
 | 
			
		||||
# ================================== #
 | 
			
		||||
 | 
			
		||||
def WebMain() -> None:
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
def WebSender() -> None:
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
#RegisterPlatform(name="Web", main=WebMain, sender=WebSender)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										86
									
								
								LibWinDog/Platforms/Web/Web.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										86
									
								
								LibWinDog/Platforms/Web/Web.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
# ================================== #
 | 
			
		||||
# WinDog multi-purpose chatbot       #
 | 
			
		||||
# Licensed under AGPLv3 by OctoSpacc #
 | 
			
		||||
# ================================== #
 | 
			
		||||
 | 
			
		||||
""" # windog config start # """
 | 
			
		||||
 | 
			
		||||
WebConfig = {
 | 
			
		||||
	"host": ("0.0.0.0", 30264),
 | 
			
		||||
	"url": "https://windog.octt.eu.org",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
""" # end windog config # """
 | 
			
		||||
 | 
			
		||||
import queue
 | 
			
		||||
from http.server import BaseHTTPRequestHandler
 | 
			
		||||
from LibWinDog.Platforms.Web.multithread_http_server import MultiThreadHttpServer
 | 
			
		||||
 | 
			
		||||
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>"""
 | 
			
		||||
 | 
			
		||||
class WebServerClass(BaseHTTPRequestHandler):
 | 
			
		||||
	def do_GET(self):
 | 
			
		||||
		if self.path == '/':
 | 
			
		||||
			uuid = str(time.time())
 | 
			
		||||
			WebQueues[uuid] = queue.Queue()
 | 
			
		||||
			self.send_response(302)
 | 
			
		||||
			self.send_header("Location", f"/{uuid}")
 | 
			
		||||
			self.end_headers()
 | 
			
		||||
			return
 | 
			
		||||
		uuid = self.path.split('/')[-1]
 | 
			
		||||
		if self.path.startswith("/form/"):
 | 
			
		||||
			self.send_response(200)
 | 
			
		||||
			self.send_header("Content-Type", "text/html; 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.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
 | 
			
		||||
			try:
 | 
			
		||||
				self.wfile.write(("<p>" + WebQueues[uuid].get(block=False).text_html + "</p>").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)
 | 
			
		||||
		OnMessageParsed(data)
 | 
			
		||||
		if (command := ObjGet(data, "command.name")):
 | 
			
		||||
			CallEndpoint(command, EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data)
 | 
			
		||||
 | 
			
		||||
def WebMakeInputMessageData(text:str, uuid:str) -> InputMessageData:
 | 
			
		||||
	return InputMessageData(
 | 
			
		||||
		text_plain = text,
 | 
			
		||||
		command = ParseCommand(text),
 | 
			
		||||
		room = SafeNamespace(
 | 
			
		||||
			id = f"web:{uuid}",
 | 
			
		||||
		),
 | 
			
		||||
		user = SafeNamespace(
 | 
			
		||||
			settings = SafeNamespace(),
 | 
			
		||||
		),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
def WebMain() -> None:
 | 
			
		||||
	server = MultiThreadHttpServer(WebConfig["host"], 32, WebServerClass)
 | 
			
		||||
	server.start(background=True)
 | 
			
		||||
 | 
			
		||||
def WebSender(context:EventContext, data:OutputMessageData) -> None:
 | 
			
		||||
	WebQueues[context.event.room_id].put(data)
 | 
			
		||||
 | 
			
		||||
RegisterPlatform(name="Web", main=WebMain, sender=WebSender)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										119
									
								
								LibWinDog/Platforms/Web/multithread_http_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								LibWinDog/Platforms/Web/multithread_http_server.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
#!/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()))
 | 
			
		||||
 | 
			
		||||
@@ -12,12 +12,17 @@ class SafeNamespace(SimpleNamespace):
 | 
			
		||||
		except AttributeError:
 | 
			
		||||
			return None
 | 
			
		||||
 | 
			
		||||
# we just use these for type hinting:
 | 
			
		||||
 | 
			
		||||
class EventContext(SafeNamespace):
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
class InputMessageData(SafeNamespace):
 | 
			
		||||
class MessageData(SafeNamespace):
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
class OutputMessageData(SafeNamespace):
 | 
			
		||||
class InputMessageData(MessageData):
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
class OutputMessageData(MessageData):
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
def cEcho(context:EventContext, data:InputMessageData) -> None:
 | 
			
		||||
	if not (text := ObjGet(data, "command.body")):
 | 
			
		||||
		return SendMessage(context, OutputMessageData(text_html=context.endpoint.get_string("empty", data.user.settings.language)))
 | 
			
		||||
		return SendMessage(context, {"text_html": context.endpoint.get_string("empty", data.user.settings.language)})
 | 
			
		||||
	prefix = f'<a href="{data.message_url}">🗣️</a> '
 | 
			
		||||
	#prefix = f"[🗣️]({context.linker(data).message}) "
 | 
			
		||||
	if len(data.command.tokens) == 2:
 | 
			
		||||
@@ -17,7 +17,7 @@ def cEcho(context:EventContext, data:InputMessageData) -> None:
 | 
			
		||||
		if nonascii:
 | 
			
		||||
			# text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis)
 | 
			
		||||
			prefix = ''
 | 
			
		||||
	SendMessage(context, OutputMessageData(text_html=(prefix + html_escape(text))))
 | 
			
		||||
	SendMessage(context, {"text_html": (prefix + html_escape(text))})
 | 
			
		||||
 | 
			
		||||
RegisterModule(name="Echo", endpoints=[
 | 
			
		||||
	SafeNamespace(names=["echo"], handler=cEcho),
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None:
 | 
			
		||||
	if len(data.command.tokens) >= 2:
 | 
			
		||||
		# Find links in command body
 | 
			
		||||
		text = (data.text_markdown + ' ' + data.text_plain)
 | 
			
		||||
	elif (quoted := data.quoted) and (quoted.text_auto or quoted.text_markdown or quoted.text_html):
 | 
			
		||||
	elif (quoted := data.quoted) and (quoted.text_plain or quoted.text_markdown or quoted.text_html):
 | 
			
		||||
		# Find links in quoted message
 | 
			
		||||
		text = ((quoted.text_markdown or '') + ' ' + (quoted.text_plain or '') + ' ' + (quoted.text_html or ''))
 | 
			
		||||
	else:
 | 
			
		||||
 
 | 
			
		||||
@@ -107,8 +107,8 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
 | 
			
		||||
			for img_elem in img_list:
 | 
			
		||||
				img_array.append({"url": img_elem.get_attribute("src")}) #, "bytes": HttpReq(img_url).read()})
 | 
			
		||||
			SendMessage(context, {
 | 
			
		||||
				"TextPlain": f'"{prompt}"',
 | 
			
		||||
				"TextMarkdown": (f'"_{CharEscape(prompt, "MARKDOWN")}_"'),
 | 
			
		||||
				"text_plain": f'"{prompt}"',
 | 
			
		||||
				"text_html": f'"<i>{html_escape(prompt)}</i>"',
 | 
			
		||||
				"media": img_array,
 | 
			
		||||
			})
 | 
			
		||||
			return closeSelenium(driver_index, driver)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								WinDog.py
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								WinDog.py
									
									
									
									
									
								
							@@ -14,7 +14,7 @@ from os.path import isfile, isdir
 | 
			
		||||
from random import choice, choice as randchoice, randint
 | 
			
		||||
from threading import Thread
 | 
			
		||||
from traceback import format_exc, format_exc as traceback_format_exc
 | 
			
		||||
from urllib import parse as urlparse, urllib_parse
 | 
			
		||||
from urllib import parse as urlparse, parse as urllib_parse
 | 
			
		||||
from yaml import load as yaml_load, BaseLoader as yaml_BaseLoader
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
from markdown import markdown
 | 
			
		||||
@@ -29,11 +29,14 @@ def ObjectUnion(*objects:object, clazz:object=None):
 | 
			
		||||
	dikt = {}
 | 
			
		||||
	auto_clazz = None
 | 
			
		||||
	for obj in objects:
 | 
			
		||||
		obj_clazz = obj.__class__
 | 
			
		||||
		if not obj:
 | 
			
		||||
			continue
 | 
			
		||||
		if type(obj) == dict:
 | 
			
		||||
			obj = (clazz or SafeNamespace)(**obj)
 | 
			
		||||
		for key, value in tuple(obj.__dict__.items()):
 | 
			
		||||
			dikt[key] = value
 | 
			
		||||
		auto_clazz = obj.__class__
 | 
			
		||||
		auto_clazz = obj_clazz
 | 
			
		||||
	return (clazz or auto_clazz)(**dikt)
 | 
			
		||||
 | 
			
		||||
def Log(text:str, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None:
 | 
			
		||||
@@ -185,7 +188,7 @@ def ParseCommand(text:str) -> SafeNamespace|None:
 | 
			
		||||
	command.body = text[len(command.tokens[0]):].strip()
 | 
			
		||||
	if command.name not in Endpoints:
 | 
			
		||||
		return command
 | 
			
		||||
	if (endpoint_arguments := Endpoints[command.name].arguments):#["arguments"]):
 | 
			
		||||
	if (endpoint_arguments := Endpoints[command.name].arguments):
 | 
			
		||||
		command.arguments = {}
 | 
			
		||||
		index = 1
 | 
			
		||||
		for key in endpoint_arguments:
 | 
			
		||||
@@ -201,17 +204,35 @@ def ParseCommand(text:str) -> SafeNamespace|None:
 | 
			
		||||
	return command
 | 
			
		||||
 | 
			
		||||
def OnMessageParsed(data:InputMessageData) -> None:
 | 
			
		||||
	DumpMessage(data)
 | 
			
		||||
	UpdateUserDb(data.user)
 | 
			
		||||
	for bridge in BridgesConfig:
 | 
			
		||||
		if data.room.id in bridge:
 | 
			
		||||
			rooms = list(bridge)
 | 
			
		||||
			rooms.remove(data.room.id)
 | 
			
		||||
			for room in rooms:
 | 
			
		||||
				tokens = room.split(':')
 | 
			
		||||
				SendMessage(SafeNamespace(platform=tokens[0]), ObjectUnion(data, {"room_id": ':'.join(tokens)}))
 | 
			
		||||
	dump_message(data, prefix='>')
 | 
			
		||||
	#handle_bridging(SendMessage, data, from_sent=False)
 | 
			
		||||
	update_user_db(data.user)
 | 
			
		||||
 | 
			
		||||
def UpdateUserDb(user:SafeNamespace) -> None:
 | 
			
		||||
def OnMessageSent(data:OutputMessageData) -> None:
 | 
			
		||||
	dump_message(data, prefix='<')
 | 
			
		||||
	#handle_bridging(SendMessage, data, from_sent=True) # TODO fix duplicate messages lol
 | 
			
		||||
 | 
			
		||||
# TODO: fix to send messages to different rooms, this overrides destination data but that gives problems with rebroadcasting the bot's own messages
 | 
			
		||||
def handle_bridging(method:callable, data:MessageData, from_sent:bool):
 | 
			
		||||
	if data.user:
 | 
			
		||||
		if (text_plain := ObjGet(data, "text_plain")):
 | 
			
		||||
			text_plain = f"<{data.user.name}>: {text_plain}"
 | 
			
		||||
		if (text_html := ObjGet(data, "text_html")):
 | 
			
		||||
			text_html = (urlparse.quote(f"<{data.user.name}>: ") + text_html)
 | 
			
		||||
	for bridge in BridgesConfig:
 | 
			
		||||
		if data.room.id not in bridge:
 | 
			
		||||
			continue
 | 
			
		||||
		rooms = list(bridge)
 | 
			
		||||
		rooms.remove(data.room.id)
 | 
			
		||||
		for room_id in rooms:
 | 
			
		||||
			method(
 | 
			
		||||
				SafeNamespace(platform=room_id.split(':')[0]),
 | 
			
		||||
				ObjectUnion(data, {"room_id": room_id}, ({"text_plain": text_plain, "text_markdown": None, "text_html": text_html} if data.user else None)),
 | 
			
		||||
				from_sent)
 | 
			
		||||
 | 
			
		||||
def update_user_db(user:SafeNamespace) -> None:
 | 
			
		||||
	if not (user and user.id):
 | 
			
		||||
		return
 | 
			
		||||
	try:
 | 
			
		||||
		User.get(User.id == user.id)
 | 
			
		||||
	except User.DoesNotExist:
 | 
			
		||||
@@ -222,17 +243,17 @@ def UpdateUserDb(user:SafeNamespace) -> None:
 | 
			
		||||
		except User.DoesNotExist:
 | 
			
		||||
			User.create(id=user.id, id_hash=user_hash)
 | 
			
		||||
 | 
			
		||||
def DumpMessage(data:InputMessageData) -> None:
 | 
			
		||||
def dump_message(data:InputMessageData, prefix:str='') -> None:
 | 
			
		||||
	if not (Debug and (DumpToFile or DumpToConsole)):
 | 
			
		||||
		return
 | 
			
		||||
	text = (data.text_plain.replace('\n', '\\n') if data.text_plain else '')
 | 
			
		||||
	text = f"[{int(time.time())}] [{time.ctime()}] [{data.room and data.room.id}] [{data.message_id}] [{data.user.id}] {text}"
 | 
			
		||||
	text = f"{prefix} [{int(time.time())}] [{time.ctime()}] [{data.room and data.room.id}] [{data.message_id}] [{data.user and data.user.id}] {text}"
 | 
			
		||||
	if DumpToConsole:
 | 
			
		||||
		print(text, data)
 | 
			
		||||
	if DumpToFile:
 | 
			
		||||
		open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n')
 | 
			
		||||
 | 
			
		||||
def SendMessage(context:EventContext, data:OutputMessageData) -> None:
 | 
			
		||||
def SendMessage(context:EventContext, data:OutputMessageData, from_sent:bool=False) -> None:
 | 
			
		||||
	data = (OutputMessageData(**data) if type(data) == dict else data)
 | 
			
		||||
 | 
			
		||||
	# TODO remove this after all modules are changed
 | 
			
		||||
@@ -240,14 +261,16 @@ def SendMessage(context:EventContext, data:OutputMessageData) -> None:
 | 
			
		||||
		data.text = data.Text
 | 
			
		||||
	if data.TextPlain and not data.text_plain:
 | 
			
		||||
		data.text_plain = data.TextPlain
 | 
			
		||||
	if data.TextMarkdown and not data.text_markdown:
 | 
			
		||||
		data.text_markdown = data.TextMarkdown
 | 
			
		||||
	if data.text and not data.text_plain:
 | 
			
		||||
		data.text_plain = data.text
 | 
			
		||||
 | 
			
		||||
	if data.text_plain or data.text_markdown or data.text_html:
 | 
			
		||||
		if data.text_html and not data.text_plain:
 | 
			
		||||
			data.text_plain = BeautifulSoup(data.text_html, "html.parser").get_text()
 | 
			
		||||
		elif data.text_markdown and not data.text_plain:
 | 
			
		||||
			data.text_plain = data.text_markdown
 | 
			
		||||
		elif data.text_plain and not data.text_html:
 | 
			
		||||
			data.text_html = html_escape(data.text_plain)
 | 
			
		||||
	elif data.text:
 | 
			
		||||
		# our old system attempts to always receive Markdown and retransform when needed
 | 
			
		||||
		data.text_plain = MdToTxt(data.text)
 | 
			
		||||
@@ -266,7 +289,10 @@ def SendMessage(context:EventContext, data:OutputMessageData) -> None:
 | 
			
		||||
	platform = Platforms[context.platform]
 | 
			
		||||
	if (not context.manager) and (manager := platform.manager_class):
 | 
			
		||||
		context.manager = (manager() if callable(manager) else manager)
 | 
			
		||||
	return platform.sender(context, data)
 | 
			
		||||
	result = platform.sender(context, data)
 | 
			
		||||
	if not from_sent:
 | 
			
		||||
		OnMessageSent(data)
 | 
			
		||||
	return result
 | 
			
		||||
 | 
			
		||||
def SendNotice(context:EventContext, data) -> None:
 | 
			
		||||
	pass
 | 
			
		||||
@@ -275,7 +301,7 @@ def DeleteMessage(context:EventContext, data) -> None:
 | 
			
		||||
	pass
 | 
			
		||||
 | 
			
		||||
def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, event_class=None, manager_class=None) -> None:
 | 
			
		||||
	Platforms[name.lower()] = SafeNamespace(main=main, sender=sender, linker=linker, event_class=event_class, manager_class=manager_class)
 | 
			
		||||
	Platforms[name.lower()] = SafeNamespace(name=name, main=main, sender=sender, linker=linker, event_class=event_class, manager_class=manager_class)
 | 
			
		||||
	Log(f"{name}, ", inline=True)
 | 
			
		||||
 | 
			
		||||
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None:
 | 
			
		||||
@@ -317,9 +343,9 @@ def Main() -> None:
 | 
			
		||||
	#SetupDb()
 | 
			
		||||
	SetupLocales()
 | 
			
		||||
	Log(f"📨️ Initializing Platforms... ", newline=False)
 | 
			
		||||
	for platform in Platforms:
 | 
			
		||||
		if Platforms[platform].main():
 | 
			
		||||
			Log(f"{platform}, ", inline=True)
 | 
			
		||||
	for platform in Platforms.values():
 | 
			
		||||
		if platform.main():
 | 
			
		||||
			Log(f"{platform.name}, ", inline=True)
 | 
			
		||||
	Log("...Done. ✅️", inline=True, newline=True)
 | 
			
		||||
	Log("🐶️ WinDog Ready!")
 | 
			
		||||
	while True:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user