From 6ebc68127e949340298dcdf03d35e56d5bd3a4c8 Mon Sep 17 00:00:00 2001 From: octospacc Date: Sat, 10 Aug 2024 01:36:54 +0200 Subject: [PATCH] Legacy removals, code restructuring, add send_... functions and better help --- .gitignore | 9 +- LibWinDog/Config.py | 10 +- LibWinDog/Database.py | 17 +- LibWinDog/Platforms/Mastodon/Mastodon.py | 15 +- LibWinDog/Platforms/Matrix/Matrix.py | 27 ++- LibWinDog/Platforms/Telegram/Telegram.py | 17 +- LibWinDog/Platforms/Web/Web.py | 13 +- LibWinDog/Types.py | 11 +- ModWinDog/Base/Base.py | 49 ++--- ModWinDog/Broadcast/Broadcast.py | 24 +-- ModWinDog/Codings/Codings.py | 59 ++++-- ModWinDog/Codings/Codings.yaml | 0 ModWinDog/Codings/requirements.txt | 1 + ModWinDog/Dumper/Dumper.py | 18 +- ModWinDog/Echo/Echo.py | 8 +- ModWinDog/GPT/GPT.py | 14 +- ModWinDog/GPT/GPT.yaml | 2 + ModWinDog/Hashing/Hashing.py | 18 +- ModWinDog/Help/Help.py | 41 +++-- ModWinDog/Internet/Internet.py | 99 +++++----- ModWinDog/Multifun/Multifun.py | 12 +- ModWinDog/Percenter/Percenter.py | 13 +- ModWinDog/Scrapers/Scrapers.py | 55 +++--- ModWinDog/Scripting/Scripting.py | 26 ++- ModWinDog/Start/Start.py | 4 +- ModWinDog/System/System.py | 19 +- ModWinDog/System/System.yaml | 3 + RunWinDog.sh | 19 ++ StartWinDog.sh | 3 - WinDog.py | 218 ++++++++++++++--------- WinDog.yaml | 38 ++++ requirements.txt | 1 - 32 files changed, 512 insertions(+), 351 deletions(-) mode change 100644 => 100755 ModWinDog/Codings/Codings.yaml create mode 100755 ModWinDog/Codings/requirements.txt create mode 100755 RunWinDog.sh delete mode 100755 StartWinDog.sh mode change 100644 => 100755 WinDog.yaml 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 feature def cHelp(context:EventContext, data:InputMessageData) -> None: - text = (context.endpoint.get_string(lang=data.user.settings.language) or '').strip() language = data.user.settings.language - for module in Modules: - summary = Modules[module].get_string("summary", language) - endpoints = Modules[module].endpoints - 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* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else '')) - text = text.strip() - SendMessage(context, {"text_html": text}) + prefix = data.command.prefix + if (endpoint := data.command.arguments.endpoint): + if endpoint[0] in CommandPrefixes: + endpoint = endpoint[1:] + if endpoint in Endpoints: + return send_message(context, {"text_html": get_help_text(endpoint, language, prefix)}) + text = (context.endpoint.get_string(lang=data.user.settings.language) or '').strip() + for group in ModuleGroups: + text += f"\n\n[ {group} ]" + for module in ModuleGroups[group]: + summary = Modules[module].get_string("summary", language) + endpoints = Modules[module].endpoints + 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 = text.strip() + return send_message(context, {"text_html": text}) RegisterModule(name="Help", group="Basic", endpoints=[ - SafeNamespace(names=["help"], handler=cHelp), + SafeNamespace(names=["help"], handler=cHelp, arguments={ + "endpoint": False, + }), ]) diff --git a/ModWinDog/Internet/Internet.py b/ModWinDog/Internet/Internet.py index 4149a42..3e3d751 100755 --- a/ModWinDog/Internet/Internet.py +++ b/ModWinDog/Internet/Internet.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 # """ @@ -12,10 +12,14 @@ MicrosoftBingSettings = {} from urlextract import URLExtract from urllib.request import urlopen, Request +def RandomHexString(length:int) -> str: + return ''.join([randchoice('0123456789abcdef') for i in range(length)]) + def HttpReq(url:str, method:str|None=None, *, body:bytes=None, headers:dict[str, str]={"User-Agent": WebUserAgent}): return urlopen(Request(url, method=method, data=body, headers=headers)) -def cEmbedded(context:EventContext, data:InputMessageData) -> None: +def cEmbedded(context:EventContext, data:InputMessageData): + language = data.user.settings.language if len(data.command.tokens) >= 2: # Find links in command body text = (data.text_markdown + ' ' + data.text_plain) @@ -23,9 +27,7 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None: # Find links in quoted message text = ((quoted.text_markdown or '') + ' ' + (quoted.text_plain or '') + ' ' + (quoted.text_html or '')) else: - # TODO Error message - return - pass + return send_status_400(context, language) urls = URLExtract().find_urls(text) if len(urls) > 0: proto = 'https://' @@ -47,55 +49,57 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None: elif urlDomain == "vm.tiktok.com": urlDomain = "vm.vxtiktok.com" url = (urlDomain + '/' + '/'.join(url.split('/')[1:])) - SendMessage(context, {"text_plain": f"{{{proto}{url}}}"}) - # else TODO error message? + return send_message(context, {"text_plain": f"{{{proto}{url}}}"}) + return send_message(context, {"text_plain": "No links found."}) -def cWeb(context:EventContext, data:InputMessageData) -> None: +def cWeb(context:EventContext, data:InputMessageData): + language = data.user.settings.language if not (query := data.command.body): - return # TODO show message + return send_status_400(context, language) try: - QueryUrl = urlparse.quote(query) - Req = HttpReq(f'https://html.duckduckgo.com/html?q={QueryUrl}') - Caption = f'πŸ¦†πŸ”Ž "{query}": https://duckduckgo.com/?q={QueryUrl}\n\n' - Index = 0 - for Line in Req.read().decode().replace('\t', ' ').splitlines(): - if ' class="result__a" ' in Line and ' href="//duckduckgo.com/l/?uddg=' in Line: - Index += 1 - Link = urlparse.unquote(Line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&rut=')[0]) - Title = Line.strip().split('')[0].strip().split('')[-1].strip().split('>') - if len(Title) > 1: - Title = html_unescape(Title[1].strip()) - Caption += f'[{Index}] {Title} : {{{Link}}}\n\n' + query_url = urlparse.quote(query) + request = HttpReq(f'https://html.duckduckgo.com/html?q={query_url}') + caption = f'πŸ¦†πŸ”Ž "{query}": https://duckduckgo.com/?q={query_url}\n\n' + index = 0 + for line in request.read().decode().replace('\t', ' ').splitlines(): + if ' class="result__a" ' in line and ' href="//duckduckgo.com/l/?uddg=' in line: + index += 1 + link = urlparse.unquote(line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&rut=')[0]) + title = line.strip().split('')[0].strip().split('')[-1].strip().split('>') + if len(title) > 1: + title = html_unescape(title[1].strip()) + caption += f'[{index}] {title} : {{{link}}}\n\n' else: continue - SendMessage(context, {"TextPlain": f'{Caption}...'}) + return send_message(context, {"text_plain": f'{caption}...'}) except Exception: - raise + return send_status_error(context, language) -def cImages(context:EventContext, data:InputMessageData) -> None: +def cImages(context:EventContext, data:InputMessageData): pass -def cNews(context:EventContext, data:InputMessageData) -> None: +def cNews(context:EventContext, data:InputMessageData): pass -def cTranslate(context:EventContext, data:InputMessageData) -> None: +def cTranslate(context:EventContext, data:InputMessageData): + language = data.user.settings.language instances = ["lingva.ml", "lingva.lunar.icu"] - language_to = data.command.arguments["language_to"] + language_to = data.command.arguments.language_to text_input = (data.command.body or (data.quoted and data.quoted.text_plain)) if not (text_input and language_to): - return SendMessage(context, {"TextPlain": f"Usage: /translate "}) + return send_status_400(context, language) try: result = json.loads(HttpReq(f'https://{randchoice(instances)}/api/v1/auto/{language_to}/{urlparse.quote(text_input)}').read()) - SendMessage(context, {"TextPlain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"}) + return send_message(context, {"text_plain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"}) except Exception: - raise + return send_status_error(context, language) # unsplash source appears to be deprecated! #def cUnsplash(context:EventContext, data:InputMessageData) -> None: # try: # Req = HttpReq(f'https://source.unsplash.com/random/?{urlparse.quote(data.command.body)}') # ImgUrl = Req.geturl().split('?')[0] -# SendMessage(context, { +# send_message(context, { # "TextPlain": f'{{{ImgUrl}}}', # "TextMarkdown": MarkdownCode(ImgUrl, True), # "Media": Req.read(), @@ -103,20 +107,21 @@ def cTranslate(context:EventContext, data:InputMessageData) -> None: # except Exception: # raise -def cSafebooru(context:EventContext, data:InputMessageData) -> None: - ApiUrl = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags=' +def cSafebooru(context:EventContext, data:InputMessageData): + language = data.user.settings.language + api_url = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags=' try: img_id, img_url = None, None if (query := data.command.body): for i in range(7): # retry a bunch of times if we can't find a really random result - ImgUrls = HttpReq(f'{ApiUrl}md5:{RandHexStr(3)}%20{urlparse.quote(query)}').read().decode().split(' file_url="')[1:] - if ImgUrls: + img_urls = HttpReq(f'{api_url}md5:{RandomHexString(3)}%20{urlparse.quote(query)}').read().decode().split(' file_url="')[1:] + if img_urls: break - if not ImgUrls: # literal search - ImgUrls = HttpReq(f'{ApiUrl}{urlparse.quote(query)}').read().decode().split(' file_url="')[1:] - if not ImgUrls: - return SendMessage(context, {"Text": "Error: Could not get any result from Safebooru."}) - ImgXml = choice(ImgUrls) + if not img_urls: # literal search + img_urls = HttpReq(f'{api_url}{urlparse.quote(query)}').read().decode().split(' file_url="')[1:] + if not img_urls: + return send_status(context, 404, language, "Could not get any result from Safebooru.", summary=False) + ImgXml = choice(img_urls) img_url = ImgXml.split('"')[0] img_id = ImgXml.split(' id="')[1].split('"')[0] else: @@ -127,14 +132,14 @@ def cSafebooru(context:EventContext, data:InputMessageData) -> None: img_id = img_url.split('?')[-1] break if img_url: - SendMessage(context, OutputMessageData( + return send_message(context, OutputMessageData( text_plain=f"[{img_id}]\n{{{img_url}}}", text_html=f"[{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() # text = (re_compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub('', output)) - SendMessage(context, OutputMessageData( - text_plain=text, text_html=f"
{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