diff --git a/.gitignore b/.gitignore index a7709a7..a4d4993 100755 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -/Config.py -/Database.sqlite -/Dump.txt -/Log.txt -/Selenium-WinDog/ +/Data/ /downloaded_files/ -/session.txt *.pyc +*.lock +/RunWinDog.py diff --git a/LibWinDog/Config.py b/LibWinDog/Config.py index c88c359..4a96d47 100755 --- a/LibWinDog/Config.py +++ b/LibWinDog/Config.py @@ -7,7 +7,7 @@ # If you have modified the bot's code, you should set this. ModifiedSourceUrl = "" -# Logging of system information and runtime errors. Recommended to be on at least for console. +# Logging of system information and runtime errors. Recommended to be on in some way to diagnose errors. LogToConsole = True LogToFile = True @@ -21,14 +21,8 @@ BridgesConfig = [] DefaultLanguage = "en" Debug = False -CmdPrefixes = ".!/" +CommandPrefixes = ".!/" WebUserAgent = "WinDog v.Staging" -#ModuleGroups = (ModuleGroups | { -ModuleGroups = { - "Basic": "", - "Geek": "", -} - # Only for the platforms you want to use, uncomment the below credentials and fill with your own: """ # end windog config # """ diff --git a/LibWinDog/Database.py b/LibWinDog/Database.py index a377fc4..5768079 100755 --- a/LibWinDog/Database.py +++ b/LibWinDog/Database.py @@ -1,12 +1,12 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # from peewee import * from LibWinDog.Types import * -Db = SqliteDatabase("Database.sqlite") +Db = SqliteDatabase("./Data/Database.sqlite") class BaseModel(Model): class Meta: @@ -28,3 +28,10 @@ class Room(Entity): Db.create_tables([EntitySettings, User, Room], safe=True) +class UserSettingsData(): + def __new__(cls, user_id:str) -> SafeNamespace|None: + try: + return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get()) + except EntitySettings.DoesNotExist: + return None + diff --git a/LibWinDog/Platforms/Mastodon/Mastodon.py b/LibWinDog/Platforms/Mastodon/Mastodon.py index 3818380..a5e3c62 100755 --- a/LibWinDog/Platforms/Mastodon/Mastodon.py +++ b/LibWinDog/Platforms/Mastodon/Mastodon.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # """ # windog config start # @@ -35,20 +35,19 @@ def MastodonMakeInputMessageData(status:dict) -> InputMessageData: command_tokens = data.text_plain.strip().replace("\t", " ").split(" ") while command_tokens[0].strip().startswith('@') or not command_tokens[0]: command_tokens.pop(0) - data.command = ParseCommand(" ".join(command_tokens), "mastodon") + data.command = TextCommandData(" ".join(command_tokens), "mastodon") data.user = UserData( id = ("mastodon:" + strip_url_scheme(status["account"]["uri"])), name = status["account"]["display_name"], ) - data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace()) + data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace()) return data def MastodonHandler(event, Mastodon): if event["type"] == "mention": data = MastodonMakeInputMessageData(event["status"]) OnInputMessageParsed(data) - if (command := ObjGet(data, "command.name")): - CallEndpoint(command, EventContext(platform="mastodon", event=event, manager=Mastodon), data) + call_endpoint(EventContext(platform="mastodon", event=event, manager=Mastodon), data) def MastodonSender(context:EventContext, data:OutputMessageData) -> None: media_results = None diff --git a/LibWinDog/Platforms/Matrix/Matrix.py b/LibWinDog/Platforms/Matrix/Matrix.py index fb5c40a..321b502 100755 --- a/LibWinDog/Platforms/Matrix/Matrix.py +++ b/LibWinDog/Platforms/Matrix/Matrix.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # """ # windog config start # @@ -41,9 +41,9 @@ def MatrixMain() -> bool: global MatrixClient MatrixClient = nio.AsyncClient(MatrixUrl, MatrixUsername) login = await MatrixClient.login(password=MatrixPassword, token=MatrixToken) - if MatrixPassword and (not MatrixToken) and (token := ObjGet(login, "access_token")): + if MatrixPassword and (not MatrixToken) and (token := obj_get(login, "access_token")): open("./Config.py", 'a').write(f'\n# Added automatically #\nMatrixToken = "{token}"\n') - if (bot_id := ObjGet(login, "user_id")): + if (bot_id := obj_get(login, "user_id")): upgrade_username(bot_id) # ensure username is fully qualified for the API await MatrixClient.sync(30000) # resync old messages first to "skip read ones" asyncio.ensure_future(queue_handler()) @@ -58,8 +58,8 @@ def MatrixMakeInputMessageData(room:nio.MatrixRoom, event:nio.RoomMessage) -> In message_id = f"matrix:{event.event_id}", datetime = event.server_timestamp, text_plain = event.body, - text_html = ObjGet(event, "formatted_body"), # this could be unavailable - media = ({"url": event.url} if ObjGet(event, "url") else None), + text_html = obj_get(event, "formatted_body"), # this could be unavailable + media = ({"url": event.url} if obj_get(event, "url") else None), room = SafeNamespace( id = f"matrix:{room.room_id}", name = room.display_name, @@ -69,11 +69,11 @@ 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://"): + if (mxc_url := obj_get(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, "matrix") - data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace()) + data.command = TextCommandData(data.text_plain, "matrix") + data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace()) return data async def MatrixInviteHandler(room:nio.MatrixRoom, event:nio.InviteEvent) -> None: @@ -84,8 +84,7 @@ async def MatrixMessageHandler(room:nio.MatrixRoom, event:nio.RoomMessage) -> No return # ignore messages that come from the bot itself data = MatrixMakeInputMessageData(room, event) OnInputMessageParsed(data) - if (command := ObjGet(data, "command.name")): - CallEndpoint(command, EventContext(platform="matrix", event=SafeNamespace(room=room, event=event), manager=MatrixClient), data) + call_endpoint(EventContext(platform="matrix", event=SafeNamespace(room=room, event=event), manager=MatrixClient), data) def MatrixSender(context:EventContext, data:OutputMessageData): try: @@ -94,7 +93,7 @@ def MatrixSender(context:EventContext, data:OutputMessageData): MatrixQueue.put((context, data)) return None asyncio.create_task(context.manager.room_send( - room_id=((data.room and data.room.id) or ObjGet(context, "event.room.room_id")), + room_id=((data.room and data.room.id) or obj_get(context, "event.room.room_id")), message_type="m.room.message", content={"msgtype": "m.text", "body": data.text_plain})) diff --git a/LibWinDog/Platforms/Telegram/Telegram.py b/LibWinDog/Platforms/Telegram/Telegram.py index 26af7e3..e9375ab 100755 --- a/LibWinDog/Platforms/Telegram/Telegram.py +++ b/LibWinDog/Platforms/Telegram/Telegram.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # """ # windog config start # @@ -54,8 +54,8 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData: name = (message.chat.title or message.chat.first_name), ), ) - data.command = ParseCommand(data.text_plain, "telegram") - data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace()) + data.command = TextCommandData(data.text_plain, "telegram") + data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace()) linked = TelegramLinker(data) data.message_url = linked.message data.room.url = linked.room @@ -69,8 +69,7 @@ def TelegramHandler(update:telegram.Update, context:CallbackContext=None) -> Non if (quoted := update.message.reply_to_message): data.quoted = TelegramMakeInputMessageData(quoted) OnInputMessageParsed(data) - if (command := ObjGet(data, "command.name")): - CallEndpoint(command, EventContext(platform="telegram", event=update, manager=context), data) + call_endpoint(EventContext(platform="telegram", event=update, manager=context), data) Thread(target=handler).start() def TelegramSender(context:EventContext, data:OutputMessageData): @@ -83,7 +82,7 @@ def TelegramSender(context:EventContext, data:OutputMessageData): if data.media: for medium in data.media: result = context.event.message.reply_photo( - (ObjGet(medium, "bytes") or ObjGet(medium, "url")), + (obj_get(medium, "bytes") or obj_get(medium, "url")), caption=(data.text_html or data.text_markdown or data.text_plain), parse_mode=("HTML" if data.text_html else "MarkdownV2" if data.text_markdown else None), reply_to_message_id=replyToId) diff --git a/LibWinDog/Platforms/Web/Web.py b/LibWinDog/Platforms/Web/Web.py index 8911dc7..6ce2c38 100755 --- a/LibWinDog/Platforms/Web/Web.py +++ b/LibWinDog/Platforms/Web/Web.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # """ # windog config start # """ @@ -60,13 +60,12 @@ class WebServerClass(BaseHTTPRequestHandler): self.end_headers() data = WebMakeInputMessageData(text, uuid) OnInputMessageParsed(data) - if (command := ObjGet(data, "command.name")): - CallEndpoint(command, EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data) + call_endpoint(EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data) def WebMakeInputMessageData(text:str, uuid:str) -> InputMessageData: return InputMessageData( text_plain = text, - command = ParseCommand(text, "web"), + command = TextCommandData(text, "web"), room = SafeNamespace( id = f"web:{uuid}", ), diff --git a/LibWinDog/Types.py b/LibWinDog/Types.py index 83e84ce..1cce058 100755 --- a/LibWinDog/Types.py +++ b/LibWinDog/Types.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # from types import SimpleNamespace @@ -22,6 +22,9 @@ class SafeNamespace(DictNamespace): # we just use these for type hinting and clearer code: +class CommandData(SafeNamespace): + pass + class EventContext(SafeNamespace): pass diff --git a/ModWinDog/Base/Base.py b/ModWinDog/Base/Base.py index 1e0e833..673ffa8 100755 --- a/ModWinDog/Base/Base.py +++ b/ModWinDog/Base/Base.py @@ -1,37 +1,46 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # -def cSource(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"text_plain": ("""\ +def cSource(context:EventContext, data:InputMessageData): + send_message(context, {"text_plain": ("""\ * Original Code: {https://gitlab.com/octospacc/WinDog} * Mirror: {https://github.com/octospacc/WinDog} """ + (f"* Modified Code: {{{ModifiedSourceUrl}}}" if ModifiedSourceUrl else ""))}) -def cGdpr(context:EventContext, data:InputMessageData) -> None: +def cGdpr(context:EventContext, data:InputMessageData): pass -def cConfig(context:EventContext, data:InputMessageData) -> None: - if not (settings := GetUserSettings(data.user.id)): +UserSettingsLimits = { + "language": 13, +} + +def cConfig(context:EventContext, data:InputMessageData): + language = data.user.settings.language + if not (settings := UserSettingsData(data.user.id)): User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute() - if (to_set := ObjGet(data, "command.arguments.set")): - pass # TODO set in db, but first we need to ensure data is handled safely - if (to_get := ObjGet(data, "command.arguments.get")): - # TODO show a hint on possible options? - return SendMessage(context, OutputMessageData(text_plain=str(ObjGet(data.user.settings, to_get)))) + 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) + EntitySettings.update(**{key: value}).where(EntitySettings.entity == data.user.id).execute() + settings = UserSettingsData(data.user.id) + if (key): + # TODO show a hint on possible options? and add proper text hints for results + return send_message(context, {"text_plain": str(obj_get(settings, key))}) # TODO show general help when no useful parameters are passed - #Cmd = TelegramHandleCmd(update) - #if not Cmd: return # ... area: eu, us, ... # ... language: en, it, ... # ... userdata: import, export, delete -def cPing(context:EventContext, data:InputMessageData) -> None: +def cPing(context:EventContext, data:InputMessageData): # nice experiment, but it won't work with Telegram since time is not to milliseconds (?) #time_diff = (time_now := int(time.time())) - (time_sent := data.datetime) - #SendMessage(context, OutputMessageData(text_html=f"Pong!\n\n{time_sent} β {time_now} = {time_diff}")) - SendMessage(context, OutputMessageData(text_html="Pong!")) + #send_message(context, OutputMessageData(text_html=f"Pong!\n\n{time_sent} β {time_now} = {time_diff}")) + send_message(context, OutputMessageData(text_html="Pong!")) #def cTime(update:Update, context:CallbackContext) -> None: # update.message.reply_markdown_v2( @@ -39,7 +48,7 @@ def cPing(context:EventContext, data:InputMessageData) -> None: # reply_to_message_id=update.message.message_id) #def cEval(context:EventContext, data:InputMessageData) -> None: -# SendMessage(context, {"Text": choice(Locale.__('eval'))}) +# send_message(context, {"Text": choice(Locale.__('eval'))}) RegisterModule(name="Base", endpoints=[ SafeNamespace(names=["source"], handler=cSource), diff --git a/ModWinDog/Broadcast/Broadcast.py b/ModWinDog/Broadcast/Broadcast.py index 9e725d9..0038a56 100755 --- a/ModWinDog/Broadcast/Broadcast.py +++ b/ModWinDog/Broadcast/Broadcast.py @@ -1,17 +1,19 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # -def cBroadcast(context:EventContext, data:InputMessageData) -> None: +def cBroadcast(context:EventContext, data:InputMessageData): + language = data.user.settings.language if (data.user.id not in AdminIds) and (data.user.tag not in AdminIds): - return SendMessage(context, {"Text": "Permission denied."}) - destination = data.command.arguments["destination"] - text = data.command.body + return send_status(context, 403, language) + destination = data.command.arguments.destination + text = (data.command.body or (data.quoted and data.quoted.text_plain)) if not (destination and text): - return SendMessage(context, OutputMessageData(text_plain="Bad usage.")) - SendMessage(context, {"text_plain": text, "room": SafeNamespace(id=destination)}) - SendMessage(context, {"text_plain": "Executed."}) + return send_status_400(context, language) + result = send_message(context, {"text_plain": text, "room": SafeNamespace(id=destination)}) + send_message(context, {"text_plain": "Executed."}) + return result RegisterModule(name="Broadcast", endpoints=[ SafeNamespace(names=["broadcast"], handler=cBroadcast, body=True, arguments={ diff --git a/ModWinDog/Codings/Codings.py b/ModWinDog/Codings/Codings.py index 67b2802..a7ef27b 100755 --- a/ModWinDog/Codings/Codings.py +++ b/ModWinDog/Codings/Codings.py @@ -1,28 +1,49 @@ +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # + import base64 -from binascii import Error as binascii_Error +import base256 + +CodingsAlgorithms = [] +CodingsMethods = { + "encode:ascii85": base64.a85encode, + "decode:ascii85": base64.a85decode, + "encode:base16": base64.b16encode, + "decode:base16": base64.b16decode, + "encode:base32": base64.b32encode, + "decode:base32": base64.b32decode, + "encode:base32hex": base64.b32hexencode, + "decode:base32hex": base64.b32hexdecode, + "encode:base64": base64.b64encode, + "decode:base64": base64.b64decode, + "encode:base85": base64.b85encode, + "decode:base85": base64.b85decode, + "encode:base256": (lambda decoded: base256.encode_string(decoded.decode()).encode()), + "decode:base256": (lambda encoded: base256.decode_string(encoded.decode()).encode()), +} +for method in dict(CodingsMethods): + method2 = method.replace("ascii", 'a').replace("base", 'b') + CodingsMethods[method2] = CodingsMethods[method] + if (name := method.split(':')[1]) not in CodingsAlgorithms: + CodingsAlgorithms.append(name) def mCodings(context:EventContext, data:InputMessageData): - algorithms = ["base64"] - methods = { - "encode_base64": base64.b64encode, - "decode_base64": base64.b64decode, - "encode_b64": base64.b64encode, - "decode_b64": base64.b64decode, - } - if (method := ObjGet(methods, f"{data.command.name}_{data.command.arguments.algorithm}")): - try: - result = method((data.command.body or (data.quoted and data.quoted.text_plain)).encode()).decode() - SendMessage(context, {"text_html": f"
{html_escape(result)}"}) - except binascii_Error: - SendMessage(context, {"text_plain": f"An error occurred."}) - else: - language = data.user.settings.language - SendMessage(context, { - "text_html": f'{context.endpoint.help_text(language)}\n\n{context.module.get_string("algorithms", language)}: {algorithms}'}) + language = data.user.settings.language + method = obj_get(CodingsMethods, f"{data.command.name}:{data.command.arguments.algorithm}") + text = (data.command.body or (data.quoted and data.quoted.text_plain)) + if not (method and text): + return send_status_400(context, language) + try: + return send_message(context, { + "text_html": f"
{html_escape(method(text.encode()).decode())}"}) + except Exception: + return send_status_error(context, language) RegisterModule(name="Codings", group="Geek", endpoints=[ SafeNamespace(names=["encode", "decode"], handler=mCodings, body=False, quoted=False, arguments={ "algorithm": True, - }), + }, help_extra=(lambda endpoint, lang: f'{endpoint.module.get_string("algorithms", lang)}:
{"
, ".join(CodingsAlgorithms)}
.')),
])
diff --git a/ModWinDog/Codings/Codings.yaml b/ModWinDog/Codings/Codings.yaml
old mode 100644
new mode 100755
diff --git a/ModWinDog/Codings/requirements.txt b/ModWinDog/Codings/requirements.txt
new file mode 100755
index 0000000..d1809d4
--- /dev/null
+++ b/ModWinDog/Codings/requirements.txt
@@ -0,0 +1 @@
+base256
diff --git a/ModWinDog/Dumper/Dumper.py b/ModWinDog/Dumper/Dumper.py
index 99793cf..f7905e9 100755
--- a/ModWinDog/Dumper/Dumper.py
+++ b/ModWinDog/Dumper/Dumper.py
@@ -1,16 +1,16 @@
-# ================================== #
-# WinDog multi-purpose chatbot #
-# Licensed under AGPLv3 by OctoSpacc #
-# ================================== #
+# ==================================== #
+# WinDog multi-purpose chatbot #
+# Licensed under AGPLv3 by OctoSpacc #
+# ==================================== #
from json import dumps as json_dumps
+# TODO work with links to messages
def cDump(context:EventContext, data:InputMessageData):
- if (message := data.quoted):
- dump_text = json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")
- SendMessage(context, {
- "text_html": (f'{html_escape(dump_text)}' if message - else context.endpoint.help_text(data.user.settings.language))}) + if not (message := data.quoted): + return send_status_400(context, data.user.settings.language) + text = json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ") + return send_message(context, {"text_html": f'
{html_escape(text)}'}) RegisterModule(name="Dumper", group="Geek", endpoints=[ SafeNamespace(names=["dump"], handler=cDump, quoted=True), diff --git a/ModWinDog/Echo/Echo.py b/ModWinDog/Echo/Echo.py index b4c1999..e38a643 100755 --- a/ModWinDog/Echo/Echo.py +++ b/ModWinDog/Echo/Echo.py @@ -3,9 +3,9 @@ # Licensed under AGPLv3 by OctoSpacc # # ==================================== # -def cEcho(context:EventContext, data:InputMessageData) -> None: - if not (text := ObjGet(data, "command.body")): - return SendMessage(context, { +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'π£οΈ ' if len(data.command.tokens) == 2: # text is a single word @@ -18,7 +18,7 @@ def cEcho(context:EventContext, data:InputMessageData) -> None: # word is not ascii, probably an emoji (altough not necessarily) # so just pass it as is (useful for Telegram emojis) prefix = '' - SendMessage(context, {"text_html": (prefix + html_escape(text))}) + return send_message(context, {"text_html": (prefix + html_escape(text))}) RegisterModule(name="Echo", endpoints=[ SafeNamespace(names=["echo"], handler=cEcho, body=True), diff --git a/ModWinDog/GPT/GPT.py b/ModWinDog/GPT/GPT.py index d113017..bc284c2 100755 --- a/ModWinDog/GPT/GPT.py +++ b/ModWinDog/GPT/GPT.py @@ -1,21 +1,21 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # from g4f.client import Client as G4FClient g4fClient = G4FClient() -def cGpt(context:EventContext, data:InputMessageData) -> None: +def cGpt(context:EventContext, data:InputMessageData): if not (prompt := data.command.body): - return SendMessage(context, {"text_plain": "You must type some text."}) + return send_status_400(context, data.user.settings.language) output = None while not output or output.startswith("sorry, ζ¨ηipε·²η±δΊθ§¦ει²ζ»₯η¨ζ£ζ΅θθ’«ε°η¦,ζ¬ζε‘η½εζ―"): # quick fix for a strange ratelimit message output = "" for completion in g4fClient.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], stream=True): output += (completion.choices[0].delta.content or "") - return SendMessage(context, {"text_plain": f"[π€οΈ GPT]\n\n{output}"}) + return send_message(context, {"text_plain": f"[π€οΈ GPT]\n\n{output}"}) RegisterModule(name="GPT", endpoints=[ SafeNamespace(names=["gpt", "chatgpt"], handler=cGpt, body=True), diff --git a/ModWinDog/GPT/GPT.yaml b/ModWinDog/GPT/GPT.yaml index 2099301..c310dae 100755 --- a/ModWinDog/GPT/GPT.yaml +++ b/ModWinDog/GPT/GPT.yaml @@ -3,4 +3,6 @@ endpoints: summary: en: > Sends a message to GPT to get back a response. Note: conversations are not yet supported, and this is more standard GPT than ChatGPT, and in general there are many bugs! + body: + en: Prompt diff --git a/ModWinDog/Hashing/Hashing.py b/ModWinDog/Hashing/Hashing.py index 251f6a5..7a3e512 100755 --- a/ModWinDog/Hashing/Hashing.py +++ b/ModWinDog/Hashing/Hashing.py @@ -1,23 +1,21 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # import hashlib def cHash(context:EventContext, data:InputMessageData): text_input = (data.command.body or (data.quoted and data.quoted.text_plain)) algorithm = data.command.arguments.algorithm - language = data.user.settings.language if not (text_input and (algorithm in hashlib.algorithms_available)): - return SendMessage(context, { - "text_html": f'{context.endpoint.help_text(language)}\n\n{context.endpoint.get_string("algorithms", language)}: {hashlib.algorithms_available}'}) - hashed = hashlib.new(algorithm, text_input.encode()).hexdigest() - return SendMessage(context, {"text_html": f"
{hashed}"}) + return send_status_400(context, data.user.settings.language) + return send_message(context, { + "text_html": f"
{html_escape(hashlib.new(algorithm, text_input.encode()).hexdigest())}"}) RegisterModule(name="Hashing", group="Geek", endpoints=[ SafeNamespace(names=["hash"], handler=cHash, body=False, quoted=False, arguments={ "algorithm": True, - }), + }, help_extra=(lambda endpoint, lang: f'{endpoint.get_string("algorithms", lang)}:
{"
, ".join(hashlib.algorithms_available)}
.')),
])
diff --git a/ModWinDog/Help/Help.py b/ModWinDog/Help/Help.py
index cf6b88d..f758efd 100755
--- a/ModWinDog/Help/Help.py
+++ b/ModWinDog/Help/Help.py
@@ -1,23 +1,32 @@
-# ================================== #
-# WinDog multi-purpose chatbot #
-# Licensed under AGPLv3 by OctoSpacc #
-# ================================== #
+# ==================================== #
+# WinDog multi-purpose chatbot #
+# Licensed under AGPLv3 by OctoSpacc #
+# ==================================== #
-# TODO: implement /help {img_id}
]\n{img_url}", media={"url": img_url})) else: - pass - except Exception as error: - raise + return send_status_400(context, language) + except Exception: + return send_status_error(context, language) RegisterModule(name="Internet", endpoints=[ SafeNamespace(names=["embedded"], handler=cEmbedded, body=False, quoted=False), diff --git a/ModWinDog/Multifun/Multifun.py b/ModWinDog/Multifun/Multifun.py index d5e9415..c2c255f 100755 --- a/ModWinDog/Multifun/Multifun.py +++ b/ModWinDog/Multifun/Multifun.py @@ -1,9 +1,9 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # -def mMultifun(context:EventContext, data:InputMessageData) -> None: +def mMultifun(context:EventContext, data:InputMessageData): reply_to = None fun_strings = {} for key in ("empty", "bot", "self", "others"): @@ -19,7 +19,7 @@ def mMultifun(context:EventContext, data:InputMessageData) -> None: else: if fun_strings["empty"]: text = choice(fun_strings["empty"]) - SendMessage(context, {"text_html": text, "ReplyTo": reply_to}) + return send_message(context, {"text_html": text, "ReplyTo": reply_to}) RegisterModule(name="Multifun", endpoints=[ SafeNamespace(names=["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], handler=mMultifun), diff --git a/ModWinDog/Percenter/Percenter.py b/ModWinDog/Percenter/Percenter.py index 25f0db8..1859632 100755 --- a/ModWinDog/Percenter/Percenter.py +++ b/ModWinDog/Percenter/Percenter.py @@ -3,12 +3,17 @@ # Licensed under AGPLv3 by OctoSpacc # # ==================================== # -def mPercenter(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"text_html": (context.endpoint.get_string( +# NOTE: with this implementation there is a 1/100 probability (high!) of result 100.00, which is not always ideal +def RandomPercentString() -> str: + num = randint(0,100) + return (f'{num}.00' if num == 100 else f'{num}.{randint(0,9)}{randint(0,9)}') + +def mPercenter(context:EventContext, data:InputMessageData): + return send_message(context, {"text_html": (context.endpoint.get_string( ("done" if data.command.body else "empty"), data.user.settings.language - ) or context.endpoint.help_text(data.user.settings.language) - ).format(RandPercent(), data.command.body)}) + ) or context.endpoint.get_help_text(data.user.settings.language) + ).format(RandomPercentString(), data.command.body)}) RegisterModule(name="Percenter", endpoints=[ SafeNamespace(names=["wish", "level"], handler=mPercenter, body=True), diff --git a/ModWinDog/Scrapers/Scrapers.py b/ModWinDog/Scrapers/Scrapers.py index 461b935..9879090 100755 --- a/ModWinDog/Scrapers/Scrapers.py +++ b/ModWinDog/Scrapers/Scrapers.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # """ # windog config start # """ @@ -22,7 +22,7 @@ def getSelenium() -> tuple[int, Driver]|bool: if index not in currentSeleniumDrivers: currentSeleniumDrivers.append(index) break - return (index, Driver(uc=True, headless2=True, user_data_dir=f"./Selenium-WinDog/{index}")) + return (index, Driver(uc=True, headless2=True, user_data_dir=f"./Data/Selenium/{index}")) def closeSelenium(index:int, driver:Driver) -> None: if driver: @@ -30,19 +30,19 @@ def closeSelenium(index:int, driver:Driver) -> None: driver.close() driver.quit() except: - Log(format_exc()) + app_log(format_exc()) if index: currentSeleniumDrivers.remove(index) -def cDalleSelenium(context:EventContext, data:InputMessageData) -> None: +def cDalleSelenium(context:EventContext, data:InputMessageData): warning_text = "has been blocked by Microsoft because it violates their content policy. Further attempts might lead to a ban on your profile. Please review the Code of Conduct for Image Creator in this picture or at https://www.bing.com/new/termsofuseimagecreator#content-policy." if not (prompt := data.command.body): - return SendMessage(context, {"Text": "Please tell me what to generate."}) + return send_message(context, {"text_plain": "Please tell me what to generate."}) driver_index, driver = None, None try: driver = getSelenium() if not driver: - return SendMessage(context, {"Text": "Couldn't access a web scraping VM as they are all busy. Please try again later."}) + return send_message(context, {"text_plain": "Couldn't access a web scraping VM as they are all busy. Please try again later."}) driver_index, driver = driver driver.get("https://www.bing.com/images/create/") driver.refresh() @@ -50,11 +50,11 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None: driver.find_element('form a[role="button"]').submit() try: driver.find_element('img.gil_err_img[alt="Content warning"]') - SendMessage(context, {"Text": f"Content warning: This prompt {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) + send_message(context, {"text_plain": f"Content warning: This prompt {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) return closeSelenium(driver_index, driver) except Exception: # warning element was not found, we should be good pass - SendMessage(context, {"Text": "Request sent successfully, please wait..."}) + send_message(context, {"text_plain": "Request sent successfully, please wait..."}) retry_index = 3 while retry_index < 12: # note that sometimes generation can still fail and we will never get any image! @@ -64,8 +64,9 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None: if not len(img_list): try: driver.find_element('img.gil_err_img[alt="Unsafe image content detected"]') - SendMessage(context, {"Text": f"Unsafe image content detected: This result {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) - return closeSelenium(driver_index, driver) + result = send_message(context, {"text_plain": f"Unsafe image content detected: This result {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) + closeSelenium(driver_index, driver) + return result except: # no error is present, so we just have to wait more for the images continue img_array = [] @@ -73,30 +74,32 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None: img_url = img_url.get_attribute("src").split('?')[0] img_array.append({"url": img_url}) #, "bytes": HttpReq(img_url).read()}) page_url = driver.current_url.split('?')[0] - SendMessage(context, OutputMessageData( + result = send_message(context, OutputMessageData( text_plain=f'"{prompt}"\n{{{page_url}}}', text_html=f'"{html_escape(prompt)}"\n
{page_url}', media=img_array)) - return closeSelenium(driver_index, driver) + closeSelenium(driver_index, driver) + return result raise Exception("VM timed out.") except Exception as error: - Log(format_exc()) - SendMessage(context, {"TextPlain": "An unexpected error occurred."}) + app_log(format_exc()) + result = send_message(context, {"text_plain": "An unexpected error occurred."}) closeSelenium(driver_index, driver) + return result -def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None: +def cCraiyonSelenium(context:EventContext, data:InputMessageData): if not (prompt := data.command.body): - return SendMessage(context, {"Text": "Please tell me what to generate."}) + return send_message(context, {"text_plain": "Please tell me what to generate."}) driver_index, driver = None, None try: driver = getSelenium() if not driver: - return SendMessage(context, {"Text": "Couldn't access a web scraping VM as they are all busy. Please try again later."}) + return send_message(context, {"text_plain": "Couldn't access a web scraping VM as they are all busy. Please try again later."}) driver_index, driver = driver driver.get("https://www.craiyon.com/") driver.find_element('textarea#prompt').send_keys(prompt) driver.execute_script("arguments[0].click();", driver.find_element('button#generateButton')) - SendMessage(context, {"Text": "Request sent successfully, please wait up to 60 seconds..."}) + send_message(context, {"text_plain": "Request sent successfully, please wait up to 60 seconds..."}) retry_index = 3 while retry_index < 16: time.sleep(retry_index := retry_index + 1) @@ -106,17 +109,19 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None: img_array = [] for img_elem in img_list: img_array.append({"url": img_elem.get_attribute("src")}) #, "bytes": HttpReq(img_url).read()}) - SendMessage(context, { + result = send_message(context, { "text_plain": f'"{prompt}"', "text_html": f'"{html_escape(prompt)}"', "media": img_array, }) - return closeSelenium(driver_index, driver) + closeSelenium(driver_index, driver) + return result raise Exception("VM timed out.") except Exception as error: - Log(format_exc()) - SendMessage(context, {"TextPlain": "An unexpected error occurred."}) + app_log(format_exc()) + result = send_message(context, {"text_plain": "An unexpected error occurred."}) closeSelenium(driver_index, driver) + return result RegisterModule(name="Scrapers", endpoints=[ SafeNamespace(names=["dalle"], handler=cDalleSelenium, body=True), diff --git a/ModWinDog/Scripting/Scripting.py b/ModWinDog/Scripting/Scripting.py index f7ac483..99fd80c 100755 --- a/ModWinDog/Scripting/Scripting.py +++ b/ModWinDog/Scripting/Scripting.py @@ -1,7 +1,7 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # """ # windog config start # """ @@ -23,11 +23,10 @@ def luaAttributeFilter(obj, attr_name, is_setting): raise AttributeError("Access Denied.") # TODO make print behave the same as normal Lua, and expose a function for printing without newlines -def cLua(context:EventContext, data:InputMessageData) -> None: +def cLua(context:EventContext, data:InputMessageData): # TODO update quoted api getting - scriptText = (data.command.body or (data.quoted and data.quoted.text_plain)) - if not scriptText: - return SendMessage(context, {"text_plain": "You must provide some Lua code to execute."}) + if not (script_text := (data.command.body or (data.quoted and data.quoted.text_plain))): + return send_message(context, {"text_plain": "You must provide some Lua code to execute."}) luaRuntime = NewLuaRuntime(max_memory=LuaMemoryLimit, register_eval=False, register_builtins=False, attribute_filter=luaAttributeFilter) luaRuntime.eval(f"""(function() _windog = {{ stdout = "" }} @@ -44,13 +43,12 @@ end)()""") elif key not in LuaGlobalsWhitelist: del luaRuntime.globals()[key] try: - textOutput = ("[Κα΄α΄ κ±α΄α΄ α΄α΄α΄]\n\n" + luaRuntime.eval(f"""(function() -_windog.scriptout = (function()\n{scriptText}\nend)() + return send_message(context, {"text_plain": ("[Κα΄α΄ κ±α΄α΄ α΄α΄α΄]\n\n" + luaRuntime.eval(f"""(function() +_windog.scriptout = (function()\n{script_text}\nend)() return _windog.stdout .. (_windog.scriptout or '') -end)()""")) - except (LuaError, LuaSyntaxError) as error: - Log(textOutput := ("Lua Error: " + str(error))) - SendMessage(context, {"TextPlain": textOutput}) +end)()"""))}) + except (LuaError, LuaSyntaxError): + return send_status_error(context, data.user.settings.language) RegisterModule(name="Scripting", group="Geek", endpoints=[ SafeNamespace(names=["lua"], handler=cLua, body=False, quoted=False), diff --git a/ModWinDog/Start/Start.py b/ModWinDog/Start/Start.py index 6489eb7..12df3f4 100755 --- a/ModWinDog/Start/Start.py +++ b/ModWinDog/Start/Start.py @@ -3,8 +3,8 @@ # Licensed under AGPLv3 by OctoSpacc # # ==================================== # -def cStart(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, OutputMessageData( +def cStart(context:EventContext, data:InputMessageData): + return send_message(context, OutputMessageData( text_html=context.endpoint.get_string( "start", data.user.settings.language).format(data.user.name))) diff --git a/ModWinDog/System/System.py b/ModWinDog/System/System.py index a97b988..53348be 100755 --- a/ModWinDog/System/System.py +++ b/ModWinDog/System/System.py @@ -13,19 +13,28 @@ ExecAllowed = {"date": False, "fortune": False, "neofetch": True, "uptime": Fals import subprocess from re import compile as re_compile -def cExec(context:EventContext, data:InputMessageData) -> None: - if not (len(data.command.tokens) >= 2 and data.command.tokens[1].lower() in ExecAllowed): - return SendMessage(context, {"text_plain": "This feature is not implemented [Security Issue]."}) +def cExec(context:EventContext, data:InputMessageData): + language = data.user.settings.language + if not (len(data.command.tokens) >= 2): + return send_status_400(context, language) + if not data.command.tokens[1].lower() in ExecAllowed: + return send_status(context, 404, language, context.endpoint.get_string("statuses.404", language), summary=False) command = data.command.tokens[1].lower() output = subprocess.run( ("sh", "-c", f"export PATH=$PATH:/usr/games; {command}"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode() #
{html_escape(text)}")) + return send_message(context, {"text_html": f'
{html_escape(text)}'}) + +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."}) + open("./.WinDog.Restart.lock", 'w').close() + return send_message(context, {"text_plain": "Bot restart queued."}) RegisterModule(name="System", endpoints=[ SafeNamespace(names=["exec"], handler=cExec, body=True), + SafeNamespace(names=["restart"], handler=cRestart), ]) diff --git a/ModWinDog/System/System.yaml b/ModWinDog/System/System.yaml index 3a5adfa..dc60af9 100755 --- a/ModWinDog/System/System.yaml +++ b/ModWinDog/System/System.yaml @@ -2,4 +2,7 @@ endpoints: exec: summary: en: Execute a system command from the allowed ones and return stdout+stderr. + statuses: + 404: + en: The requested command is not available. diff --git a/RunWinDog.sh b/RunWinDog.sh new file mode 100755 index 0000000..43f8bf9 --- /dev/null +++ b/RunWinDog.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +windog_start(){ + rm -f ./.WinDog.Restart.lock + python3 ./WinDog.py & +} + +cd "$( dirname "$( realpath "$0" )" )" +windog_start + +while true +do + if [ -f ./.WinDog.Restart.lock ] + then + kill "$!" + windog_start + fi + sleep 5 +done \ No newline at end of file diff --git a/StartWinDog.sh b/StartWinDog.sh deleted file mode 100755 index 34251d5..0000000 --- a/StartWinDog.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cd "$( dirname "$( realpath "$0" )" )" -python3 ./WinDog.py diff --git a/WinDog.py b/WinDog.py index 81a9502..f71ac93 100755 --- a/WinDog.py +++ b/WinDog.py @@ -11,6 +11,7 @@ from html import escape as html_escape, unescape as html_unescape from os import listdir from os.path import isfile, isdir from random import choice, choice as randchoice, randint +from sys import exc_info as sys_exc_info from threading import Thread from traceback import format_exc, format_exc as traceback_format_exc from urllib import parse as urlparse, parse as urllib_parse @@ -32,23 +33,35 @@ def ObjectUnion(*objects:object, clazz:object=None): dikt[key] = value return (clazz or auto_clazz)(**dikt) -def Log(text:str, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None: - endline = '\n' - if newline == False or (inline and newline == None): - endline = '' - text = (text if inline else f"[{level}] [{time.ctime()}] [{int(time.time())}] {text}") - if LogToConsole: - print(text, end=endline) - if LogToFile: - open("./Log.txt", 'a').write(text + endline) - -def call_or_return(obj:any) -> any: - return (obj() if callable(obj) else obj) +def ObjectClone(obj:object): + return ObjectUnion(obj, {}); def SureArray(array:any) -> list|tuple: return (array if type(array) in [list, tuple] else [array]) -def ObjGet(node:object, query:str, /) -> any: +def app_log(text:str=None, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None: + if not text: + text = get_exception_text(full=True) + endline = '\n' + if newline == False or (inline and newline == None): + endline = '' + text = (str(text) if inline else f"[{level}] [{time.ctime()}] [{int(time.time())}] {text}") + if LogToConsole: + print(text, end=endline) + if LogToFile: + open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Data/Log.txt"), 'a').write(text + endline) + +def get_exception_text(full:bool=False): + exc_type, exc_value, exc_traceback = sys_exc_info() + text = f'{exc_type.__qualname__}: {exc_value}' + if full: + text = f'@{exc_traceback.tb_frame.f_code.co_name}:{exc_traceback.tb_lineno} {text}' + return text + +def call_or_return(obj:any, *args) -> any: + return (obj(*args) if callable(obj) else obj) + +def obj_get(node:object, query:str, /) -> any: for key in query.split('.'): if hasattr(node, "__getitem__") and node.__getitem__: # dicts and such @@ -67,19 +80,30 @@ def ObjGet(node:object, query:str, /) -> any: def good_yaml_load(text:str): return yaml_load(text.replace("\t", " "), Loader=yaml_BaseLoader) -def get_string(bank:dict, query:str|dict, lang:str=None) -> str|list[str]|None: - if not (result := ObjGet(bank, f"{query}.{lang or DefaultLanguage}")): - if not (result := ObjGet(bank, f"{query}.en")): - result = ObjGet(bank, query) +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}")): + if not (result := obj_get(bank, f"{query}.en")): + result = obj_get(bank, query) + if result: + result = result.strip() return result -def help_text(endpoint, lang:str=None) -> str: +def get_help_text(endpoint, lang:str=None, prefix:str=None) -> str: + if type(endpoint) == str: + endpoint = instanciate_endpoint(endpoint, prefix) global_string = (lambda query: get_string(GlobalStrings, query, lang)) - text = f'{endpoint.get_string("summary", lang) or ""}\n\n{global_string("usage")}:' + text = f'{endpoint.get_string("summary", lang) or ""}\n\n{global_string("usage")}: {prefix or ""}{endpoint.name}' if endpoint.arguments: for argument in endpoint.arguments: - if endpoint.arguments[argument]: - text += f' <{endpoint.get_string(f"arguments.{argument}", lang) or endpoint.module.get_string(f"arguments.{argument}", lang) or argument}>' + if not ((endpoint.body != None) and (endpoint.arguments[argument] == False)): + argument_help = (endpoint.get_string(f"arguments.{argument}", lang) + or endpoint.module.get_string(f"arguments.{argument}", lang) + or argument) + if endpoint.arguments[argument] == True: + text += f' <{argument_help}>' + elif endpoint.arguments[argument] == False: + text += f' [{argument_help}]' body_help = (endpoint.get_string("body", lang) or endpoint.module.get_string("body", lang)) quoted_help = (global_string("quoted_message") + (f': {body_help}' if body_help else '')) if not body_help: @@ -95,51 +119,38 @@ def help_text(endpoint, lang:str=None) -> str: text += f' <{quoted_help}>' elif endpoint.quoted == False: text += f' [{quoted_help}]' + if (extra := call_or_return(endpoint.help_extra, endpoint, lang)): + text += f'\n\n{extra}' return text def strip_url_scheme(url:str) -> str: tokens = urlparse.urlparse(url) return f"{tokens.netloc}{tokens.path}" -def RandPercent() -> int: - num = randint(0,100) - return (f'{num}.00' if num == 100 else f'{num}.{randint(0,9)}{randint(0,9)}') - -def RandHexStr(length:int) -> str: - hexa = '' - for char in range(length): - hexa += choice('0123456789abcdef') - return hexa - -def GetUserSettings(user_id:str) -> SafeNamespace|None: - try: - return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get()) - except EntitySettings.DoesNotExist: - return None - -def ParseCommand(text:str, platform:str) -> SafeNamespace|None: +def TextCommandData(text:str, platform:str) -> CommandData|None: if not text: return None text = text.strip() - try: # ensure text is a non-empty command - if not (text[0] in CmdPrefixes and text[1:].strip()): + try: # ensure text is non-empty and an actual command + if not (text[0] in CommandPrefixes and text[1:].strip()): return None except IndexError: return None command = SafeNamespace() 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()): return None command.body = text[len(command.tokens[0]):].strip() - if command.name not in Endpoints: - return command - if (endpoint_arguments := Endpoints[command.name].arguments): + if not (endpoint := obj_get(Endpoints, command.name)): + return command # TODO shouldn't this return None? + if (endpoint.arguments): command.arguments = SafeNamespace() index = 1 - for key in endpoint_arguments: - if not endpoint_arguments[key]: - continue # skip optional (False) arguments for now, they will be implemented later + for key in endpoint.arguments: + if (endpoint.body != None) and (endpoint.arguments[key] == False): + continue # skip optional (False) arguments for now if command expects a body, they will be implemented later try: value = command.tokens[index] command.body = command.body[len(value):].strip() @@ -151,7 +162,7 @@ def ParseCommand(text:str, platform:str) -> SafeNamespace|None: def OnInputMessageParsed(data:InputMessageData) -> None: dump_message(data, prefix='> ') - handle_bridging(SendMessage, data, from_sent=False) + handle_bridging(send_message, data, from_sent=False) update_user_db(data.user) def OnOutputMessageSent(output_data:OutputMessageData, input_data:InputMessageData, from_sent:bool) -> None: @@ -159,14 +170,13 @@ def OnOutputMessageSent(output_data:OutputMessageData, input_data:InputMessageDa output_data = ObjectUnion(output_data, {"room": input_data.room}) dump_message(output_data, prefix=f'<{"*" if from_sent else " "}') if not from_sent: - handle_bridging(SendMessage, output_data, from_sent=True) + handle_bridging(send_message, output_data, from_sent=True) -# 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")): + if (text_plain := obj_get(data, "text_plain")): text_plain = f"<{data.user.name}>: {text_plain}" - if (text_html := ObjGet(data, "text_html")): + if (text_html := obj_get(data, "text_html")): text_html = (urlparse.quote(f"<{data.user.name}>: ") + text_html) for bridge in BridgesConfig: if data.room.id not in bridge: @@ -200,9 +210,28 @@ def dump_message(data:InputMessageData, prefix:str='') -> None: if DumpToConsole: print(text, data) if DumpToFile: - open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n') + open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Data/Dump.txt"), 'a').write(text + '\n') -def SendMessage(context:EventContext, data:OutputMessageData, from_sent:bool=False) -> None: +def send_status(context:EventContext, code:int, lang:str=None, extra:str=None, preamble:bool=True, summary:bool=True): + global_string = (lambda query: get_string(GlobalStrings, query, lang)) + summary_text = (global_string(f"statuses.{code}.summary") or '') + 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()}) + +def send_status_400(context:EventContext, lang:str=None, extra:str=None): + return send_status(context, 400, lang, + f'{context.endpoint.get_help_text(lang)}\n\n{extra or ""}', preamble=False, summary=False) + +def send_status_error(context:EventContext, lang:str=None, code:int=500, extra:str=None): + result = send_status(context, code, lang, + f'{html_escape(get_exception_text())}\n\n{extra or ""}') + app_log() + return result + +def send_message(context:EventContext, data:OutputMessageData, *, from_sent:bool=False): + context = ObjectClone(context) data = (OutputMessageData(**data) if type(data) == dict else data) if data.text_html and not data.text_plain: data.text_plain = BeautifulSoup(data.text_html, "html.parser").get_text() @@ -229,15 +258,18 @@ def SendMessage(context:EventContext, data:OutputMessageData, from_sent:bool=Fal OnOutputMessageSent(data, context.data, from_sent) return result -def SendNotice(context:EventContext, data) -> None: +def send_notice(context:EventContext, data): pass -def DeleteMessage(context:EventContext, data) -> None: +def edit_message(context:EventContext, data:MessageData): + pass + +def delete_message(context:EventContext, data:MessageData): pass def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, event_class=None, manager_class=None, agent_info=None) -> None: Platforms[name.lower()] = SafeNamespace(name=name, main=main, sender=sender, linker=linker, event_class=event_class, manager_class=manager_class, agent_info=agent_info) - Log(f"{name}, ", inline=True) + app_log(f"{name}, ", inline=True) def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None: module = SafeNamespace(group=group, endpoints=endpoints, get_string=(lambda query, lang=None: None)) @@ -245,27 +277,40 @@ def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None: module.strings = good_yaml_load(open(file, 'r').read()) module.get_string = (lambda query, lang=None: get_string(module.strings, query, lang)) Modules[name] = module - Log(f"{name}, ", inline=True) + if group not in ModuleGroups: + ModuleGroups[group] = [] + ModuleGroups[group].append(name) + app_log(f"{name}, ", inline=True) for endpoint in endpoints: endpoint.module = module for name in endpoint.names: Endpoints[name] = endpoint -def CallEndpoint(name:str, context:EventContext, data:InputMessageData): - endpoint = Endpoints[name] +def instanciate_endpoint(name:str, prefix:str): + if not (endpoint := obj_get(Endpoints, name)): + return None + endpoint = ObjectClone(endpoint) + endpoint.name = name + endpoint.get_string = (lambda query=name, lang=None: + endpoint.module.get_string(f"endpoints.{name}.{query}", lang)) + endpoint.get_help_text = (lambda lang=None: get_help_text(endpoint, lang, prefix)) + return endpoint + +def call_endpoint(context:EventContext, data:InputMessageData): + if not ((command := data.command) and (name := command.name)): + return + if not (endpoint := instanciate_endpoint(name, command.prefix)): + return context.data = data context.module = endpoint.module context.endpoint = endpoint - context.endpoint.get_string = (lambda query=data.command.name, lang=None: - endpoint.module.get_string(f"endpoints.{data.command.name}.{query}", lang)) - context.endpoint.help_text = (lambda lang=None: help_text(endpoint, lang)) if callable(agent_info := Platforms[context.platform].agent_info): Platforms[context.platform].agent_info = agent_info() return endpoint.handler(context, data) -def WriteNewConfig() -> None: - Log("πΎοΈ No configuration found! Generating and writing to `./Config.py`... ", inline=True) - with open("./Config.py", 'w') as configFile: +def write_new_config() -> None: + app_log("πΎοΈ No configuration found! Generating and writing to `./Data/Config.py`... ", inline=True) + with open("./Data/Config.py", 'w') as configFile: opening = '# windog config start #' closing = '# end windog config #' for folder in ("LibWinDog", "ModWinDog"): @@ -279,50 +324,49 @@ def WriteNewConfig() -> None: except IndexError: pass -def Main() -> None: +def app_main() -> None: #SetupDb() - Log(f"π¨οΈ Initializing Platforms... ", newline=False) + app_log(f"π¨οΈ Initializing Platforms... ", newline=False) for platform in Platforms.values(): if platform.main(): - Log(f"{platform.name}, ", inline=True) - Log("...Done. β οΈ", inline=True, newline=True) - Log("πΆοΈ WinDog Ready!") + app_log(f"{platform.name}, ", inline=True) + app_log("...Done. β οΈ", inline=True, newline=True) + app_log("πΆοΈ WinDog Ready!") while True: time.sleep(9**9) if __name__ == '__main__': - Log("ποΈ WinDog Starting...") + app_log("ποΈ WinDog Starting...") GlobalStrings = good_yaml_load(open("./WinDog.yaml", 'r').read()) Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {} for folder in ("LibWinDog/Platforms", "ModWinDog"): match folder: case "LibWinDog/Platforms": - Log("π©οΈ Loading Platforms... ", newline=False) + app_log("π©οΈ Loading Platforms... ", newline=False) case "ModWinDog": - Log("π©οΈ Loading Modules... ", newline=False) + app_log("π©οΈ Loading Modules... ", newline=False) for name in listdir(f"./{folder}"): path = f"./{folder}/{name}" - if isfile(path): - exec(open(path, 'r').read()) + if path.endswith(".py") and isfile(path): + exec(open(path).read()) elif isdir(path): files = listdir(path) if f"{name}.py" in files: files.remove(f"{name}.py") exec(open(f"{path}/{name}.py", 'r').read()) - for file in files: - if file.endswith(".py"): - exec(open(f"{path}/{name}.py", 'r').read()) - Log("...Done. β οΈ", inline=True, newline=True) + #for file in files: + # if file.endswith(".py"): + # exec(open(f"{path}/{file}", 'r').read()) + app_log("...Done. β οΈ", inline=True, newline=True) - Log("π½οΈ Loading Configuration... ", newline=False) - from Config import * - if isfile("./Config.py"): - from Config import * + app_log("π½οΈ Loading Configuration... ", newline=False) + if isfile("./Data/Config.py"): + exec(open("./Data/Config.py", 'r').read()) else: - WriteNewConfig() - Log("Done. β οΈ", inline=True, newline=True) + write_new_config() + app_log("Done. β οΈ", inline=True, newline=True) - Main() - Log("ποΈ WinDog Stopping...") + app_main() + app_log("ποΈ WinDog Stopping...") diff --git a/WinDog.yaml b/WinDog.yaml old mode 100644 new mode 100755 index ab4ad13..b5d2b2b --- a/WinDog.yaml +++ b/WinDog.yaml @@ -1,3 +1,6 @@ +error: + en: Error + it: Errore or: en: or it: o @@ -10,4 +13,39 @@ text: usage: en: Usage it: Uso +statuses: + 102: + title: + en: Processing + icon: βοΈ + 403: + title: + en: Forbidden + it: Vietato + summary: + en: You don't have the necessary permissions to access the requested resource or complete the specified action. + it: Non hai i permessi necessari per accedere alla risorsa richiesta o completare l'azione specificata. + icon: βοΈ + 404: + title: + en: Not Found + it: Non Trovato + summary: + en: The requested resource was not found or the specified action cannot be completed. + it: La risorsa richiesta non Γ¨ stata trovata o non Γ¨ possibile completare l'azione specificata. + icon: π³οΈ + 500: + title: + en: Internal Error + it: Errore Interno + icon: π£οΈ + 503: + title: + en: Service Unavailable + it: Servizio non Disponibile + icon: πͺ¦οΈ + 504: + title: + en: Gateway Timeout + icon: π§»οΈ diff --git a/requirements.txt b/requirements.txt index 1e72581..f45243d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ beautifulsoup4 -Markdown peewee PyYAML