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

View File

@@ -3,11 +3,6 @@
# 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}
@@ -20,8 +15,12 @@ def cGdpr(context:EventContext, data:InputMessageData) -> None:
def cConfig(context:EventContext, data:InputMessageData) -> None:
if not (settings := GetUserSettings(data.user.id)):
User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute()
if (get := ObjGet(data, "command.arguments.get")):
SendMessage(context, OutputMessageData(text_plain=str(ObjGet(data.user.settings, get))))
if (to_set := ObjGet(data, "command.arguments.set")):
pass # TODO set in db, but first we need to ensure data is handled safely
if (to_get := ObjGet(data, "command.arguments.get")):
# TODO show a hint on possible options?
return SendMessage(context, OutputMessageData(text_plain=str(ObjGet(data.user.settings, to_get))))
# TODO show general help when no useful parameters are passed
#Cmd = TelegramHandleCmd(update)
#if not Cmd: return
# ... area: eu, us, ...
@@ -39,30 +38,14 @@ def cPing(context:EventContext, data:InputMessageData) -> None:
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),
RegisterModule(name="Base", endpoints=[
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={
"get": True,
}),
#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=["eval"], summary="Execute a Python command (or safe literal operation) in the current context. Currently not implemented.", handler=cEval),
#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),

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:
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, OutputMessageData(text=(prefix + text)))
else:
SendMessage(context, OutputMessageData(text_html=context.endpoint.get_string('empty')))
if not (text := ObjGet(data, "command.body")):
return SendMessage(context, OutputMessageData(text_html=context.endpoint.get_string("empty", data.user.settings.language)))
prefix = f'<a href="{data.message_url}">🗣️</a> '
#prefix = f"[🗣️]({context.linker(data).message}) "
if len(data.command.tokens) == 2:
nonascii = True
for char in data.command.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, OutputMessageData(text_html=(prefix + html_escape(text))))
RegisterModule(name="Echo", endpoints=[
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:
echo:
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()
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."})
output = None
while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix for a strange ratelimit message
output = ""
for completion in g4fClient.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": data.command.body}], stream=True):
for completion in g4fClient.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], stream=True):
output += (completion.choices[0].delta.content or "")
return SendMessage(context, {"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)):
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),
})
return SendMessage(context, OutputMessageData(text_plain=hashed, text_html=f"<pre>{hashed}</pre>"))
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={
RegisterModule(name="Hashing", group="Geek", endpoints=[
SafeNamespace(names=["hash"], handler=cHash, arguments={
"algorithm": True,
}),
])

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

@@ -1,14 +1,15 @@
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
endpoints:
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

@@ -5,17 +5,18 @@
# TODO: implement /help <commandname> feature
def cHelp(context:EventContext, data:InputMessageData) -> None:
moduleList = ''
module_list = ''
language = data.user.settings.language
for module in Modules:
summary = Modules[module].summary
summary = Modules[module].get_string("summary", language)#summary
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:
summary = endpoint.summary
moduleList += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else ''))
SendMessage(context, {"Text": f"[ Available Modules ]{moduleList}"})
summary = Modules[module].get_string(f"endpoints.{endpoint.names[0]}.summary", language)
module_list += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else ''))
SendMessage(context, OutputMessageData(text=module_list))
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 # """
from urlextract import URLExtract
from urllib import parse as UrlParse
from urllib.request import urlopen, Request
def HttpReq(url:str, method:str|None=None, *, body:bytes=None, headers:dict[str, str]={"User-Agent": WebUserAgent}):
return urlopen(Request(url, method=method, data=body, headers=headers))
def cEmbedded(context:EventContext, data:InputMessageData) -> None:
if len(data.Tokens) >= 2:
if len(data.command.tokens) >= 2:
# Find links in command body
Text = (data.TextMarkdown + ' ' + data.TextPlain)
elif data.Quoted and data.Quoted.Text:
Text = (data.text_markdown + ' ' + data.text_plain)
elif data.quoted and data.quoted.text_auto:
# Find links in quoted message
Text = (data.Quoted.TextMarkdown + ' ' + data.Quoted.TextPlain)
Text = (data.quoted.text_markdown + ' ' + data.quoted.text_plain)
else:
# TODO Error message
return
@@ -41,38 +40,37 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None:
url = "https://hlb0.octt.eu.org/cors-main.php/https://" + url
proto = ''
else:
if urlDomain == "instagram.com":
if urlDomain in ("instagram.com", "www.instagram.com"):
urlDomain = "ddinstagram.com"
elif urlDomain in ("twitter.com", "x.com"):
urlDomain = "fxtwitter.com"
elif urlDomain == "vm.tiktok.com":
urlDomain = "vm.vxtiktok.com"
url = urlDomain + url[len(urlDomain):]
url = (urlDomain + '/' + '/'.join(url.split('/')[1:]))
SendMessage(context, {"TextPlain": f"{{{proto}{url}}}"})
# else TODO error message?
def cWeb(context:EventContext, data:InputMessageData) -> None:
if data.Body:
try:
QueryUrl = UrlParse.quote(data.Body)
Req = HttpReq(f'https://html.duckduckgo.com/html?q={QueryUrl}')
Caption = f'🦆🔎 "{data.Body}": https://duckduckgo.com/?q={QueryUrl}\n\n'
Index = 0
for Line in Req.read().decode().replace('\t', ' ').splitlines():
if ' class="result__a" ' in Line and ' href="//duckduckgo.com/l/?uddg=' in Line:
Index += 1
Link = UrlParse.unquote(Line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&amp;rut=')[0])
Title = Line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>')
if len(Title) > 1:
Title = HtmlUnescape(Title[1].strip())
Caption += f'[{Index}] {Title} : {{{Link}}}\n\n'
else:
continue
SendMessage(context, {"TextPlain": f'{Caption}...'})
except Exception:
raise
else:
pass
if not (query := data.command.body):
return # TODO show message
try:
QueryUrl = urlparse.quote(query)
Req = HttpReq(f'https://html.duckduckgo.com/html?q={QueryUrl}')
Caption = f'🦆🔎 "{query}": https://duckduckgo.com/?q={QueryUrl}\n\n'
Index = 0
for Line in Req.read().decode().replace('\t', ' ').splitlines():
if ' class="result__a" ' in Line and ' href="//duckduckgo.com/l/?uddg=' in Line:
Index += 1
Link = urlparse.unquote(Line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&amp;rut=')[0])
Title = Line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>')
if len(Title) > 1:
Title = html_unescape(Title[1].strip())
Caption += f'[{Index}] {Title} : {{{Link}}}\n\n'
else:
continue
SendMessage(context, {"TextPlain": f'{Caption}...'})
except Exception:
raise
def cImages(context:EventContext, data:InputMessageData) -> None:
pass
@@ -82,57 +80,57 @@ def cNews(context:EventContext, data:InputMessageData) -> None:
def cTranslate(context:EventContext, data:InputMessageData) -> None:
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):
return SendMessage(context, {"TextPlain": f"Usage: /translate <to language> <text>"})
try:
# 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']}"})
except Exception:
raise
# 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:
try:
Req = HttpReq(f'https://source.unsplash.com/random/?{UrlParse.quote(data.Body)}')
ImgUrl = Req.geturl().split('?')[0]
SendMessage(context, {
"TextPlain": f'{{{ImgUrl}}}',
"TextMarkdown": MarkdownCode(ImgUrl, True),
"Media": Req.read(),
})
except Exception:
raise
#def cUnsplash(context:EventContext, data:InputMessageData) -> None:
# try:
# Req = HttpReq(f'https://source.unsplash.com/random/?{urlparse.quote(data.command.body)}')
# ImgUrl = Req.geturl().split('?')[0]
# SendMessage(context, {
# "TextPlain": f'{{{ImgUrl}}}',
# "TextMarkdown": MarkdownCode(ImgUrl, True),
# "Media": Req.read(),
# })
# except Exception:
# raise
def cSafebooru(context:EventContext, data:InputMessageData) -> None:
ApiUrl = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags='
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
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:
break
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:
return SendMessage(context, {"Text": "Error: Could not get any result from Safebooru."})
ImgXml = choice(ImgUrls)
ImgUrl = ImgXml.split('"')[0]
ImgId = ImgXml.split(' id="')[1].split('"')[0]
img_url = ImgXml.split('"')[0]
img_id = ImgXml.split(' id="')[1].split('"')[0]
else:
HtmlReq = HttpReq(HttpReq('https://safebooru.org/index.php?page=post&s=random').geturl())
for Line in HtmlReq.read().decode().replace('\t', ' ').splitlines():
if '<img ' in Line and ' id="image" ' in Line and ' src="':
ImgUrl = Line.split(' src="')[1].split('"')[0]
ImgId = ImgUrl.split('?')[-1]
img_url = Line.split(' src="')[1].split('"')[0]
img_id = img_url.split('?')[-1]
break
if ImgUrl:
SendMessage(context, {
"TextPlain": f'[{ImgId}]\n{{{ImgUrl}}}',
"TextMarkdown": (f'\\[`{ImgId}`\\]\n' + MarkdownCode(ImgUrl, True)),
"media": {"url": ImgUrl}, #, "bytes": HttpReq(ImgUrl).read()},
})
if img_url:
SendMessage(context, OutputMessageData(
text_plain=f"[{img_id}]\n{{{img_url}}}",
text_html=f"[<code>{img_id}</code>]\n<pre>{img_url}</pre>",
media={"url": img_url}))
else:
pass
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:
cmdkey = data.Name
cmdkey = data.command.name
replyToId = None
if data.Quoted:
replyFromUid = data.Quoted.User.Id
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)
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
Text = choice(Locale.__(f'{cmdkey}.others')).format(data.user.name, data.quoted.user.name)
replyToId = data.quoted.message_id
else:
if 'empty' in Locale.__(cmdkey):
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:
SendMessage(context, {"Text": choice(Locale.__(f'{data.Name}.{"done" if data.Body else "empty"}')).format(
Cmd=data.Tokens[0], Percent=RandPercent(), Thing=data.Body)})
SendMessage(context, {"Text": choice(Locale.__(f'{data.command.name}.{"done" if data.command.body else "empty"}')).format(
Cmd=data.command.tokens[0], Percent=RandPercent(), Thing=data.command.body)})
RegisterModule(name="Percenter", endpoints=[
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:
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:
if not (prompt := data.command.body):
return SendMessage(context, {"Text": "Please tell me what to generate."})
driver_index, driver = None, None
try:
@@ -74,11 +73,10 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
img_url = img_url.get_attribute("src").split('?')[0]
img_array.append({"url": img_url}) #, "bytes": HttpReq(img_url).read()})
page_url = driver.current_url.split('?')[0]
SendMessage(context, {
"TextPlain": f'"{prompt}"\n{{{page_url}}}',
"TextMarkdown": (f'"_{CharEscape(prompt, "MARKDOWN")}_"\n' + MarkdownCode(page_url, True)),
"media": img_array,
})
SendMessage(context, OutputMessageData(
text_plain=f'"{prompt}"\n{{{page_url}}}',
text_html=f'"<i>{html_escape(prompt)}</i>"\n<pre>{page_url}</pre>',
media=img_array))
return closeSelenium(driver_index, driver)
raise Exception("VM timed out.")
except Exception as error:
@@ -87,8 +85,7 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
closeSelenium(driver_index, driver)
def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
prompt = data.command.body
if not prompt:
if not (prompt := data.command.body):
return SendMessage(context, {"Text": "Please tell me what to generate."})
driver_index, driver = None, None
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
def cLua(context:EventContext, data:InputMessageData) -> None:
# 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:
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)

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.