Misc updates, improve global API, start work on db and module strings

This commit is contained in:
2024-06-25 02:08:49 +02:00
parent 7d426e9497
commit 8f1b80ab14
26 changed files with 389 additions and 210 deletions

2
.gitignore vendored
View File

@ -3,6 +3,6 @@
/Dump.txt /Dump.txt
/Log.txt /Log.txt
/Selenium-WinDog/ /Selenium-WinDog/
/downloaded_files /downloaded_files/
/session.txt /session.txt
*.pyc *.pyc

View File

@ -4,12 +4,14 @@
# ================================== # # ================================== #
""" # windog config start # """ """ # 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 = "" ModifiedSourceUrl = ""
# Logging of system information and runtime errors. Recommended to be on at least for console.
LogToConsole = True LogToConsole = True
LogToFile = True LogToFile = True
# Dumping of the bot's remote events. Should stay off unless needed for debugging.
DumpToConsole = False DumpToConsole = False
DumpToFile = False DumpToFile = False

View File

@ -6,3 +6,17 @@ class BaseModel(Model):
class Meta: class Meta:
database = Db 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)

View File

@ -14,6 +14,7 @@ MastodonUrl, MastodonToken = None, None
import mastodon import mastodon
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from magic import Magic
def MastodonMain() -> bool: def MastodonMain() -> bool:
if not (MastodonUrl and MastodonToken): if not (MastodonUrl and MastodonToken):
@ -36,19 +37,19 @@ def MastodonHandler(event):
if command: if command:
command.messageId = event['status']['id'] command.messageId = event['status']['id']
if command.Name in Endpoints: 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'): 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': while Media['url'] == 'null':
Media = manager.media(Media) Media = context.manager.media(Media)
if textPlain or Media: if textPlain or Media:
manager.status_post( context.manager.status_post(
status=(textPlain + '\n\n@' + event['account']['acct']), status=(textPlain + '\n\n@' + context.event['account']['acct']),
media_ids=(Media if InDict(data, 'Media') else None), media_ids=(Media if InDict(data, 'Media') else None),
in_reply_to_id=event['status']['id'], in_reply_to_id=context.event['status']['id'],
visibility=('direct' if event['status']['visibility'] == 'direct' else 'unlisted'), visibility=('direct' if context.event['status']['visibility'] == 'direct' else 'unlisted'),
) )
RegisterPlatform(name="Mastodon", main=MastodonMain, sender=MastodonSender, managerClass=mastodon.Mastodon) RegisterPlatform(name="Mastodon", main=MastodonMain, sender=MastodonSender, managerClass=mastodon.Mastodon)

View File

@ -41,13 +41,15 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
data.room = SafeNamespace( data.room = SafeNamespace(
id = f"telegram:{message.chat.id}", id = f"telegram:{message.chat.id}",
tag = message.chat.username, tag = message.chat.username,
name = message.chat.title, 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,
) )
#if (db_user := GetUserData(data.user.id)):
# data.user.language = db_user.language
return data return data
def TelegramHandlerWrapper(update:telegram.Update, context:CallbackContext=None) -> None: 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) cmd = ParseCmd(update.message.text)
if cmd: if cmd:
cmd.command = data.command cmd.command = data.command
cmd.quoted = data.quoted
cmd.messageId = update.message.message_id cmd.messageId = update.message.message_id
cmd.TextPlain = cmd.Body cmd.TextPlain = cmd.Body
cmd.TextMarkdown = update.message.text_markdown_v2 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}', "Id": f'telegram:{update.message.reply_to_message.from_user.id}',
}), }),
}) })
Endpoints[cmd.Name]["handler"]({"Event": update, "Manager": context}, cmd) CallEndpoint(cmd.Name, EventContext(platform="telegram", event=update, manager=context), cmd)
#Endpoints[cmd.Name]["handler"](SafeNamespace(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: if destination:
manager.bot.send_message(destination, text=textPlain) result = context.manager.bot.send_message(destination, text=textPlain)
else: 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"): if InDict(data, "Media") and not InDict(data, "media"):
data["media"] = {"bytes": data["Media"]} data["media"] = {"bytes": data["Media"]}
if InDict(data, "media"): if InDict(data, "media"):
for medium in SureArray(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")), (DictGet(medium, "bytes") or DictGet(medium, "url")),
caption=(textMarkdown if textMarkdown else textPlain if textPlain else None), caption=(textMarkdown if textMarkdown else textPlain if textPlain else None),
parse_mode=("MarkdownV2" if textMarkdown else None), parse_mode=("MarkdownV2" if textMarkdown else None),
reply_to_message_id=replyToId) reply_to_message_id=replyToId)
elif textMarkdown: 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: 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: def TelegramLinker(data:InputMessageData) -> SafeNamespace:
linked = SafeNamespace() linked = SafeNamespace()

65
ModWinDog/Base/Base.py Executable file
View File

@ -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()
# <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=["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),
])

View File

@ -12,9 +12,9 @@ def cBroadcast(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, {"TextPlain": data.command.body}, destination) SendMessage(context, {"TextPlain": data.command.body}, destination)
SendMessage(context, {"TextPlain": "Executed."}) SendMessage(context, {"TextPlain": "Executed."})
RegisterModule(name="Broadcast", endpoints={ RegisterModule(name="Broadcast", endpoints=[
"Broadcast": CreateEndpoint(["broadcast"], summary="Sends an admin message over to any chat destination.", handler=cBroadcast, arguments={ SafeNamespace(names=["broadcast"], summary="Sends an admin message over to any chat destination.", handler=cBroadcast, arguments={
"destination": True, "destination": True,
}), }),
}) ])

16
ModWinDog/Dumper/Dumper.py Executable file
View File

@ -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),
])

View File

@ -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.

27
ModWinDog/Echo/Echo.py Normal file
View File

@ -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),
])

8
ModWinDog/Echo/Echo.yaml Normal file
View File

@ -0,0 +1,8 @@
endpoints:
echo:
summary:
en: Responds back with the original text of the received message.
empty:
en: <b>Echo what? Give me something to repeat.</b>
it: <b>Echo cosa? Dimmi qualcosa da ripetere.</b>

View File

@ -10,14 +10,14 @@ g4fClient = G4FClient()
def cGpt(context:EventContext, data:InputMessageData) -> None: def cGpt(context:EventContext, data:InputMessageData) -> None:
if not data.command.body: if not data.command.body:
return SendMessage(context, {"Text": "You must type some text."}) return SendMessage(context, {"Text": "You must type some text."})
output = "" output = None
while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix 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": data.command.body}], 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}"})
RegisterModule(name="ChatGPT", endpoints={ 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), 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),
}) ])

View File

@ -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,
}),
})

24
ModWinDog/Hashing/Hashing.py Executable file
View File

@ -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,
}),
])

View File

@ -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

View File

@ -7,15 +7,15 @@
def cHelp(context:EventContext, data:InputMessageData) -> None: def cHelp(context:EventContext, data:InputMessageData) -> None:
moduleList = '' moduleList = ''
for module in Modules: for module in Modules:
summary = Modules[module]["summary"] summary = Modules[module].summary
endpoints = Modules[module]["endpoints"] endpoints = Modules[module].endpoints
moduleList += (f"\n\n{module}" + (f": {summary}" if summary else '')) moduleList += (f"\n\n{module}" + (f": {summary}" if summary else ''))
for endpoint in endpoints: for endpoint in endpoints:
summary = endpoints[endpoint]["summary"] summary = endpoint.summary
moduleList += (f"\n* /{', /'.join(endpoints[endpoint]['names'])}" + (f": {summary}" if summary else '')) moduleList += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else ''))
SendMessage(context, {"Text": f"[ Available Modules ]{moduleList}"}) SendMessage(context, {"Text": f"[ Available Modules ]{moduleList}"})
RegisterModule(name="Help", group="Basic", endpoints={ RegisterModule(name="Help", group="Basic", endpoints=[
"Help": CreateEndpoint(["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp), SafeNamespace(names=["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp),
}) ])

View File

@ -137,14 +137,14 @@ def cSafebooru(context:EventContext, data:InputMessageData) -> None:
except Exception as error: except Exception as error:
raise raise
RegisterModule(name="Internet", summary="Tools and toys related to the Internet.", endpoints={ 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), SafeNamespace(names=["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), SafeNamespace(names=["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={ SafeNamespace(names=["translate"], summary="Returns the received message after translating it in another language.", handler=cTranslate, arguments={
"language_to": True, "language_to": True,
"language_from": False, "language_from": False,
}), }),
"Unsplash": CreateEndpoint(["unsplash"], summary="Sends a picture sourced from Unsplash.", handler=cUnsplash), SafeNamespace(names=["unsplash"], summary="Sends a picture sourced from Unsplash.", handler=cUnsplash),
"Safebooru": CreateEndpoint(["safebooru"], summary="Sends a picture sourced from Safebooru.", handler=cSafebooru), SafeNamespace(names=["safebooru"], summary="Sends a picture sourced from Safebooru.", handler=cSafebooru),
}) ])

View File

@ -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()
# <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={
"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),
})

View File

@ -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),
])

View File

@ -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),
])

View File

@ -65,7 +65,7 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
if not len(img_list): if not len(img_list):
try: try:
driver.find_element('img.gil_err_img[alt="Unsafe image content detected"]') 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) return closeSelenium(driver_index, driver)
except: # no error is present, so we just have to wait more for the images except: # no error is present, so we just have to wait more for the images
continue continue
@ -121,8 +121,8 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, {"TextPlain": "An unexpected error occurred."}) SendMessage(context, {"TextPlain": "An unexpected error occurred."})
closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
RegisterModule(name="Scrapers", endpoints={ RegisterModule(name="Scrapers", endpoints=[
"DALL-E": CreateEndpoint(["dalle"], summary="Sends an AI-generated picture from DALL-E 3 via Microsoft Bing.", handler=cDalleSelenium), SafeNamespace(names=["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), SafeNamespace(names=["craiyon"], summary="Sends an AI-generated picture from Craiyon.com.", handler=cCraiyonSelenium),
}) ])

View File

@ -52,7 +52,7 @@ end)()"""))
Log(textOutput := ("Lua Error: " + str(error))) Log(textOutput := ("Lua Error: " + str(error)))
SendMessage(context, {"TextPlain": textOutput}) SendMessage(context, {"TextPlain": textOutput})
RegisterModule(name="Scripting", group="Geek", summary="Tools for programming the bot and expanding its features.", endpoints={ 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), SafeNamespace(names=["lua"], summary="Execute a Lua snippet and get its output.", handler=cLua),
}) ])

146
WinDog.py
View File

@ -7,12 +7,14 @@
import json, time import json, time
from binascii import hexlify from binascii import hexlify
from glob import glob from glob import glob
from magic import Magic from hashlib import new as hashlib_new
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, randint
from types import SimpleNamespace from types import SimpleNamespace
from traceback import format_exc 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 bs4 import BeautifulSoup
from html import unescape as HtmlUnescape from html import unescape as HtmlUnescape
from markdown import markdown from markdown import markdown
@ -38,6 +40,13 @@ class InputMessageData(SafeNamespace):
class OutputMessageData(SafeNamespace): class OutputMessageData(SafeNamespace):
pass 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: def Log(text:str, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None:
endline = '\n' endline = '\n'
if newline == False or (inline and newline == None): 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: def DictGet(dikt:dict, key:str, /) -> any:
return (dikt[key] if key in dikt else None) 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: 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):
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: 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)
@ -159,22 +196,28 @@ def RandHexStr(length:int) -> str:
hexa += choice('0123456789abcdef') hexa += choice('0123456789abcdef')
return hexa 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: def ParseCommand(text:str) -> SafeNamespace|None:
command = SafeNamespace()
if not text: if not text:
return command return None
text = text.strip() text = text.strip()
try: # ensure text is a non-empty command try: # ensure text is a non-empty command
if not (text[0] in CmdPrefixes and text[1:].strip()): if not (text[0] in CmdPrefixes and text[1:].strip()):
return command return None
except IndexError: except IndexError:
return return None
command = SafeNamespace()
command.tokens = text.replace("\r", " ").replace("\n", " ").replace("\t", " ").replace(" ", " ").replace(" ", " ").split(" ") command.tokens = text.replace("\r", " ").replace("\n", " ").replace("\t", " ").replace(" ", " ").replace(" ", " ").split(" ")
command.name = command.tokens[0][1:].lower() command.name = command.tokens[0][1:].lower()
command.body = text[len(command.tokens[0]):].strip() command.body = text[len(command.tokens[0]):].strip()
if command.name not in Endpoints: if command.name not in Endpoints:
return command return command
if (endpoint_arguments := Endpoints[command.name]["arguments"]): if (endpoint_arguments := Endpoints[command.name].arguments):#["arguments"]):
command.arguments = {} command.arguments = {}
index = 1 index = 1
for key in endpoint_arguments: for key in endpoint_arguments:
@ -190,20 +233,31 @@ def ParseCommand(text:str) -> SafeNamespace|None:
return command return command
def OnMessageParsed(data:InputMessageData) -> None: def OnMessageParsed(data:InputMessageData) -> None:
if Debug and (DumpToFile or DumpToConsole): DumpMessage(data)
text = (data.text_auto.replace('\n', '\\n') if data.text_auto else '') UpdateUserDb(data.user)
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')
def SendMessage(context, data:OutputMessageData, destination=None) -> None: def UpdateUserDb(user:SafeNamespace) -> None:
if type(context) == dict: try:
event = context['Event'] if 'Event' in context else None User.get(User.id == user.id)
manager = context['Manager'] if 'Manager' in context else None except User.DoesNotExist:
else: user_hash = ("sha256:" + hashlib_new("sha256", user.id.encode()).hexdigest())
[event, manager] = [context, context] 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'): if InDict(data, 'TextPlain') or InDict(data, 'TextMarkdown'):
textPlain = InDict(data, 'TextPlain') textPlain = InDict(data, 'TextPlain')
textMarkdown = InDict(data, 'TextMarkdown') 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)) textMarkdown = CharEscape(HtmlUnescape(data['Text']), InferMdEscape(HtmlUnescape(data['Text']), textPlain))
for platform in Platforms: for platform in Platforms:
platform = Platforms[platform] platform = Platforms[platform]
if isinstanceSafe(event, platform.eventClass) or isinstanceSafe(manager, platform.managerClass): if isinstanceSafe(context.event, platform.eventClass) or isinstanceSafe(context.manager, platform.managerClass):
platform.sender(event, manager, data, destination, textPlain, textMarkdown) return platform.sender(context, data, destination, textPlain, textMarkdown)
def SendNotice(context, data) -> None: def SendNotice(context:EventContext, data) -> None:
pass 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 <module>.")
# 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: 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) Platforms[name] = SafeNamespace(main=main, sender=sender, linker=linker, eventClass=eventClass, managerClass=managerClass)
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:
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) Log(f"{name}, ", inline=True)
for endpoint in endpoints: for endpoint in endpoints:
endpoint = endpoints[endpoint] endpoint.module = module
for name in endpoint["names"]: 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 Endpoints[name] = endpoint
# TODO register endpoint with this instead of RegisterModule def CallEndpoint(name:str, context:EventContext, data:InputMessageData):
def CreateEndpoint(names:list[str], handler:callable, arguments:dict[str, bool]|None=None, *, summary:str|None=None) -> dict: endpoint = Endpoints[name]
return {"names": names, "summary": summary, "handler": handler, "arguments": arguments} context.endpoint = endpoint
context.module = endpoint.module
return endpoint.handler(context, data)
def WriteNewConfig() -> None: def WriteNewConfig() -> None:
Log("💾️ No configuration found! Generating and writing to `./Config.py`... ", inline=True) 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("...Done. ✅️", inline=True, newline=True)
Log("💽️ Loading Configuration... ", newline=False) Log("💽️ Loading Configuration... ", newline=False)
#exec(open("./LibWinDog/Config.py", 'r').read())
from Config import * from Config import *
if isfile("./Config.py"): if isfile("./Config.py"):
from Config import * from Config import *

View File

@ -1,3 +1,4 @@
beautifulsoup4 beautifulsoup4
Markdown Markdown
peewee peewee
PyYAML