diff --git a/.gitignore b/.gitignore index dbf1ccd..a7709a7 100755 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ /Dump.txt /Log.txt /Selenium-WinDog/ -/downloaded_files +/downloaded_files/ /session.txt *.pyc diff --git a/LibWinDog/Config.py b/LibWinDog/Config.py index 52e50bb..c50f5c6 100755 --- a/LibWinDog/Config.py +++ b/LibWinDog/Config.py @@ -4,12 +4,14 @@ # ================================== # """ # windog config start # """ -# If you have modified the bot's code, you should set this +# 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. LogToConsole = True LogToFile = True +# Dumping of the bot's remote events. Should stay off unless needed for debugging. DumpToConsole = False DumpToFile = False diff --git a/LibWinDog/Database.py b/LibWinDog/Database.py index 95c233e..54bf325 100755 --- a/LibWinDog/Database.py +++ b/LibWinDog/Database.py @@ -6,3 +6,17 @@ class BaseModel(Model): class Meta: database = Db +class Entity(BaseModel): + id = CharField(null=True) + id_hash = CharField() + #settings = ForeignKeyField(EntitySettings, backref="entity") + #language = CharField(null=True) + +class User(Entity): + pass + +class Room(Entity): + pass + +Db.create_tables([User, Room], safe=True) + diff --git a/LibWinDog/Platforms/Mastodon/Mastodon.py b/LibWinDog/Platforms/Mastodon/Mastodon.py index 4ab9d79..87d66dc 100755 --- a/LibWinDog/Platforms/Mastodon/Mastodon.py +++ b/LibWinDog/Platforms/Mastodon/Mastodon.py @@ -14,6 +14,7 @@ MastodonUrl, MastodonToken = None, None import mastodon from bs4 import BeautifulSoup +from magic import Magic def MastodonMain() -> bool: if not (MastodonUrl and MastodonToken): @@ -36,19 +37,19 @@ def MastodonHandler(event): if command: command.messageId = event['status']['id'] if command.Name in Endpoints: - Endpoints[command.Name]["handler"]({"Event": event, "Manager": Mastodon}, command) + CallEndpoint(command.Name, EventContext(platform="mastodon", event=event, manager=Mastodon), command) -def MastodonSender(event, manager, data:OutputMessageData, destination, textPlain, textMarkdown) -> None: +def MastodonSender(context:EventContext, data:OutputMessageData, destination, textPlain, textMarkdown) -> None: if InDict(data, 'Media'): - Media = manager.media_post(data['Media'], Magic(mime=True).from_buffer(data['Media'])) + Media = context.manager.media_post(data['Media'], Magic(mime=True).from_buffer(data['Media'])) while Media['url'] == 'null': - Media = manager.media(Media) + Media = context.manager.media(Media) if textPlain or Media: - manager.status_post( - status=(textPlain + '\n\n@' + event['account']['acct']), + context.manager.status_post( + status=(textPlain + '\n\n@' + context.event['account']['acct']), media_ids=(Media if InDict(data, 'Media') else None), - in_reply_to_id=event['status']['id'], - visibility=('direct' if event['status']['visibility'] == 'direct' else 'unlisted'), + in_reply_to_id=context.event['status']['id'], + visibility=('direct' if context.event['status']['visibility'] == 'direct' else 'unlisted'), ) RegisterPlatform(name="Mastodon", main=MastodonMain, sender=MastodonSender, managerClass=mastodon.Mastodon) diff --git a/LibWinDog/Platforms/Telegram/Telegram.py b/LibWinDog/Platforms/Telegram/Telegram.py index 04c8bb6..c3c3157 100755 --- a/LibWinDog/Platforms/Telegram/Telegram.py +++ b/LibWinDog/Platforms/Telegram/Telegram.py @@ -41,13 +41,15 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData: data.room = SafeNamespace( id = f"telegram:{message.chat.id}", tag = message.chat.username, - name = message.chat.title, + name = (message.chat.title or message.chat.first_name), ) data.user = SafeNamespace( id = f"telegram:{message.from_user.id}", tag = message.from_user.username, name = message.from_user.first_name, ) + #if (db_user := GetUserData(data.user.id)): + # data.user.language = db_user.language return data def TelegramHandlerWrapper(update:telegram.Update, context:CallbackContext=None) -> None: @@ -63,6 +65,7 @@ def TelegramHandlerCore(update:telegram.Update, context:CallbackContext=None) -> cmd = ParseCmd(update.message.text) if cmd: cmd.command = data.command + cmd.quoted = data.quoted cmd.messageId = update.message.message_id cmd.TextPlain = cmd.Body cmd.TextMarkdown = update.message.text_markdown_v2 @@ -86,27 +89,28 @@ def TelegramHandlerCore(update:telegram.Update, context:CallbackContext=None) -> "Id": f'telegram:{update.message.reply_to_message.from_user.id}', }), }) - Endpoints[cmd.Name]["handler"]({"Event": update, "Manager": context}, cmd) - #Endpoints[cmd.Name]["handler"](SafeNamespace(platform="telegram", event=update, manager=context), cmd) + CallEndpoint(cmd.Name, EventContext(platform="telegram", event=update, manager=context), cmd) -def TelegramSender(event, manager, data:OutputMessageData, destination, textPlain, textMarkdown) -> None: +def TelegramSender(context:EventContext, data:OutputMessageData, destination, textPlain, textMarkdown): + result = None if destination: - manager.bot.send_message(destination, text=textPlain) + result = context.manager.bot.send_message(destination, text=textPlain) else: - replyToId = (data["ReplyTo"] if ("ReplyTo" in data and data["ReplyTo"]) else event.message.message_id) + replyToId = (data["ReplyTo"] if ("ReplyTo" in data and data["ReplyTo"]) else context.event.message.message_id) if InDict(data, "Media") and not InDict(data, "media"): data["media"] = {"bytes": data["Media"]} if InDict(data, "media"): for medium in SureArray(data["media"]): - event.message.reply_photo( + result = context.event.message.reply_photo( (DictGet(medium, "bytes") or DictGet(medium, "url")), caption=(textMarkdown if textMarkdown else textPlain if textPlain else None), parse_mode=("MarkdownV2" if textMarkdown else None), reply_to_message_id=replyToId) elif textMarkdown: - event.message.reply_markdown_v2(textMarkdown, reply_to_message_id=replyToId) + result = context.event.message.reply_markdown_v2(textMarkdown, reply_to_message_id=replyToId) elif textPlain: - event.message.reply_text(textPlain, reply_to_message_id=replyToId) + result = context.event.message.reply_text(textPlain, reply_to_message_id=replyToId) + return TelegramMakeInputMessageData(result) def TelegramLinker(data:InputMessageData) -> SafeNamespace: linked = SafeNamespace() diff --git a/ModWinDog/Base/Base.py b/ModWinDog/Base/Base.py new file mode 100755 index 0000000..b45fb32 --- /dev/null +++ b/ModWinDog/Base/Base.py @@ -0,0 +1,65 @@ +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # + +import re, subprocess + +def cStart(context:EventContext, data:InputMessageData) -> None: + SendMessage(context, {"Text": choice(Locale.__('start')).format(data.User.Name)}) + +def cSource(context:EventContext, data:InputMessageData) -> None: + SendMessage(context, {"TextPlain": ("""\ +* 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: + pass + +# Module: Config +# ... +#def cConfig(update:telegram.Update, context:CallbackContext) -> None: +# Cmd = TelegramHandleCmd(update) +# if not Cmd: return +# # ... area: eu, us, ... +# # ... language: en, it, ... +# # ... userdata: import, export, delete + +def cPing(context:EventContext, data:InputMessageData) -> None: + SendMessage(context, {"Text": "*Pong!*"}) + +#def cTime(update:Update, context:CallbackContext) -> None: +# update.message.reply_markdown_v2( +# CharEscape(choice(Locale.__('time')).format(time.ctime().replace(' ', ' ')), 'MARKDOWN_SPEECH'), +# reply_to_message_id=update.message.message_id) + +def cEval(context:EventContext, data:InputMessageData) -> None: + SendMessage(context, {"Text": choice(Locale.__('eval'))}) + +def cExec(context:EventContext, data:InputMessageData) -> None: + if len(data.Tokens) >= 2 and data.Tokens[1].lower() in ExecAllowed: + cmd = data.Tokens[1].lower() + Out = subprocess.run(('sh', '-c', f'export PATH=$PATH:/usr/games; {cmd}'), + stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode() + # + Caption = (re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])').sub('', Out)) + SendMessage(context, { + "TextPlain": Caption, + "TextMarkdown": MarkdownCode(Caption, True), + }) + else: + SendMessage(context, {"Text": choice(Locale.__('eval'))}) + +RegisterModule(name="Misc", endpoints=[ + SafeNamespace(names=["start"], summary="Salutes the user, hinting that the bot is working and providing basic quick help.", handler=cStart), + SafeNamespace(names=["source"], summary="Provides a copy of the bot source codes and/or instructions on how to get it.", handler=cSource), + #SafeNamespace(names=["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr), + SafeNamespace(names=["ping"], summary="Responds pong, useful for testing messaging latency.", handler=cPing), + SafeNamespace(names=["eval"], summary="Execute a Python command (or safe literal operation) in the current context. Currently not implemented.", handler=cEval), + SafeNamespace(names=["exec"], summary="Execute a system command from the allowed ones and return stdout+stderr.", handler=cExec), + #SafeNamespace(names=["format"], summary="Reformat text using an handful of rules. Not yet implemented.", handler=cFormat), + #SafeNamespace(names=["frame"], summary="Frame someone's message into a platform-styled image. Not yet implemented.", handler=cFrame), + #SafeNamespace(names=["repeat"], summary="I had this planned but I don't remember what this should have done. Not yet implemented.", handler=cRepeat), +]) + diff --git a/ModWinDog/Broadcast.py b/ModWinDog/Broadcast/Broadcast.py similarity index 77% rename from ModWinDog/Broadcast.py rename to ModWinDog/Broadcast/Broadcast.py index bf40c34..aa0833b 100755 --- a/ModWinDog/Broadcast.py +++ b/ModWinDog/Broadcast/Broadcast.py @@ -12,9 +12,9 @@ def cBroadcast(context:EventContext, data:InputMessageData) -> None: SendMessage(context, {"TextPlain": data.command.body}, destination) SendMessage(context, {"TextPlain": "Executed."}) -RegisterModule(name="Broadcast", endpoints={ - "Broadcast": CreateEndpoint(["broadcast"], summary="Sends an admin message over to any chat destination.", handler=cBroadcast, arguments={ +RegisterModule(name="Broadcast", endpoints=[ + SafeNamespace(names=["broadcast"], summary="Sends an admin message over to any chat destination.", handler=cBroadcast, arguments={ "destination": True, }), -}) +]) diff --git a/ModWinDog/Codings.py b/ModWinDog/Codings/Codings.py similarity index 100% rename from ModWinDog/Codings.py rename to ModWinDog/Codings/Codings.py diff --git a/ModWinDog/Dumper/Dumper.py b/ModWinDog/Dumper/Dumper.py new file mode 100755 index 0000000..72b1564 --- /dev/null +++ b/ModWinDog/Dumper/Dumper.py @@ -0,0 +1,16 @@ +# ================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ================================== # + +from json import dumps as json_dumps + +def cDump(context:EventContext, data:InputMessageData): + if not (message := ObjGet(data, "quoted")): + pass + SendMessage(context, {"TextPlain": json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")}) + +RegisterModule(name="Dumper", group="Geek", endpoints=[ + SafeNamespace(names=["dump"], handler=cDump), +]) + diff --git a/ModWinDog/Dumper/Dumper.yaml b/ModWinDog/Dumper/Dumper.yaml new file mode 100644 index 0000000..eec9361 --- /dev/null +++ b/ModWinDog/Dumper/Dumper.yaml @@ -0,0 +1,6 @@ +summary: + en: Functions for dumping of various data. +dump: + summary: + en: Dumps the data for a quoted message and returns the JSON representation. + diff --git a/ModWinDog/Echo/Echo.py b/ModWinDog/Echo/Echo.py new file mode 100644 index 0000000..f6e9fbf --- /dev/null +++ b/ModWinDog/Echo/Echo.py @@ -0,0 +1,27 @@ +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # + +def cEcho(context:EventContext, data:InputMessageData) -> None: + text = ObjGet(data, "command.body") + if text: + prefix = "🗣️ " + #prefix = f"[🗣️]({context.linker(data).message}) " + if len(data.Tokens) == 2: + nonascii = True + for char in data.Tokens[1]: + if ord(char) < 256: + nonascii = False + break + if nonascii: + # text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis) + prefix = '' + SendMessage(context, {"Text": (prefix + text)}) + else: + SendMessage(context, {"Text": choice(Locale.__('echo.empty'))}) #context.endpoint.get_string('empty') + +RegisterModule(name="Echo", endpoints=[ + SafeNamespace(names=["echo"], handler=cEcho), +]) + diff --git a/ModWinDog/Echo/Echo.yaml b/ModWinDog/Echo/Echo.yaml new file mode 100644 index 0000000..0ddb8a4 --- /dev/null +++ b/ModWinDog/Echo/Echo.yaml @@ -0,0 +1,8 @@ +endpoints: + echo: + summary: + en: Responds back with the original text of the received message. + empty: + en: Echo what? Give me something to repeat. + it: Echo cosa? Dimmi qualcosa da ripetere. + diff --git a/ModWinDog/ChatGPT/ChatGPT.py b/ModWinDog/GPT/GPT.py similarity index 70% rename from ModWinDog/ChatGPT/ChatGPT.py rename to ModWinDog/GPT/GPT.py index 410ccf2..4bcfc3e 100644 --- a/ModWinDog/ChatGPT/ChatGPT.py +++ b/ModWinDog/GPT/GPT.py @@ -10,14 +10,14 @@ g4fClient = G4FClient() def cGpt(context:EventContext, data:InputMessageData) -> None: if not data.command.body: return SendMessage(context, {"Text": "You must type some text."}) - output = "" - while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix + 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": data.command.body}], stream=True): output += (completion.choices[0].delta.content or "") return SendMessage(context, {"TextPlain": f"[🤖️ GPT]\n\n{output}"}) -RegisterModule(name="ChatGPT", endpoints={ - "GPT": CreateEndpoint(["gpt", "chatgpt"], summary="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!", handler=cGpt), -}) +RegisterModule(name="ChatGPT", endpoints=[ + SafeNamespace(names=["gpt", "chatgpt"], summary="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!", handler=cGpt), +]) diff --git a/ModWinDog/ChatGPT/requirements.txt b/ModWinDog/GPT/requirements.txt similarity index 100% rename from ModWinDog/ChatGPT/requirements.txt rename to ModWinDog/GPT/requirements.txt diff --git a/ModWinDog/Hashing.py b/ModWinDog/Hashing.py deleted file mode 100755 index 84c880d..0000000 --- a/ModWinDog/Hashing.py +++ /dev/null @@ -1,24 +0,0 @@ -# ================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ================================== # - -import hashlib - -def cHash(context:EventContext, data:InputMessageData) -> None: - algorithm = data.command.arguments["algorithm"] - if data.command.body and algorithm in hashlib.algorithms_available: - hashed = hashlib.new(algorithm, data.command.body.encode()).hexdigest() - SendMessage(context, { - "TextPlain": hashed, - "TextMarkdown": MarkdownCode(hashed, True), - }) - else: - SendMessage(context, {"Text": choice(Locale.__('hash.usage')).format(data.command.tokens[0], hashlib.algorithms_available)}) - -RegisterModule(name="Hashing", group="Geek", summary="Functions for hashing of textual content.", endpoints={ - "Hash": CreateEndpoint(names=["hash"], summary="Responds with the hash-sum of a message received.", handler=cHash, arguments={ - "algorithm": True, - }), -}) - diff --git a/ModWinDog/Hashing/Hashing.py b/ModWinDog/Hashing/Hashing.py new file mode 100755 index 0000000..5eaaec7 --- /dev/null +++ b/ModWinDog/Hashing/Hashing.py @@ -0,0 +1,24 @@ +# ================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ================================== # + +import hashlib + +def cHash(context:EventContext, data:InputMessageData): + text_input = ObjGet(data, "command.body") + algorithm = ObjGet(data, "command.arguments.algorithm") + if not (text_input and (algorithm in hashlib.algorithms_available)): + return SendMessage(context, {"Text": choice(Locale.__('hash.usage')).format(data.command.tokens[0], hashlib.algorithms_available)}) + hashed = hashlib.new(algorithm, text_input.encode()).hexdigest() + return SendMessage(context, { + "TextPlain": hashed, + "TextMarkdown": MarkdownCode(hashed, True), + }) + +RegisterModule(name="Hashing", group="Geek", summary="Functions for hashing of textual content.", endpoints=[ + SafeNamespace(names=["hash"], summary="Responds with the hash-sum of a message received.", handler=cHash, arguments={ + "algorithm": True, + }), +]) + diff --git a/ModWinDog/Hashing/Hashing.yaml b/ModWinDog/Hashing/Hashing.yaml new file mode 100644 index 0000000..27a5757 --- /dev/null +++ b/ModWinDog/Hashing/Hashing.yaml @@ -0,0 +1,14 @@ +summary: + en: Functions for calculating hashes of content. + it: Funzioni per calcolare hash di contenuti. +hash: + summary: + en: Responds with the hash-sum of the received message. + arguments: + algorithm: + en: Algorithm + it: Algoritmo + body: + en: Text to hash + it: Testo da hashare + diff --git a/ModWinDog/Help.py b/ModWinDog/Help/Help.py similarity index 53% rename from ModWinDog/Help.py rename to ModWinDog/Help/Help.py index e1ce245..fb3939e 100755 --- a/ModWinDog/Help.py +++ b/ModWinDog/Help/Help.py @@ -7,15 +7,15 @@ def cHelp(context:EventContext, data:InputMessageData) -> None: moduleList = '' for module in Modules: - summary = Modules[module]["summary"] - endpoints = Modules[module]["endpoints"] + summary = Modules[module].summary + endpoints = Modules[module].endpoints moduleList += (f"\n\n{module}" + (f": {summary}" if summary else '')) for endpoint in endpoints: - summary = endpoints[endpoint]["summary"] - moduleList += (f"\n* /{', /'.join(endpoints[endpoint]['names'])}" + (f": {summary}" if summary else '')) + summary = endpoint.summary + moduleList += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else '')) SendMessage(context, {"Text": f"[ Available Modules ]{moduleList}"}) -RegisterModule(name="Help", group="Basic", endpoints={ - "Help": CreateEndpoint(["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp), -}) +RegisterModule(name="Help", group="Basic", endpoints=[ + SafeNamespace(names=["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp), +]) diff --git a/ModWinDog/Internet/Internet.py b/ModWinDog/Internet/Internet.py index 6f2e68a..cccca91 100755 --- a/ModWinDog/Internet/Internet.py +++ b/ModWinDog/Internet/Internet.py @@ -137,14 +137,14 @@ def cSafebooru(context:EventContext, data:InputMessageData) -> None: except Exception as error: raise -RegisterModule(name="Internet", summary="Tools and toys related to the Internet.", endpoints={ - "Embedded": CreateEndpoint(["embedded"], summary="Rewrites a link, trying to bypass embed view protection.", handler=cEmbedded), - "Web": CreateEndpoint(["web"], summary="Provides results of a DuckDuckGo search.", handler=cWeb), - "Translate": CreateEndpoint(["translate"], summary="Returns the received message after translating it in another language.", handler=cTranslate, arguments={ +RegisterModule(name="Internet", summary="Tools and toys related to the Internet.", endpoints=[ + SafeNamespace(names=["embedded"], summary="Rewrites a link, trying to bypass embed view protection.", handler=cEmbedded), + SafeNamespace(names=["web"], summary="Provides results of a DuckDuckGo search.", handler=cWeb), + SafeNamespace(names=["translate"], summary="Returns the received message after translating it in another language.", handler=cTranslate, arguments={ "language_to": True, "language_from": False, }), - "Unsplash": CreateEndpoint(["unsplash"], summary="Sends a picture sourced from Unsplash.", handler=cUnsplash), - "Safebooru": CreateEndpoint(["safebooru"], summary="Sends a picture sourced from Safebooru.", handler=cSafebooru), -}) + SafeNamespace(names=["unsplash"], summary="Sends a picture sourced from Unsplash.", handler=cUnsplash), + SafeNamespace(names=["safebooru"], summary="Sends a picture sourced from Safebooru.", handler=cSafebooru), +]) diff --git a/ModWinDog/Misc.py b/ModWinDog/Misc.py deleted file mode 100755 index bbb2c0d..0000000 --- a/ModWinDog/Misc.py +++ /dev/null @@ -1,108 +0,0 @@ -# ==================================== # -# WinDog multi-purpose chatbot # -# Licensed under AGPLv3 by OctoSpacc # -# ==================================== # - -import re, subprocess - -def mPercenter(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"Text": choice(Locale.__(f'{data.Name}.{"done" if data.Body else "empty"}')).format( - Cmd=data.Tokens[0], Percent=RandPercent(), Thing=data.Body)}) - -def mMultifun(context:EventContext, data:InputMessageData) -> None: - cmdkey = data.Name - replyToId = None - if data.Quoted: - replyFromUid = data.Quoted.User.Id - # TODO work on all platforms for the bot id - if replyFromUid.split('@')[0] == TelegramToken.split(':')[0] and 'bot' in Locale.__(cmdkey): - Text = choice(Locale.__(f'{cmdkey}.bot')) - elif replyFromUid == data.User.Id and 'self' in Locale.__(cmdkey): - Text = choice(Locale.__(f'{cmdkey}.self')).format(data.User.Name) - else: - if 'others' in Locale.__(cmdkey): - Text = choice(Locale.__(f'{cmdkey}.others')).format(data.User.Name, data.Quoted.User.Name) - replyToId = data.Quoted.messageId - else: - if 'empty' in Locale.__(cmdkey): - Text = choice(Locale.__(f'{cmdkey}.empty')) - SendMessage(context, {"Text": Text, "ReplyTo": replyToId}) - -def cStart(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"Text": choice(Locale.__('start')).format(data.User.Name)}) - -def cSource(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"TextPlain": ("""\ -* 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: - pass - -# Module: Config -# ... -#def cConfig(update:telegram.Update, context:CallbackContext) -> None: -# Cmd = TelegramHandleCmd(update) -# if not Cmd: return -# # ... area: eu, us, ... -# # ... language: en, it, ... -# # ... userdata: import, export, delete - -def cPing(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"Text": "*Pong!*"}) - -def cEcho(context:EventContext, data:InputMessageData) -> None: - if data.command.body: - prefix = "🗣️ " - #prefix = f"[🗣️]({context.linker(data).message}) " - if len(data.Tokens) == 2: - nonascii = True - for char in data.Tokens[1]: - if ord(char) < 256: - nonascii = False - break - if nonascii: - # text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis) - prefix = '' - SendMessage(context, {"Text": (prefix + data.command.body)}) - else: - SendMessage(context, {"Text": choice(Locale.__('echo.empty'))}) - -#def cTime(update:Update, context:CallbackContext) -> None: -# update.message.reply_markdown_v2( -# CharEscape(choice(Locale.__('time')).format(time.ctime().replace(' ', ' ')), 'MARKDOWN_SPEECH'), -# reply_to_message_id=update.message.message_id) - -def cEval(context:EventContext, data:InputMessageData) -> None: - SendMessage(context, {"Text": choice(Locale.__('eval'))}) - -def cExec(context:EventContext, data:InputMessageData) -> None: - if len(data.Tokens) >= 2 and data.Tokens[1].lower() in ExecAllowed: - cmd = data.Tokens[1].lower() - Out = subprocess.run(('sh', '-c', f'export PATH=$PATH:/usr/games; {cmd}'), - stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode() - # - Caption = (re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])').sub('', Out)) - SendMessage(context, { - "TextPlain": Caption, - "TextMarkdown": MarkdownCode(Caption, True), - }) - else: - SendMessage(context, {"Text": choice(Locale.__('eval'))}) - -RegisterModule(name="Misc", endpoints={ - "Percenter": CreateEndpoint(["wish", "level"], summary="Provides fun trough percentage-based toys.", handler=mPercenter), - "Multifun": CreateEndpoint(["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], summary="Provides fun trough preprogrammed-text-based toys.", handler=mMultifun), - "Start": CreateEndpoint(["start"], summary="Salutes the user, hinting that the bot is working and providing basic quick help.", handler=cStart), - "Source": CreateEndpoint(["source"], summary="Provides a copy of the bot source codes and/or instructions on how to get it.", handler=cSource), - "GDPR": CreateEndpoint(["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr), - "Ping": CreateEndpoint(["ping"], summary="Responds pong, useful for testing messaging latency.", handler=cPing), - "Echo": CreateEndpoint(["echo"], summary="Responds back with the original text of the received message.", handler=cEcho), - "Eval": CreateEndpoint(["eval"], summary="Execute a Python command (or safe literal operation) in the current context. Currently not implemented.", handler=cEval), - "Exec": CreateEndpoint(["exec"], summary="Execute a system command from the allowed ones and return stdout+stderr.", handler=cExec), - #"Format": CreateEndpoint(["format"], summary="Reformat text using an handful of rules. Not yet implemented.", handler=cFormat), - #"Frame": CreateEndpoint(["frame"], summary="Frame someone's message into a platform-styled image. Not yet implemented.", handler=cFrame), - #"Repeat": CreateEndpoint(["repeat"], summary="I had this planned but I don't remember what this should have done. Not yet implemented.", handler=cRepeat), -}) - diff --git a/ModWinDog/Multifun/Multifun.py b/ModWinDog/Multifun/Multifun.py new file mode 100644 index 0000000..476611b --- /dev/null +++ b/ModWinDog/Multifun/Multifun.py @@ -0,0 +1,28 @@ +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # + +def mMultifun(context:EventContext, data:InputMessageData) -> None: + cmdkey = data.Name + replyToId = None + if data.Quoted: + replyFromUid = data.Quoted.User.Id + # TODO work on all platforms for the bot id + if replyFromUid.split(':')[1] == TelegramToken.split(':')[0] and 'bot' in Locale.__(cmdkey): + Text = choice(Locale.__(f'{cmdkey}.bot')) + elif replyFromUid == data.User.Id and 'self' in Locale.__(cmdkey): + Text = choice(Locale.__(f'{cmdkey}.self')).format(data.User.Name) + else: + if 'others' in Locale.__(cmdkey): + Text = choice(Locale.__(f'{cmdkey}.others')).format(data.User.Name, data.Quoted.User.Name) + replyToId = data.Quoted.messageId + else: + if 'empty' in Locale.__(cmdkey): + Text = choice(Locale.__(f'{cmdkey}.empty')) + SendMessage(context, {"Text": Text, "ReplyTo": replyToId}) + +RegisterModule(name="Multifun", endpoints=[ + SafeNamespace(names=["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], summary="Provides fun trough preprogrammed-text-based toys.", handler=mMultifun), +]) + diff --git a/ModWinDog/Percenter/Percenter.py b/ModWinDog/Percenter/Percenter.py new file mode 100644 index 0000000..187af46 --- /dev/null +++ b/ModWinDog/Percenter/Percenter.py @@ -0,0 +1,13 @@ +# ==================================== # +# WinDog multi-purpose chatbot # +# Licensed under AGPLv3 by OctoSpacc # +# ==================================== # + +def mPercenter(context:EventContext, data:InputMessageData) -> None: + SendMessage(context, {"Text": choice(Locale.__(f'{data.Name}.{"done" if data.Body else "empty"}')).format( + Cmd=data.Tokens[0], Percent=RandPercent(), Thing=data.Body)}) + +RegisterModule(name="Percenter", endpoints=[ + SafeNamespace(names=["wish", "level"], summary="Provides fun trough percentage-based toys.", handler=mPercenter), +]) + diff --git a/ModWinDog/Scrapers/Scrapers.py b/ModWinDog/Scrapers/Scrapers.py index 672d25d..734340d 100755 --- a/ModWinDog/Scrapers/Scrapers.py +++ b/ModWinDog/Scrapers/Scrapers.py @@ -65,7 +65,7 @@ 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": "Unsafe image content detected: This result {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) + 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) except: # no error is present, so we just have to wait more for the images continue @@ -121,8 +121,8 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None: SendMessage(context, {"TextPlain": "An unexpected error occurred."}) closeSelenium(driver_index, driver) -RegisterModule(name="Scrapers", endpoints={ - "DALL-E": CreateEndpoint(["dalle"], summary="Sends an AI-generated picture from DALL-E 3 via Microsoft Bing.", handler=cDalleSelenium), - "Craiyon": CreateEndpoint(["craiyon"], summary="Sends an AI-generated picture from Craiyon.com.", handler=cCraiyonSelenium), -}) +RegisterModule(name="Scrapers", endpoints=[ + SafeNamespace(names=["dalle"], summary="Sends an AI-generated picture from DALL-E 3 via Microsoft Bing.", handler=cDalleSelenium), + SafeNamespace(names=["craiyon"], summary="Sends an AI-generated picture from Craiyon.com.", handler=cCraiyonSelenium), +]) diff --git a/ModWinDog/Scripting/Scripting.py b/ModWinDog/Scripting/Scripting.py index 91671c8..e24ec99 100755 --- a/ModWinDog/Scripting/Scripting.py +++ b/ModWinDog/Scripting/Scripting.py @@ -52,7 +52,7 @@ end)()""")) Log(textOutput := ("Lua Error: " + str(error))) SendMessage(context, {"TextPlain": textOutput}) -RegisterModule(name="Scripting", group="Geek", summary="Tools for programming the bot and expanding its features.", endpoints={ - "Lua": CreateEndpoint(["lua"], summary="Execute a Lua snippet and get its output.", handler=cLua), -}) +RegisterModule(name="Scripting", group="Geek", summary="Tools for programming the bot and expanding its features.", endpoints=[ + SafeNamespace(names=["lua"], summary="Execute a Lua snippet and get its output.", handler=cLua), +]) diff --git a/WinDog.py b/WinDog.py index 2f125e6..d35396c 100755 --- a/WinDog.py +++ b/WinDog.py @@ -7,12 +7,14 @@ import json, time from binascii import hexlify from glob import glob -from magic import Magic +from hashlib import new as hashlib_new from os import listdir from os.path import isfile, isdir from random import choice, randint from types import SimpleNamespace from traceback import format_exc +from yaml import load as yaml_load, BaseLoader as yaml_BaseLoader +#from xml.etree import ElementTree from bs4 import BeautifulSoup from html import unescape as HtmlUnescape from markdown import markdown @@ -38,6 +40,13 @@ class InputMessageData(SafeNamespace): class OutputMessageData(SafeNamespace): pass +def NamespaceUnion(namespaces:list|tuple, clazz=SimpleNamespace): + dikt = {} + for namespace in namespaces: + for key, value in tuple(namespace.__dict__.items()): + dikt[key] = value + return 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): @@ -90,11 +99,39 @@ def InDict(dikt:dict, key:str, /) -> any: def DictGet(dikt:dict, key:str, /) -> any: return (dikt[key] if key in dikt else None) +def ObjGet(node:object, query:str, /) -> any: + for key in query.split('.'): + if hasattr(node, "__getitem__") and node.__getitem__: + # dicts and such + method = "__getitem__" + exception = KeyError + else: + # namespaces and such + method = "__getattribute__" + exception = AttributeError + try: + node = node.__getattribute__(method)(key) + except exception: + return None + #try: + # node = node[key] + #except TypeError: + # node = node.__getattribute__(key) + #except (TypeError, KeyError, AttributeError): + # return None + return node + def isinstanceSafe(clazz:any, instance:any) -> bool: if instance != None: return isinstance(clazz, instance) return False +def GetString(bank:dict, query:str|dict, lang:str=None): + if not (result := ObjGet(bank, f"{query}.{lang or DefaultLang}")): + if not (result := ObjGet(bank, f"{query}.en")): + result = tuple(ObjGet(bank, query).values())[0] + return result + def CharEscape(String:str, Escape:str='') -> str: if Escape == 'MARKDOWN': return escape_markdown(String, version=2) @@ -159,22 +196,28 @@ def RandHexStr(length:int) -> str: hexa += choice('0123456789abcdef') return hexa +def GetUserData(user_id:str) -> SafeNamespace|None: + try: + return User.get(User.id == user_id) + except User.DoesNotExist: + return None + def ParseCommand(text:str) -> SafeNamespace|None: - command = SafeNamespace() if not text: - return command + return None text = text.strip() try: # ensure text is a non-empty command if not (text[0] in CmdPrefixes and text[1:].strip()): - return command + return None except IndexError: - return + return None + command = SafeNamespace() command.tokens = text.replace("\r", " ").replace("\n", " ").replace("\t", " ").replace(" ", " ").replace(" ", " ").split(" ") command.name = command.tokens[0][1:].lower() command.body = text[len(command.tokens[0]):].strip() if command.name not in Endpoints: return command - if (endpoint_arguments := Endpoints[command.name]["arguments"]): + if (endpoint_arguments := Endpoints[command.name].arguments):#["arguments"]): command.arguments = {} index = 1 for key in endpoint_arguments: @@ -190,20 +233,31 @@ def ParseCommand(text:str) -> SafeNamespace|None: return command def OnMessageParsed(data:InputMessageData) -> None: - if Debug and (DumpToFile or DumpToConsole): - text = (data.text_auto.replace('\n', '\\n') if data.text_auto else '') - text = f"[{int(time.time())}] [{time.ctime()}] [{data.room.id}] [{data.message_id}] [{data.user.id}] {text}" - if DumpToConsole: - print(text) - if DumpToFile: - open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n') + DumpMessage(data) + UpdateUserDb(data.user) -def SendMessage(context, data:OutputMessageData, destination=None) -> None: - if type(context) == dict: - event = context['Event'] if 'Event' in context else None - manager = context['Manager'] if 'Manager' in context else None - else: - [event, manager] = [context, context] +def UpdateUserDb(user:SafeNamespace) -> None: + try: + User.get(User.id == user.id) + except User.DoesNotExist: + user_hash = ("sha256:" + hashlib_new("sha256", user.id.encode()).hexdigest()) + try: + User.get(User.id_hash == user_hash) + User.update(id=user.id).where(User.id_hash == user_hash) + except User.DoesNotExist: + User.create(id=user.id, id_hash=user_hash) + +def DumpMessage(data:InputMessageData) -> None: + if not (Debug and (DumpToFile or DumpToConsole)): + return + text = (data.text_auto.replace('\n', '\\n') if data.text_auto else '') + text = f"[{int(time.time())}] [{time.ctime()}] [{data.room.id}] [{data.message_id}] [{data.user.id}] {text}" + if DumpToConsole: + print(text, data) + if DumpToFile: + open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n') + +def SendMessage(context:EventContext, data:OutputMessageData, destination=None) -> None: if InDict(data, 'TextPlain') or InDict(data, 'TextMarkdown'): textPlain = InDict(data, 'TextPlain') textMarkdown = InDict(data, 'TextMarkdown') @@ -215,27 +269,62 @@ def SendMessage(context, data:OutputMessageData, destination=None) -> None: textMarkdown = CharEscape(HtmlUnescape(data['Text']), InferMdEscape(HtmlUnescape(data['Text']), textPlain)) for platform in Platforms: platform = Platforms[platform] - if isinstanceSafe(event, platform.eventClass) or isinstanceSafe(manager, platform.managerClass): - platform.sender(event, manager, data, destination, textPlain, textMarkdown) + if isinstanceSafe(context.event, platform.eventClass) or isinstanceSafe(context.manager, platform.managerClass): + return platform.sender(context, data, destination, textPlain, textMarkdown) -def SendNotice(context, data) -> None: +def SendNotice(context:EventContext, data) -> None: pass +def DeleteMessage(context:EventContext, data) -> None: + pass + +#def ParseModuleTree(module:ElementTree.Element): +# def parseTexts(tree:ElementTree.Element): +# texts = {} +# for text in tree: +# texts[text.tag] = text.text +# return texts +# if module.tag != "module": +# raise Exception(f"Unknown root element <{module.tag}> in {FILE}; it should be .") +# for option in module: +# match option.tag: +# case _: +# parseTexts(option) +# case "endpoints": +# for endpoint in option: +# for option in endpoint: +# match option.tag: +# case _: +# parseTexts(option) +# case "arguments": +# for argument in option: +# parseTexts(argument) + def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, eventClass=None, managerClass=None) -> None: Platforms[name] = SafeNamespace(main=main, sender=sender, linker=linker, eventClass=eventClass, managerClass=managerClass) Log(f"{name}, ", inline=True) def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None: - Modules[name] = {"group": group, "summary": summary, "endpoints": endpoints} + module = SafeNamespace(group=group, endpoints=(SafeNamespace(**endpoints) if type(endpoints) == dict else endpoints)) + # TODO load XML data, add to both module and locale objects + #if isfile(file := f"./ModWinDog/{name}/{name}.xml"): + # ParseModuleTree(ElementTree.parse(file).getroot()) + if isfile(file := f"./ModWinDog/{name}/{name}.yaml"): + module.strings = yaml_load(open(file, 'r').read().replace("\t", " "), Loader=yaml_BaseLoader) + module.get_string = (lambda query, lang=None: GetString(module.strings, query, lang)) + Modules[name] = module Log(f"{name}, ", inline=True) for endpoint in endpoints: - endpoint = endpoints[endpoint] - for name in endpoint["names"]: + endpoint.module = module + endpoint.get_string = (lambda query, lang=None: module.get_string(f"endpoints.{endpoint.names[0]}.{query}", lang)) + for name in endpoint.names: Endpoints[name] = endpoint -# TODO register endpoint with this instead of RegisterModule -def CreateEndpoint(names:list[str], handler:callable, arguments:dict[str, bool]|None=None, *, summary:str|None=None) -> dict: - return {"names": names, "summary": summary, "handler": handler, "arguments": arguments} +def CallEndpoint(name:str, context:EventContext, data:InputMessageData): + endpoint = Endpoints[name] + context.endpoint = endpoint + context.module = endpoint.module + return endpoint.handler(context, data) def WriteNewConfig() -> None: Log("💾️ No configuration found! Generating and writing to `./Config.py`... ", inline=True) @@ -296,7 +385,6 @@ if __name__ == '__main__': Log("...Done. ✅️", inline=True, newline=True) Log("💽️ Loading Configuration... ", newline=False) - #exec(open("./LibWinDog/Config.py", 'r').read()) from Config import * if isfile("./Config.py"): from Config import * diff --git a/requirements.txt b/requirements.txt index 338670a..1e72581 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ beautifulsoup4 Markdown peewee +PyYAML