More work on Matrix, move commands to new HTML locales, fix Mastodon

This commit is contained in:
2024-06-28 01:57:42 +02:00
parent 2c73846554
commit 4afb5f3275
28 changed files with 307 additions and 271 deletions

0
Assets/ImageCreator-CodeOfConduct.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 839 KiB

After

Width:  |  Height:  |  Size: 839 KiB

View File

@ -22,29 +22,42 @@ def MastodonMain() -> bool:
Mastodon = mastodon.Mastodon(api_base_url=MastodonUrl, access_token=MastodonToken) Mastodon = mastodon.Mastodon(api_base_url=MastodonUrl, access_token=MastodonToken)
class MastodonListener(mastodon.StreamListener): class MastodonListener(mastodon.StreamListener):
def on_notification(self, event): def on_notification(self, event):
MastodonHandler(event) MastodonHandler(event, Mastodon)
Mastodon.stream_user(MastodonListener(), run_async=True) Mastodon.stream_user(MastodonListener(), run_async=True)
return True return True
def MastodonHandler(event): def MastodonMakeInputMessageData(status:dict) -> InputMessageData:
if event['type'] == 'mention': data = InputMessageData(
#OnMessageParsed() message_id = ("mastodon:" + strip_url_scheme(status["uri"])),
message = BeautifulSoup(event['status']['content'], 'html.parser').get_text(' ').strip().replace('\t', ' ') text_html = status["content"],
if not message.split('@')[0]: )
message = ' '.join('@'.join(message.split('@')[1:]).strip().split(' ')[1:]).strip() data.text_plain = BeautifulSoup(data.text_html, "html.parser").get_text()
if message[0] in CmdPrefixes: data.text_auto = GetWeightedText(data.text_html, data.text_plain)
command = ParseCmd(message) command_tokens = data.text_plain.strip().replace("\t", " ").split(" ")
if command: while command_tokens[0].strip().startswith('@') or not command_tokens[0]:
command.messageId = event['status']['id'] command_tokens.pop(0)
if command.Name in Endpoints: data.command = ParseCommand(" ".join(command_tokens))
CallEndpoint(command.Name, EventContext(platform="mastodon", event=event, manager=Mastodon), command) data.user = SafeNamespace(
id = ("mastodon:" + strip_url_scheme(status["account"]["uri"])),
name = status["account"]["display_name"],
)
data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace())
return data
def MastodonHandler(event, Mastodon):
if event["type"] == "mention":
data = MastodonMakeInputMessageData(event["status"])
OnMessageParsed(data)
if (command := ObjGet(data, "command.name")):
CallEndpoint(command, EventContext(platform="mastodon", event=event, manager=Mastodon), data)
def MastodonSender(context:EventContext, data:OutputMessageData, destination) -> None: def MastodonSender(context:EventContext, data:OutputMessageData, destination) -> None:
media_results = None media_results = None
if data.media: if data.media:
media_results = [] media_results = []
for medium in data.media[:4]: # Mastodon limits posts to 4 attachments # TODO support media by url (do we have to upload them or can just pass the original URL?)
medium_result = context.manager.media_post(medium, Magic(mime=True).from_buffer(medium)) for medium in data.media[:4]: # Mastodon limits posts to 4 attachments, so we drop any more
medium_result = context.manager.media_post(medium["bytes"], Magic(mime=True).from_buffer(medium["bytes"]))
while medium_result["url"] == "null": while medium_result["url"] == "null":
medium_result = context.manager.media(medium_result) medium_result = context.manager.media(medium_result)
media_results.append(medium_result) media_results.append(medium_result)

View File

@ -7,30 +7,64 @@
# MatrixUrl = "https://matrix.example.com" # MatrixUrl = "https://matrix.example.com"
# MatrixUsername = "username" # MatrixUsername = "username"
# Provide either your password, or an active access_token below.
# MatrixPassword = "hunter2" # MatrixPassword = "hunter2"
# If logging in via password, a token will be automatically generated and saved to Config.
# MatrixToken = ""
# end windog config # """ # end windog config # """
MatrixUrl, MatrixUsername, MatrixPassword = None, None, None MatrixUrl, MatrixUsername, MatrixPassword, MatrixToken = None, None, None, None
from asyncio import run as asyncio_run
import nio import nio
import simplematrixbotlib as MatrixBotLib #from nio import AsyncClient, MatrixRoom, RoomMessageText
from threading import Thread #import simplematrixbotlib as MatrixBotLib
async def MatrixMessageHandler(room:nio.MatrixRoom, event:nio.RoomMessage) -> None:
data = MatrixMakeInputMessageData(room, event)
def MatrixMakeInputMessageData(room:nio.MatrixRoom, event:nio.RoomMessage) -> InputMessageData:
data = InputMessageData(
message_id = f"matrix:{event.event_id}",
room = SafeNamespace(
id = f"matrix:{room.room_id}",
name = room.display_name,
),
user = SafeNamespace(
id = f"matrix:{event.sender}",
)
)
print(data)
return data
def MatrixMain() -> bool: def MatrixMain() -> bool:
if not (MatrixUrl and MatrixUsername and MatrixPassword): if not (MatrixUrl and MatrixUsername and (MatrixPassword or MatrixToken)):
return False return False
MatrixBot = MatrixBotLib.Bot(MatrixBotLib.Creds(MatrixUrl, MatrixUsername, MatrixPassword)) #MatrixBot = MatrixBotLib.Bot(MatrixBotLib.Creds(MatrixUrl, MatrixUsername, MatrixPassword))
@MatrixBot.listener.on_message_event ##@MatrixBot.listener.on_message_event
@MatrixBot.listener.on_custom_event(nio.events.room_events.RoomMessageFile) #@MatrixBot.listener.on_custom_event(nio.RoomMessageText)
async def MatrixMessageListener(room, message) -> None: #async def MatrixMessageListener(room, message, event) -> None:
pass # print(message)
#print(message) # #match = MatrixBotLib.MessageMatch(room, message, MatrixBot)
#match = MatrixBotLib.MessageMatch(room, message, MatrixBot) # #OnMessageParsed()
#OnMessageParsed() # #if match.is_not_from_this_bot() and match.command("windogtest"):
#if match.is_not_from_this_bot() and match.command("windogtest"): # # pass #await MatrixBot.api.send_text_message(room.room_id, " ".join(arg for arg in match.args()))
# pass #await MatrixBot.api.send_text_message(room.room_id, " ".join(arg for arg in match.args())) #@MatrixBot.listener.on_custom_event(nio.RoomMessageFile)
Thread(target=lambda:MatrixBot.run()).start() #async def MatrixMessageFileListener(room, event):
# print(event)
#Thread(target=lambda:MatrixBot.run()).start()
async def client() -> None:
client = nio.AsyncClient(MatrixUrl, MatrixUsername)
login = await client.login(password=MatrixPassword, token=MatrixToken)
if MatrixPassword and (not MatrixToken) and (token := ObjGet(login, "access_token")):
open("./Config.py", 'a').write(f'\n# Added automatically #\nMatrixToken = "{token}"\n')
await client.sync(30000) # resync old messages first to "skip read ones"
client.add_event_callback(MatrixMessageHandler, nio.RoomMessage)
await client.sync_forever(timeout=30000)
Thread(target=lambda:asyncio_run(client())).start()
return True return True
def MatrixSender() -> None: def MatrixSender() -> None:

View File

@ -1 +1,2 @@
matrix-nio
simplematrixbotlib simplematrixbotlib

View File

@ -23,7 +23,7 @@ def TelegramMain() -> bool:
return False return False
updater = telegram.ext.Updater(TelegramToken) updater = telegram.ext.Updater(TelegramToken)
dispatcher = updater.dispatcher dispatcher = updater.dispatcher
dispatcher.add_handler(MessageHandler(Filters.text | Filters.command, TelegramHandlerWrapper)) dispatcher.add_handler(MessageHandler(Filters.text | Filters.command, TelegramHandler))
updater.start_polling() updater.start_polling()
#app = Application.builder().token(TelegramToken).build() #app = Application.builder().token(TelegramToken).build()
#app.add_handler(MessageHandler(filters.TEXT | filters.COMMAND, TelegramHandler)) #app.add_handler(MessageHandler(filters.TEXT | filters.COMMAND, TelegramHandler))
@ -40,60 +40,33 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
) )
data.text_auto = GetWeightedText(data.text_markdown, data.text_plain) data.text_auto = GetWeightedText(data.text_markdown, data.text_plain)
data.command = ParseCommand(data.text_plain) data.command = ParseCommand(data.text_plain)
data.room = SafeNamespace(
id = f"telegram:{message.chat.id}",
tag = message.chat.username,
name = (message.chat.title or message.chat.first_name),
)
data.user = SafeNamespace( data.user = SafeNamespace(
id = f"telegram:{message.from_user.id}", id = f"telegram:{message.from_user.id}",
tag = message.from_user.username, tag = message.from_user.username,
name = message.from_user.first_name, name = message.from_user.first_name,
) )
data.user.settings = GetUserSettings(data.user.id) data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace())
data.room = SafeNamespace(
id = f"telegram:{message.chat.id}",
tag = message.chat.username,
name = (message.chat.title or message.chat.first_name),
)
linked = TelegramLinker(data)
data.message_url = linked.message
data.room.url = linked.room
return data return data
def TelegramHandlerWrapper(update:telegram.Update, context:CallbackContext=None) -> None: def TelegramHandler(update:telegram.Update, context:CallbackContext=None) -> None:
Thread(target=lambda:TelegramHandlerCore(update, context)).start() def handler() -> None:
if not update.message:
def TelegramHandlerCore(update:telegram.Update, context:CallbackContext=None) -> None: return
if not update.message: data = TelegramMakeInputMessageData(update.message)
return if (quoted := update.message.reply_to_message):
data = TelegramMakeInputMessageData(update.message) data.quoted = TelegramMakeInputMessageData(quoted)
if update.message.reply_to_message: OnMessageParsed(data)
data.quoted = TelegramMakeInputMessageData(update.message.reply_to_message) if (command := ObjGet(data, "command.name")):
OnMessageParsed(data) CallEndpoint(command, EventContext(platform="telegram", event=update, manager=context), data)
cmd = ParseCmd(update.message.text) Thread(target=handler).start()
if cmd:
# TODO remove old cmd and just pass the data object
cmd.command = data.command
cmd.quoted = data.quoted
cmd.user = data.user
cmd.message_id = data.message_id
cmd.messageId = update.message.message_id
cmd.TextPlain = cmd.Body
cmd.TextMarkdown = update.message.text_markdown_v2
cmd.Text = GetWeightedText(cmd.TextMarkdown, cmd.TextPlain)
if cmd.Tokens[0][0] in CmdPrefixes and cmd.Name in Endpoints:
cmd.User = SimpleNamespace(**{
"Name": update.message.from_user.first_name,
"Tag": update.message.from_user.username,
"Id": f'telegram:{update.message.from_user.id}',
})
if update.message.reply_to_message:
cmd.Quoted = SimpleNamespace(**{
"messageId": update.message.reply_to_message.message_id,
"Body": update.message.reply_to_message.text,
"TextPlain": update.message.reply_to_message.text,
"TextMarkdown": update.message.reply_to_message.text_markdown_v2,
"Text": GetWeightedText(update.message.reply_to_message.text_markdown_v2, update.message.reply_to_message.text),
"User": SimpleNamespace(**{
"Name": update.message.reply_to_message.from_user.first_name,
"Tag": update.message.reply_to_message.from_user.username,
"Id": f'telegram:{update.message.reply_to_message.from_user.id}',
}),
})
CallEndpoint(cmd.Name, EventContext(platform="telegram", event=update, manager=context), cmd)
def TelegramSender(context:EventContext, data:OutputMessageData, destination): def TelegramSender(context:EventContext, data:OutputMessageData, destination):
result = None result = None
@ -104,7 +77,7 @@ def TelegramSender(context:EventContext, data:OutputMessageData, destination):
if data.media: if data.media:
for medium in data.media: for medium in data.media:
result = context.event.message.reply_photo( result = context.event.message.reply_photo(
(DictGet(medium, "bytes") or DictGet(medium, "url")), (ObjGet(medium, "bytes") or ObjGet(medium, "url")),
caption=(data.text_html or data.text_markdown or data.text_plain), 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), parse_mode=("HTML" if data.text_html else "MarkdownV2" if data.text_markdown else None),
reply_to_message_id=replyToId) reply_to_message_id=replyToId)
@ -116,14 +89,17 @@ def TelegramSender(context:EventContext, data:OutputMessageData, destination):
result = context.event.message.reply_text(data.text_plain, reply_to_message_id=replyToId) result = context.event.message.reply_text(data.text_plain, reply_to_message_id=replyToId)
return TelegramMakeInputMessageData(result) return TelegramMakeInputMessageData(result)
# TODO support usernames
def TelegramLinker(data:InputMessageData) -> SafeNamespace: def TelegramLinker(data:InputMessageData) -> SafeNamespace:
linked = SafeNamespace() linked = SafeNamespace()
if data.room.id: if data.room.id:
room_id = data.room.id.split("telegram:")[1] # prefix must be dropped for groups and channels, while direct chats apparently can never be linked
linked.room = f"https://t.me/{room_id}" if (room_id := "100".join(data.room.id.split("telegram:")[1].split("100")[1:])):
if data.message_id: # apparently Telegram doesn't really support links to rooms by id without a message id, so we just use a null one
message_id = data.message_id.split("telegram:")[1] linked.room = f"https://t.me/c/{room_id}/0"
linked.message = f"{linked.room}/{message_id}" if data.message_id:
message_id = data.message_id.split("telegram:")[1]
linked.message = f"https://t.me/c/{room_id}/{message_id}"
return linked return linked
RegisterPlatform(name="Telegram", main=TelegramMain, sender=TelegramSender, linker=TelegramLinker, eventClass=telegram.Update) RegisterPlatform(name="Telegram", main=TelegramMain, sender=TelegramSender, linker=TelegramLinker, eventClass=telegram.Update)

0
LibWinDog/Types.py Normal file → Executable file
View File

View File

@ -1,15 +1,7 @@
{ {
"start": [
"*Hi* {0}*!*\n\nUse /help to read a list of available commands."
],
"help": [ "help": [
"*There's no one around to help (yet).*" "*There's no one around to help (yet).*"
], ],
"echo": {
"empty": [
"*Echo what? Give me something to repeat.*"
]
},
"wish": { "wish": {
"empty": [ "empty": [
"*You wished for nothing! ✨*\n\n_Nothing happens..._" "*You wished for nothing! ✨*\n\n_Nothing happens..._"

View File

@ -1,15 +1,7 @@
{ {
"start": [
"*Ciao* {0}*!*\n\nUsa /help per leggere la lista dei comandi."
],
"help": [ "help": [
"*Non c'è nessuno qui ad aiutarti (per ora).*" "*Non c'è nessuno qui ad aiutarti (per ora).*"
], ],
"echo": {
"empty": [
"*Echo cosa? Dimmi qualcosa da ripetere.*"
]
},
"wish": { "wish": {
"empty": [ "empty": [
"*Non hai desiderato nulla! ✨*\n\n_Non succede niente..._" "*Non hai desiderato nulla! ✨*\n\n_Non succede niente..._"

View File

@ -3,11 +3,6 @@
# Licensed under AGPLv3 by OctoSpacc # # 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: def cSource(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, {"TextPlain": ("""\ SendMessage(context, {"TextPlain": ("""\
* Original Code: {https://gitlab.com/octospacc/WinDog} * Original Code: {https://gitlab.com/octospacc/WinDog}
@ -20,8 +15,12 @@ def cGdpr(context:EventContext, data:InputMessageData) -> None:
def cConfig(context:EventContext, data:InputMessageData) -> None: def cConfig(context:EventContext, data:InputMessageData) -> None:
if not (settings := GetUserSettings(data.user.id)): if not (settings := GetUserSettings(data.user.id)):
User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute() User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute()
if (get := ObjGet(data, "command.arguments.get")): if (to_set := ObjGet(data, "command.arguments.set")):
SendMessage(context, OutputMessageData(text_plain=str(ObjGet(data.user.settings, get)))) 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))))
# TODO show general help when no useful parameters are passed
#Cmd = TelegramHandleCmd(update) #Cmd = TelegramHandleCmd(update)
#if not Cmd: return #if not Cmd: return
# ... area: eu, us, ... # ... area: eu, us, ...
@ -39,30 +38,14 @@ def cPing(context:EventContext, data:InputMessageData) -> None:
def cEval(context:EventContext, data:InputMessageData) -> None: def cEval(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, {"Text": choice(Locale.__('eval'))}) SendMessage(context, {"Text": choice(Locale.__('eval'))})
def cExec(context:EventContext, data:InputMessageData) -> None: RegisterModule(name="Base", endpoints=[
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()
# <https://stackoverflow.com/a/14693789>
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=["source"], summary="Provides a copy of the bot source codes and/or instructions on how to get it.", handler=cSource),
SafeNamespace(names=["config"], handler=cConfig, arguments={ SafeNamespace(names=["config"], handler=cConfig, arguments={
"get": True, "get": True,
}), }),
#SafeNamespace(names=["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr), #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=["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=["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=["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=["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), #SafeNamespace(names=["repeat"], summary="I had this planned but I don't remember what this should have done. Not yet implemented.", handler=cRepeat),

0
ModWinDog/Dumper/Dumper.yaml Normal file → Executable file
View File

30
ModWinDog/Echo/Echo.py Normal file → Executable file
View File

@ -4,22 +4,20 @@
# ==================================== # # ==================================== #
def cEcho(context:EventContext, data:InputMessageData) -> None: def cEcho(context:EventContext, data:InputMessageData) -> None:
text = ObjGet(data, "command.body") if not (text := ObjGet(data, "command.body")):
if text: return SendMessage(context, OutputMessageData(text_html=context.endpoint.get_string("empty", data.user.settings.language)))
prefix = "🗣️ " prefix = f'<a href="{data.message_url}">🗣️</a> '
#prefix = f"[🗣️]({context.linker(data).message}) " #prefix = f"[🗣️]({context.linker(data).message}) "
if len(data.Tokens) == 2: if len(data.command.tokens) == 2:
nonascii = True nonascii = True
for char in data.Tokens[1]: for char in data.command.tokens[1]:
if ord(char) < 256: if ord(char) < 256:
nonascii = False nonascii = False
break break
if nonascii: if nonascii:
# text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis) # text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis)
prefix = '' prefix = ''
SendMessage(context, OutputMessageData(text=(prefix + text))) SendMessage(context, OutputMessageData(text_html=(prefix + html_escape(text))))
else:
SendMessage(context, OutputMessageData(text_html=context.endpoint.get_string('empty')))
RegisterModule(name="Echo", endpoints=[ RegisterModule(name="Echo", endpoints=[
SafeNamespace(names=["echo"], handler=cEcho), SafeNamespace(names=["echo"], handler=cEcho),

2
ModWinDog/Echo/Echo.yaml Normal file → Executable file
View File

@ -1,3 +1,5 @@
summary:
en: Tools for repeating messages.
endpoints: endpoints:
echo: echo:
summary: summary:

4
ModWinDog/GPT/GPT.py Normal file → Executable file
View File

@ -8,12 +8,12 @@ from g4f.client import Client as G4FClient
g4fClient = G4FClient() g4fClient = G4FClient()
def cGpt(context:EventContext, data:InputMessageData) -> None: def cGpt(context:EventContext, data:InputMessageData) -> None:
if not data.command.body: if not (prompt := data.command.body):
return SendMessage(context, {"Text": "You must type some text."}) return SendMessage(context, {"Text": "You must type some text."})
output = None output = None
while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix for a strange ratelimit message while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix for a strange ratelimit message
output = "" output = ""
for completion in g4fClient.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": data.command.body}], stream=True): 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 "") output += (completion.choices[0].delta.content or "")
return SendMessage(context, {"TextPlain": f"[🤖️ GPT]\n\n{output}"}) return SendMessage(context, {"TextPlain": f"[🤖️ GPT]\n\n{output}"})

0
ModWinDog/GPT/requirements.txt Normal file → Executable file
View File

View File

@ -11,13 +11,10 @@ def cHash(context:EventContext, data:InputMessageData):
if not (text_input and (algorithm in hashlib.algorithms_available)): 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)}) return SendMessage(context, {"Text": choice(Locale.__('hash.usage')).format(data.command.tokens[0], hashlib.algorithms_available)})
hashed = hashlib.new(algorithm, text_input.encode()).hexdigest() hashed = hashlib.new(algorithm, text_input.encode()).hexdigest()
return SendMessage(context, { return SendMessage(context, OutputMessageData(text_plain=hashed, text_html=f"<pre>{hashed}</pre>"))
"TextPlain": hashed,
"TextMarkdown": MarkdownCode(hashed, True),
})
RegisterModule(name="Hashing", group="Geek", summary="Functions for hashing of textual content.", endpoints=[ RegisterModule(name="Hashing", group="Geek", endpoints=[
SafeNamespace(names=["hash"], summary="Responds with the hash-sum of a message received.", handler=cHash, arguments={ SafeNamespace(names=["hash"], handler=cHash, arguments={
"algorithm": True, "algorithm": True,
}), }),
]) ])

21
ModWinDog/Hashing/Hashing.yaml Normal file → Executable file
View File

@ -1,14 +1,15 @@
summary: summary:
en: Functions for calculating hashes of content. en: Functions for calculating hashes of content.
it: Funzioni per calcolare hash di contenuti. it: Funzioni per calcolare hash di contenuti.
hash: endpoints:
summary: hash:
en: Responds with the hash-sum of the received message. summary:
arguments: en: Responds with the hash-sum of the received message.
algorithm: arguments:
en: Algorithm algorithm:
it: Algoritmo en: Algorithm
body: it: Algoritmo
en: Text to hash body:
it: Testo da hashare en: Text to hash
it: Testo da hashare

View File

@ -5,17 +5,18 @@
# TODO: implement /help <commandname> feature # TODO: implement /help <commandname> feature
def cHelp(context:EventContext, data:InputMessageData) -> None: def cHelp(context:EventContext, data:InputMessageData) -> None:
moduleList = '' module_list = ''
language = data.user.settings.language
for module in Modules: for module in Modules:
summary = Modules[module].summary summary = Modules[module].get_string("summary", language)#summary
endpoints = Modules[module].endpoints endpoints = Modules[module].endpoints
moduleList += (f"\n\n{module}" + (f": {summary}" if summary else '')) module_list += (f"\n\n{module}" + (f": {summary}" if summary else ''))
for endpoint in endpoints: for endpoint in endpoints:
summary = endpoint.summary summary = Modules[module].get_string(f"endpoints.{endpoint.names[0]}.summary", language)
moduleList += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else '')) module_list += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else ''))
SendMessage(context, {"Text": f"[ Available Modules ]{moduleList}"}) SendMessage(context, OutputMessageData(text=module_list))
RegisterModule(name="Help", group="Basic", endpoints=[ RegisterModule(name="Help", group="Basic", endpoints=[
SafeNamespace(names=["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp), SafeNamespace(names=["help"], handler=cHelp),
]) ])

5
ModWinDog/Help/Help.yaml Normal file
View File

@ -0,0 +1,5 @@
endpoints:
help:
summary:
en: Provides help for the bot. For now, it just lists the commands.

View File

@ -10,19 +10,18 @@ MicrosoftBingSettings = {}
""" # end windog config # """ """ # end windog config # """
from urlextract import URLExtract from urlextract import URLExtract
from urllib import parse as UrlParse
from urllib.request import urlopen, Request from urllib.request import urlopen, Request
def HttpReq(url:str, method:str|None=None, *, body:bytes=None, headers:dict[str, str]={"User-Agent": WebUserAgent}): 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)) return urlopen(Request(url, method=method, data=body, headers=headers))
def cEmbedded(context:EventContext, data:InputMessageData) -> None: def cEmbedded(context:EventContext, data:InputMessageData) -> None:
if len(data.Tokens) >= 2: if len(data.command.tokens) >= 2:
# Find links in command body # Find links in command body
Text = (data.TextMarkdown + ' ' + data.TextPlain) Text = (data.text_markdown + ' ' + data.text_plain)
elif data.Quoted and data.Quoted.Text: elif data.quoted and data.quoted.text_auto:
# Find links in quoted message # Find links in quoted message
Text = (data.Quoted.TextMarkdown + ' ' + data.Quoted.TextPlain) Text = (data.quoted.text_markdown + ' ' + data.quoted.text_plain)
else: else:
# TODO Error message # TODO Error message
return return
@ -41,38 +40,37 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None:
url = "https://hlb0.octt.eu.org/cors-main.php/https://" + url url = "https://hlb0.octt.eu.org/cors-main.php/https://" + url
proto = '' proto = ''
else: else:
if urlDomain == "instagram.com": if urlDomain in ("instagram.com", "www.instagram.com"):
urlDomain = "ddinstagram.com" urlDomain = "ddinstagram.com"
elif urlDomain in ("twitter.com", "x.com"): elif urlDomain in ("twitter.com", "x.com"):
urlDomain = "fxtwitter.com" urlDomain = "fxtwitter.com"
elif urlDomain == "vm.tiktok.com": elif urlDomain == "vm.tiktok.com":
urlDomain = "vm.vxtiktok.com" urlDomain = "vm.vxtiktok.com"
url = urlDomain + url[len(urlDomain):] url = (urlDomain + '/' + '/'.join(url.split('/')[1:]))
SendMessage(context, {"TextPlain": f"{{{proto}{url}}}"}) SendMessage(context, {"TextPlain": f"{{{proto}{url}}}"})
# else TODO error message? # else TODO error message?
def cWeb(context:EventContext, data:InputMessageData) -> None: def cWeb(context:EventContext, data:InputMessageData) -> None:
if data.Body: if not (query := data.command.body):
try: return # TODO show message
QueryUrl = UrlParse.quote(data.Body) try:
Req = HttpReq(f'https://html.duckduckgo.com/html?q={QueryUrl}') QueryUrl = urlparse.quote(query)
Caption = f'🦆🔎 "{data.Body}": https://duckduckgo.com/?q={QueryUrl}\n\n' Req = HttpReq(f'https://html.duckduckgo.com/html?q={QueryUrl}')
Index = 0 Caption = f'🦆🔎 "{query}": https://duckduckgo.com/?q={QueryUrl}\n\n'
for Line in Req.read().decode().replace('\t', ' ').splitlines(): Index = 0
if ' class="result__a" ' in Line and ' href="//duckduckgo.com/l/?uddg=' in Line: for Line in Req.read().decode().replace('\t', ' ').splitlines():
Index += 1 if ' class="result__a" ' in Line and ' href="//duckduckgo.com/l/?uddg=' in Line:
Link = UrlParse.unquote(Line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&amp;rut=')[0]) Index += 1
Title = Line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>') Link = urlparse.unquote(Line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&amp;rut=')[0])
if len(Title) > 1: Title = Line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>')
Title = HtmlUnescape(Title[1].strip()) if len(Title) > 1:
Caption += f'[{Index}] {Title} : {{{Link}}}\n\n' Title = html_unescape(Title[1].strip())
else: Caption += f'[{Index}] {Title} : {{{Link}}}\n\n'
continue else:
SendMessage(context, {"TextPlain": f'{Caption}...'}) continue
except Exception: SendMessage(context, {"TextPlain": f'{Caption}...'})
raise except Exception:
else: raise
pass
def cImages(context:EventContext, data:InputMessageData) -> None: def cImages(context:EventContext, data:InputMessageData) -> None:
pass pass
@ -82,57 +80,57 @@ def cNews(context:EventContext, data:InputMessageData) -> None:
def cTranslate(context:EventContext, data:InputMessageData) -> None: def cTranslate(context:EventContext, data:InputMessageData) -> None:
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.Body)) text_input = (data.command.body or (data.quoted and data.quoted.text_plain))
if not (text_input and language_to): if not (text_input and language_to):
return SendMessage(context, {"TextPlain": f"Usage: /translate <to language> <text>"}) return SendMessage(context, {"TextPlain": f"Usage: /translate <to language> <text>"})
try: try:
# TODO: Use many different public Lingva instances in rotation to avoid overloading a specific one # TODO: Use many different public Lingva instances in rotation to avoid overloading a specific one
result = json.loads(HttpReq(f'https://lingva.ml/api/v1/auto/{language_to}/{UrlParse.quote(text_input)}').read()) result = json.loads(HttpReq(f'https://lingva.ml/api/v1/auto/{language_to}/{urlparse.quote(text_input)}').read())
SendMessage(context, {"TextPlain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"}) SendMessage(context, {"TextPlain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"})
except Exception: except Exception:
raise raise
# unsplash source appears to be deprecated! <https://old.reddit.com/r/unsplash/comments/s13x4h/what_happened_to_sourceunsplashcom/l65epl8/> # unsplash source appears to be deprecated! <https://old.reddit.com/r/unsplash/comments/s13x4h/what_happened_to_sourceunsplashcom/l65epl8/>
def cUnsplash(context:EventContext, data:InputMessageData) -> None: #def cUnsplash(context:EventContext, data:InputMessageData) -> None:
try: # try:
Req = HttpReq(f'https://source.unsplash.com/random/?{UrlParse.quote(data.Body)}') # Req = HttpReq(f'https://source.unsplash.com/random/?{urlparse.quote(data.command.body)}')
ImgUrl = Req.geturl().split('?')[0] # ImgUrl = Req.geturl().split('?')[0]
SendMessage(context, { # SendMessage(context, {
"TextPlain": f'{{{ImgUrl}}}', # "TextPlain": f'{{{ImgUrl}}}',
"TextMarkdown": MarkdownCode(ImgUrl, True), # "TextMarkdown": MarkdownCode(ImgUrl, True),
"Media": Req.read(), # "Media": Req.read(),
}) # })
except Exception: # except Exception:
raise # raise
def cSafebooru(context:EventContext, data:InputMessageData) -> None: def cSafebooru(context:EventContext, data:InputMessageData) -> None:
ApiUrl = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags=' ApiUrl = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags='
try: try:
if data.Body: 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 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(data.Body)}').read().decode().split(' file_url="')[1:] ImgUrls = HttpReq(f'{ApiUrl}md5:{RandHexStr(3)}%20{urlparse.quote(query)}').read().decode().split(' file_url="')[1:]
if ImgUrls: if ImgUrls:
break break
if not ImgUrls: # literal search if not ImgUrls: # literal search
ImgUrls = HttpReq(f'{ApiUrl}{UrlParse.quote(data.Body)}').read().decode().split(' file_url="')[1:] ImgUrls = HttpReq(f'{ApiUrl}{urlparse.quote(query)}').read().decode().split(' file_url="')[1:]
if not ImgUrls: if not ImgUrls:
return SendMessage(context, {"Text": "Error: Could not get any result from Safebooru."}) return SendMessage(context, {"Text": "Error: Could not get any result from Safebooru."})
ImgXml = choice(ImgUrls) ImgXml = choice(ImgUrls)
ImgUrl = ImgXml.split('"')[0] img_url = ImgXml.split('"')[0]
ImgId = ImgXml.split(' id="')[1].split('"')[0] img_id = ImgXml.split(' id="')[1].split('"')[0]
else: else:
HtmlReq = HttpReq(HttpReq('https://safebooru.org/index.php?page=post&s=random').geturl()) HtmlReq = HttpReq(HttpReq('https://safebooru.org/index.php?page=post&s=random').geturl())
for Line in HtmlReq.read().decode().replace('\t', ' ').splitlines(): for Line in HtmlReq.read().decode().replace('\t', ' ').splitlines():
if '<img ' in Line and ' id="image" ' in Line and ' src="': if '<img ' in Line and ' id="image" ' in Line and ' src="':
ImgUrl = Line.split(' src="')[1].split('"')[0] img_url = Line.split(' src="')[1].split('"')[0]
ImgId = ImgUrl.split('?')[-1] img_id = img_url.split('?')[-1]
break break
if ImgUrl: if img_url:
SendMessage(context, { SendMessage(context, OutputMessageData(
"TextPlain": f'[{ImgId}]\n{{{ImgUrl}}}', text_plain=f"[{img_id}]\n{{{img_url}}}",
"TextMarkdown": (f'\\[`{ImgId}`\\]\n' + MarkdownCode(ImgUrl, True)), text_html=f"[<code>{img_id}</code>]\n<pre>{img_url}</pre>",
"media": {"url": ImgUrl}, #, "bytes": HttpReq(ImgUrl).read()}, media={"url": img_url}))
})
else: else:
pass pass
except Exception as error: except Exception as error:

14
ModWinDog/Multifun/Multifun.py Normal file → Executable file
View File

@ -4,19 +4,19 @@
# ==================================== # # ==================================== #
def mMultifun(context:EventContext, data:InputMessageData) -> None: def mMultifun(context:EventContext, data:InputMessageData) -> None:
cmdkey = data.Name cmdkey = data.command.name
replyToId = None replyToId = None
if data.Quoted: if data.quoted:
replyFromUid = data.Quoted.User.Id replyFromUid = data.quoted.user.id
# TODO work on all platforms for the bot id # TODO work on all platforms for the bot id
if replyFromUid.split(':')[1] == TelegramToken.split(':')[0] and 'bot' in Locale.__(cmdkey): if replyFromUid.split(':')[1] == TelegramToken.split(':')[0] and 'bot' in Locale.__(cmdkey):
Text = choice(Locale.__(f'{cmdkey}.bot')) Text = choice(Locale.__(f'{cmdkey}.bot'))
elif replyFromUid == data.User.Id and 'self' in Locale.__(cmdkey): elif replyFromUid == data.user.id and 'self' in Locale.__(cmdkey):
Text = choice(Locale.__(f'{cmdkey}.self')).format(data.User.Name) Text = choice(Locale.__(f'{cmdkey}.self')).format(data.user.name)
else: else:
if 'others' in Locale.__(cmdkey): if 'others' in Locale.__(cmdkey):
Text = choice(Locale.__(f'{cmdkey}.others')).format(data.User.Name, data.Quoted.User.Name) Text = choice(Locale.__(f'{cmdkey}.others')).format(data.user.name, data.quoted.user.name)
replyToId = data.Quoted.messageId replyToId = data.quoted.message_id
else: else:
if 'empty' in Locale.__(cmdkey): if 'empty' in Locale.__(cmdkey):
Text = choice(Locale.__(f'{cmdkey}.empty')) Text = choice(Locale.__(f'{cmdkey}.empty'))

4
ModWinDog/Percenter/Percenter.py Normal file → Executable file
View File

@ -4,8 +4,8 @@
# ==================================== # # ==================================== #
def mPercenter(context:EventContext, data:InputMessageData) -> None: def mPercenter(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, {"Text": choice(Locale.__(f'{data.Name}.{"done" if data.Body else "empty"}')).format( SendMessage(context, {"Text": choice(Locale.__(f'{data.command.name}.{"done" if data.command.body else "empty"}')).format(
Cmd=data.Tokens[0], Percent=RandPercent(), Thing=data.Body)}) Cmd=data.command.tokens[0], Percent=RandPercent(), Thing=data.command.body)})
RegisterModule(name="Percenter", endpoints=[ RegisterModule(name="Percenter", endpoints=[
SafeNamespace(names=["wish", "level"], summary="Provides fun trough percentage-based toys.", handler=mPercenter), SafeNamespace(names=["wish", "level"], summary="Provides fun trough percentage-based toys.", handler=mPercenter),

View File

@ -36,8 +36,7 @@ def closeSelenium(index:int, driver:Driver) -> None:
def cDalleSelenium(context:EventContext, data:InputMessageData) -> None: def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
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." 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."
prompt = data.command.body if not (prompt := data.command.body):
if not prompt:
return SendMessage(context, {"Text": "Please tell me what to generate."}) return SendMessage(context, {"Text": "Please tell me what to generate."})
driver_index, driver = None, None driver_index, driver = None, None
try: try:
@ -74,11 +73,10 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
img_url = img_url.get_attribute("src").split('?')[0] img_url = img_url.get_attribute("src").split('?')[0]
img_array.append({"url": img_url}) #, "bytes": HttpReq(img_url).read()}) img_array.append({"url": img_url}) #, "bytes": HttpReq(img_url).read()})
page_url = driver.current_url.split('?')[0] page_url = driver.current_url.split('?')[0]
SendMessage(context, { SendMessage(context, OutputMessageData(
"TextPlain": f'"{prompt}"\n{{{page_url}}}', text_plain=f'"{prompt}"\n{{{page_url}}}',
"TextMarkdown": (f'"_{CharEscape(prompt, "MARKDOWN")}_"\n' + MarkdownCode(page_url, True)), text_html=f'"<i>{html_escape(prompt)}</i>"\n<pre>{page_url}</pre>',
"media": img_array, media=img_array))
})
return closeSelenium(driver_index, driver) return closeSelenium(driver_index, driver)
raise Exception("VM timed out.") raise Exception("VM timed out.")
except Exception as error: except Exception as error:
@ -87,8 +85,7 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None: def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
prompt = data.command.body if not (prompt := data.command.body):
if not prompt:
return SendMessage(context, {"Text": "Please tell me what to generate."}) return SendMessage(context, {"Text": "Please tell me what to generate."})
driver_index, driver = None, None driver_index, driver = None, None
try: try:

View File

@ -25,7 +25,7 @@ def luaAttributeFilter(obj, attr_name, is_setting):
# TODO make print behave the same as normal Lua, and expose a function for printing without newlines # 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) -> None:
# TODO update quoted api getting # TODO update quoted api getting
scriptText = (data.command.body or (data.Quoted and data.Quoted.Body)) scriptText = (data.command.body or (data.quoted and data.quoted.text_plain))
if not scriptText: if not scriptText:
return SendMessage(context, {"Text": "You must provide some Lua code to execute."}) return SendMessage(context, {"Text": "You must provide some Lua code to execute."})
luaRuntime = NewLuaRuntime(max_memory=LuaMemoryLimit, register_eval=False, register_builtins=False, attribute_filter=luaAttributeFilter) luaRuntime = NewLuaRuntime(max_memory=LuaMemoryLimit, register_eval=False, register_builtins=False, attribute_filter=luaAttributeFilter)

14
ModWinDog/Start/Start.py Normal file
View File

@ -0,0 +1,14 @@
# ==================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ==================================== #
def cStart(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, OutputMessageData(
text_html=context.endpoint.get_string(
"start", data.user.settings.language).format(data.user.name)))
RegisterModule(name="Start", endpoints=[
SafeNamespace(names=["start"], handler=cStart),
])

View File

@ -0,0 +1,16 @@
summary:
en: Things related to starting the bot, on supported platforms.
endpoints:
start:
summary:
en: Salutes the user, hinting that the bot is working and providing basic quick help.
start:
en: |
<b>Hi</b> {0}<b>!</b>
Use /help to read a list of available commands.
it: |
<b>Ciao</b> {0}<b>!</b>
Usa /help per leggere la lista dei comandi.

View File

@ -0,0 +1,24 @@
# ==================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ==================================== #
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": choice(Locale.__('eval'))})
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()
# <https://stackoverflow.com/a/14693789>
text = (re_compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub('', output))
SendMessage(context, OutputMessageData(
text_plain=text, text_html=f"<pre>{html_escape(text)}</pre>"))
RegisterModule(name="System", endpoints=[
SafeNamespace(names=["exec"], handler=cExec),
])

View File

@ -0,0 +1,5 @@
endpoints:
exec:
summary:
en: Execute a system command from the allowed ones and return stdout+stderr.

View File

@ -8,13 +8,15 @@ import json, time
from binascii import hexlify from binascii import hexlify
from glob import glob from glob import glob
from hashlib import new as hashlib_new from hashlib import new as hashlib_new
from html import escape as html_escape, unescape as html_unescape
from os import listdir from os import listdir
from os.path import isfile, isdir from os.path import isfile, isdir
from random import choice, randint from random import choice, choice as randchoice, randint
from threading import Thread
from traceback import format_exc from traceback import format_exc
from urllib import parse as urlparse
from yaml import load as yaml_load, BaseLoader as yaml_BaseLoader from yaml import load as yaml_load, BaseLoader as yaml_BaseLoader
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from html import unescape as HtmlUnescape
from markdown import markdown from markdown import markdown
from LibWinDog.Types import * from LibWinDog.Types import *
from LibWinDog.Config import * from LibWinDog.Config import *
@ -98,17 +100,21 @@ def ObjGet(node:object, query:str, /) -> any:
return None return None
return node return node
def isinstanceSafe(clazz:any, instance:any) -> bool: def isinstanceSafe(clazz:any, instance:any, /) -> bool:
if instance != None: if instance != None:
return isinstance(clazz, instance) return isinstance(clazz, instance)
return False return False
def GetString(bank:dict, query:str|dict, lang:str=None): def get_string(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}.{lang or DefaultLang}")):
if not (result := ObjGet(bank, f"{query}.en")): if not (result := ObjGet(bank, f"{query}.en")):
result = tuple(ObjGet(bank, query).values())[0] result = ObjGet(bank, query)
return result return result
def strip_url_scheme(url:str) -> str:
tokens = urlparse.urlparse(url)
return f"{tokens.netloc}{tokens.path}"
def CharEscape(String:str, Escape:str='') -> str: def CharEscape(String:str, Escape:str='') -> str:
if Escape == 'MARKDOWN': if Escape == 'MARKDOWN':
return escape_markdown(String, version=2) return escape_markdown(String, version=2)
@ -128,9 +134,6 @@ def InferMdEscape(raw:str, plain:str) -> str:
chars += char chars += char
return chars return chars
def MarkdownCode(text:str, block:bool) -> str:
return ('```\n' + text.strip().replace('`', '\`') + '\n```')
def MdToTxt(md:str) -> str: def MdToTxt(md:str) -> str:
return BeautifulSoup(markdown(md), 'html.parser').get_text(' ') return BeautifulSoup(markdown(md), 'html.parser').get_text(' ')
@ -141,23 +144,6 @@ def HtmlEscapeFull(Raw:str) -> str:
New += f'&#x{Hex[i] + Hex[i+1]};' New += f'&#x{Hex[i] + Hex[i+1]};'
return New return New
def GetRawTokens(text:str) -> list:
return text.strip().replace('\t', ' ').replace(' ', ' ').replace(' ', ' ').split(' ')
def ParseCmd(msg) -> SafeNamespace|None:
#if not len(msg) or msg[1] not in CmdPrefixes:
# return
name = msg.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').replace(' ', ' ').split(' ')[0][1:].split('@')[0]
#if not name:
# return
return SafeNamespace(**{
"Name": name.lower(),
"Body": name.join(msg.split(name)[1:]).strip(),
"Tokens": GetRawTokens(msg),
"User": None,
"Quoted": None,
})
def GetWeightedText(*texts) -> str|None: def GetWeightedText(*texts) -> str|None:
for text in texts: for text in texts:
if text: if text:
@ -179,6 +165,7 @@ def GetUserSettings(user_id:str) -> SafeNamespace|None:
except EntitySettings.DoesNotExist: except EntitySettings.DoesNotExist:
return None return None
# TODO handle @ characters attached to command, e.g. on telegram
def ParseCommand(text:str) -> SafeNamespace|None: def ParseCommand(text:str) -> SafeNamespace|None:
if not text: if not text:
return None return None
@ -227,8 +214,8 @@ def UpdateUserDb(user:SafeNamespace) -> None:
def DumpMessage(data:InputMessageData) -> None: def DumpMessage(data:InputMessageData) -> None:
if not (Debug and (DumpToFile or DumpToConsole)): if not (Debug and (DumpToFile or DumpToConsole)):
return return
text = (data.text_auto.replace('\n', '\\n') if data.text_auto else '') text = (data.text_plain.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}" text = f"[{int(time.time())}] [{time.ctime()}] [{data.room and data.room.id}] [{data.message_id}] [{data.user.id}] {text}"
if DumpToConsole: if DumpToConsole:
print(text, data) print(text, data)
if DumpToFile: if DumpToFile:
@ -247,13 +234,13 @@ def SendMessage(context:EventContext, data:OutputMessageData, destination=None)
if data.text_plain or data.text_markdown or data.text_html: if data.text_plain or data.text_markdown or data.text_html:
if data.text_html and not data.text_plain: if data.text_html and not data.text_plain:
data.text_plain = data.text_html # TODO flatten the HTML to plaintext data.text_plain = BeautifulSoup(data.text_html, "html.parser").get_text()
elif data.text_markdown and not data.text_plain: elif data.text_markdown and not data.text_plain:
data.text_plain = data.text_markdown data.text_plain = data.text_markdown
elif data.text: elif data.text:
# our old system attempts to always receive Markdown and retransform when needed # our old system attempts to always receive Markdown and retransform when needed
data.text_plain = MdToTxt(data.text) data.text_plain = MdToTxt(data.text)
data.text_markdown = CharEscape(HtmlUnescape(data.text), InferMdEscape(HtmlUnescape(data.text), data.text_plain)) data.text_markdown = CharEscape(html_unescape(data.text), InferMdEscape(html_unescape(data.text), data.text_plain))
#data.text_html = ??? #data.text_html = ???
if data.media: if data.media:
data.media = SureArray(data.media) data.media = SureArray(data.media)
@ -272,10 +259,10 @@ def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=N
Log(f"{name}, ", inline=True) Log(f"{name}, ", inline=True)
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None: def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None:
module = SafeNamespace(group=group, endpoints=endpoints) module = SafeNamespace(group=group, endpoints=endpoints, get_string=(lambda query, lang=None, /: None))
if isfile(file := f"./ModWinDog/{name}/{name}.yaml"): if isfile(file := f"./ModWinDog/{name}/{name}.yaml"):
module.strings = yaml_load(open(file, 'r').read().replace("\t", " "), Loader=yaml_BaseLoader) 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)) module.get_string = (lambda query, lang=None: get_string(module.strings, query, lang))
Modules[name] = module Modules[name] = module
Log(f"{name}, ", inline=True) Log(f"{name}, ", inline=True)
for endpoint in endpoints: for endpoint in endpoints:
@ -287,7 +274,7 @@ def CallEndpoint(name:str, context:EventContext, data:InputMessageData):
endpoint = Endpoints[name] endpoint = Endpoints[name]
context.endpoint = endpoint context.endpoint = endpoint
context.module = endpoint.module context.module = endpoint.module
context.endpoint.get_string = (lambda query, lang=None: endpoint.module.get_string(f"endpoints.{data.command.name}.{query}", lang)) context.endpoint.get_string = (lambda query, lang=None, /: endpoint.module.get_string(f"endpoints.{data.command.name}.{query}", lang))
return endpoint.handler(context, data) return endpoint.handler(context, data)
def WriteNewConfig() -> None: def WriteNewConfig() -> None: