Initial NodeBB support; Add /wikipedia, /octospacc commands

This commit is contained in:
2025-05-10 01:06:23 +02:00
parent dc8e531079
commit a116a7e055
8 changed files with 181 additions and 55 deletions

View File

@ -0,0 +1,58 @@
# ==================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ==================================== #
""" # windog config start #
# NodeBBUrl = "https://nodebb.example.com"
# NodeBBToken = "abcdefgh-abcd-efgh-ijkl-mnopqrstuvwx"
# end windog config # """
import polling
NodeBBUrl = NodeBBToken = None
NodeBBStamps = {}
def nodebb_request(room_id:int='', method:str="GET", body:dict=None):
return json.loads(HttpReq(f"{NodeBBUrl}/api/v3/chats/{room_id}", method, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {NodeBBToken}",
}, body=(body and json.dumps(body).encode())).read().decode())
def NodeBBMain(path:str) -> bool:
def handler():
try:
for room in nodebb_request()["response"]["rooms"]:
room_id = room["roomId"]
if "roomId" not in NodeBBStamps:
NodeBBStamps[room_id] = 0
if room["teaser"]["timestamp"] > NodeBBStamps[room_id]:
message = nodebb_request(room_id)["response"]["messages"][-1]
NodeBBStamps[room_id] = message["timestamp"]
if not message["self"]:
text_plain = BeautifulSoup(message["content"]).get_text()
data = InputMessageData(
# id = message["timestamp"],
text_html = message["content"],
text_plain = text_plain,
room = SafeNamespace(
id = room_id,
),
user = UserData(
settings = UserSettingsData(),
),
command = TextCommandData(text_plain, "nodebb"),
)
on_input_message_parsed(data)
call_endpoint(EventContext(platform="nodebb"), data)
except Exception:
app_log()
Thread(target=lambda:polling.poll(handler, step=3, poll_forever=True)).start()
return True
def NodeBBSender(context:EventContext, data:OutputMessageData):
nodebb_request(context.data.room.id, "POST", {"message": data["text_plain"]})
register_platform(name="NodeBB", main=NodeBBMain, sender=NodeBBSender)

View File

@ -0,0 +1 @@
urllib3

View File

@ -27,6 +27,12 @@ from hmac import new as hmac_new
TelegramClient = None TelegramClient = None
# def telegram_trim_message(text:str) -> str:
# return trim_message(text, 4096)
# def telegram_trim_caption(text:str) -> str:
# return trim_message(text, 1024)
def TelegramMain(path:str) -> bool: def TelegramMain(path:str) -> bool:
if not TelegramToken: if not TelegramToken:
return False return False

View File

@ -11,7 +11,7 @@ WebConfig = {
"anti_drop_interval": 15, "anti_drop_interval": 15,
} }
WebTokens = {} WebTokens = {} # Generate new tokens with secrets.token_urlsafe()
""" # end windog config # """ """ # end windog config # """

View File

@ -42,7 +42,8 @@ def cPing(context:EventContext, data:InputMessageData):
# nice experiment, but it won't work with Telegram since time is not to milliseconds (?) # nice experiment, but it won't work with Telegram since time is not to milliseconds (?)
#time_diff = (time_now := int(time.time())) - (time_sent := data.datetime) #time_diff = (time_now := int(time.time())) - (time_sent := data.datetime)
#send_message(context, OutputMessageData(text_html=f"<b>Pong!</b>\n\n{time_sent} → {time_now} = {time_diff}")) #send_message(context, OutputMessageData(text_html=f"<b>Pong!</b>\n\n{time_sent} → {time_now} = {time_diff}"))
send_message(context, OutputMessageData(text_html="<b>Pong!</b>")) word = (obj_get({"dick": "cock"}, data.command.name) or data.command.name.replace('i', 'o'))
send_message(context, OutputMessageData(text_html=f"<b>{word[0].upper()}{word[1:]}!</b>"))
#def cTime(update:Update, context:CallbackContext) -> None: #def cTime(update:Update, context:CallbackContext) -> None:
# update.message.reply_markdown_v2( # update.message.reply_markdown_v2(
@ -58,7 +59,7 @@ register_module(name="Base", endpoints=[
"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"], handler=cPing), SafeNamespace(names=["ping", "bing", "ding", "dick"], 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=["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),

View File

@ -12,6 +12,7 @@ MicrosoftBingSettings = {}
from urlextract import URLExtract from urlextract import URLExtract
from urllib import parse as urlparse from urllib import parse as urlparse
from urllib.request import urlopen, Request from urllib.request import urlopen, Request
from translate_shell.translate import translate as ts_translate
def RandomHexString(length:int) -> str: def RandomHexString(length:int) -> str:
return ''.join([randchoice('0123456789abcdef') for i in range(length)]) return ''.join([randchoice('0123456789abcdef') for i in range(length)])
@ -50,32 +51,71 @@ def cEmbedded(context:EventContext, data:InputMessageData):
# elif urlDomain == "vm.tiktok.com": # elif urlDomain == "vm.tiktok.com":
# urlDomain = "vm.vxtiktok.com" # urlDomain = "vm.vxtiktok.com"
# url = (urlDomain + '/' + '/'.join(url.split('/')[1:])) # url = (urlDomain + '/' + '/'.join(url.split('/')[1:]))
if urlDomain in ("facebook.com", "www.facebook.com", "m.facebook.com", "instagram.com", "www.instagram.com", "twitter.com", "x.com", "vm.tiktok.com", "tiktok.com", "www.tiktok.com"): if urlDomain.startswith("www."):
urlDomain = '.'.join(urlDomain.split('.')[1:])
if urlDomain in ("facebook.com", "m.facebook.com", "instagram.com", "twitter.com", "x.com", "vm.tiktok.com", "tiktok.com", "threads.net", "threads.com"):
url = f"https://proxatore.octt.eu.org/{url}" url = f"https://proxatore.octt.eu.org/{url}"
proto = '' proto = ''
elif urlDomain in ("youtube.com",):
url = f"https://proxatore.octt.eu.org/{url}{'' if url.endswith('?') else '?'}&proxatore-htmlmedia=true&proxatore-mediaproxy=video"
proto = ''
return send_message(context, {"text_plain": f"{{{proto}{url}}}"}) return send_message(context, {"text_plain": f"{{{proto}{url}}}"})
return send_message(context, {"text_plain": "No links found."}) return send_message(context, {"text_plain": "No links found."})
def search_duckduckgo(query:str) -> dict:
url = f"https://html.duckduckgo.com/html?q={urlparse.quote(query)}"
request = HttpReq(url)
results = []
for line in request.read().decode().replace('\t', ' ').splitlines():
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])
title = line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>')
if len(title) > 1:
results.append({"title": html_unescape(title[1].strip()), "link": link})
return results
def format_search_result(link:str, title:str, index:int) -> str:
return f'[{index + 1}] {title} : {{{link}}}\n\n'
def cWeb(context:EventContext, data:InputMessageData): def cWeb(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if not (query := data.command.body):
return send_status_400(context, language)
try:
text = f'🦆🔎 "{query}": https://duckduckgo.com/?q={urlparse.quote(query)}\n\n'
for i,e in enumerate(search_duckduckgo(query)):
text += format_search_result(e["link"], e["title"], i)
return send_message(context, {"text_plain": trim_text(text, 4096, True), "text_mode": "trim"})
except Exception:
return send_status_error(context, language)
def cWikipedia(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if not (query := data.command.body):
return send_status_400(context, language)
try:
result = search_duckduckgo(f"site:wikipedia.org {query}")[0]
# TODO try to use API: https://*.wikipedia.org/w/api.php?action=parse&page={title}&prop=text&formatversion=2 (?)
soup = BeautifulSoup(HttpReq(result["link"]).read().decode(), "html.parser").select('#mw-content-text')[0]
if len(elems := soup.select('.infobox')):
elems[0].decompose()
for elem in soup.select('.mw-editsection'):
elem.decompose()
text = (f'{result["title"]}\n{{{result["link"]}}}\n\n' + soup.get_text().strip())
return send_message(context, {"text_plain": trim_text(text, 4096, True), "text_mode": "trim"})
except Exception:
return send_status_error(context, language)
def cFrittoMistoOctoSpacc(context:EventContext, data:InputMessageData):
language = data.user.settings.language language = data.user.settings.language
if not (query := data.command.body): if not (query := data.command.body):
return send_status_400(context, language) return send_status_400(context, language)
try: try:
query_url = urlparse.quote(query) query_url = urlparse.quote(query)
request = HttpReq(f'https://html.duckduckgo.com/html?q={query_url}') text = f'🍤🔎 "{query}": https://octospacc.altervista.org/?s={query_url}\n\n'
caption = f'🦆🔎 "{query}": https://duckduckgo.com/?q={query_url}\n\n' for i,e in enumerate(json.loads(HttpReq(f"https://octospacc.altervista.org/wp-json/wp/v2/posts?search={query_url}").read().decode())):
index = 0 text += format_search_result(e["link"], (e["title"]["rendered"] or e["slug"]), i)
for line in request.read().decode().replace('\t', ' ').splitlines(): return send_message(context, {"text_html": trim_text(text, 4096, True), "text_mode": "trim"})
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
return send_message(context, {"text_plain": f'{caption}...'})
except Exception: except Exception:
return send_status_error(context, language) return send_status_error(context, language)
@ -87,14 +127,15 @@ def cNews(context:EventContext, data:InputMessageData):
def cTranslate(context:EventContext, data:InputMessageData): def cTranslate(context:EventContext, data:InputMessageData):
language = data.user.settings.language language = data.user.settings.language
instances = ["lingva.ml", "lingva.lunar.icu"] #instances = ["lingva.ml", "lingva.lunar.icu"]
language_to = data.command.arguments.language_to language_to = data.command.arguments.language_to
text_input = (data.command.body or (data.quoted and data.quoted.text_plain)) 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 send_status_400(context, language) return send_status_400(context, language)
try: try:
result = json.loads(HttpReq(f'https://{randchoice(instances)}/api/v1/auto/{language_to}/{urlparse.quote(text_input)}').read()) #result = json.loads(HttpReq(f'https://{randchoice(instances)}/api/v1/auto/{language_to}/{urlparse.quote(text_input)}').read())
return send_message(context, {"text_plain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"}) #return send_message(context, {"text_plain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"})
return send_message(context, {"text_plain": f'[auto -> {language_to}]\n\n{ts_translate(text_input, language_to).results[0].paraphrase}'})
except Exception: except Exception:
return send_status_error(context, language) return send_status_error(context, language)
@ -146,8 +187,10 @@ def cSafebooru(context:EventContext, data:InputMessageData):
return send_status_error(context, language) return send_status_error(context, language)
register_module(name="Internet", endpoints=[ register_module(name="Internet", endpoints=[
SafeNamespace(names=["embedded"], handler=cEmbedded, body=False, quoted=False), SafeNamespace(names=["embedded", "embed", "proxy", "proxatore", "sborratore"], handler=cEmbedded, body=False, quoted=False),
SafeNamespace(names=["web"], handler=cWeb, body=True), SafeNamespace(names=["web", "search", "duck", "duckduckgo"], handler=cWeb, body=True),
SafeNamespace(names=["wikipedia", "wokipedia", "wiki"], handler=cWikipedia, body=True),
SafeNamespace(names=["frittomistodioctospacc", "fmos", "frittomisto", "octospacc"], handler=cFrittoMistoOctoSpacc, body=True),
SafeNamespace(names=["translate"], handler=cTranslate, body=False, quoted=False, arguments={ SafeNamespace(names=["translate"], handler=cTranslate, body=False, quoted=False, arguments={
"language_to": True, "language_to": True,
"language_from": False, "language_from": False,

View File

@ -1,2 +1,3 @@
urllib3 urllib3
urlextract urlextract
translate-shell

View File

@ -23,7 +23,7 @@ from LibWinDog.Database import *
from LibWinDog.Utils import * from LibWinDog.Utils import *
def app_log(text:str=None, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None: def app_log(text:str=None, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None:
if not text: if text == None:
text = get_exception_text(full=True) text = get_exception_text(full=True)
endline = '\n' endline = '\n'
if newline == False or (inline and newline == None): if newline == False or (inline and newline == None):
@ -220,6 +220,15 @@ def send_status_error(context:EventContext, lang:str=None, code:int=500, extra:s
app_log() app_log()
return result return result
def trim_text(text:str, limit:int, always_footer:bool=False) -> str:
ending = ""
footer = "\n\n"
if len(text) > limit:
text = (text[:(limit - len(ending + footer))].rstrip() + footer)
elif always_footer:
text = (text.rstrip() + footer)
return text
def get_link(context:EventContext, data:InputMessageData): def get_link(context:EventContext, data:InputMessageData):
data = (InputMessageData(**data) if type(data) == dict else data) data = (InputMessageData(**data) if type(data) == dict else data)
if (data.room and data.room.id): if (data.room and data.room.id):
@ -374,42 +383,49 @@ def app_main() -> None:
if platform.main(f"./LibWinDog/Platforms/{platform.name}"): if platform.main(f"./LibWinDog/Platforms/{platform.name}"):
app_log(f"{platform.name}, ", inline=True) app_log(f"{platform.name}, ", inline=True)
app_log("...Done. ✅️", inline=True, newline=True) app_log("...Done. ✅️", inline=True, newline=True)
app_log("🐶️ WinDog Ready!")
while True:
time.sleep(9**9)
if __name__ == '__main__': if __name__ == '__main__':
app_log("🌞️ WinDog Starting...") app_log("🌞️ WinDog Starting...")
GlobalStrings = good_yaml_load(open("./WinDog.yaml", 'r').read()) try:
Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {} GlobalStrings = good_yaml_load(open("./WinDog.yaml", 'r').read())
Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {}
for folder in ("LibWinDog/Platforms", "ModWinDog"): for folder in ("LibWinDog/Platforms", "ModWinDog"):
match folder: match folder:
case "LibWinDog/Platforms": case "LibWinDog/Platforms":
app_log("📩️ Loading Platforms... ", newline=False) app_log("📩️ Loading Platforms... ", newline=False)
case "ModWinDog": case "ModWinDog":
app_log("🔩️ Loading Modules... ", newline=False) app_log("🔩️ Loading Modules... ", newline=False)
for name in listdir(f"./{folder}"): for name in listdir(f"./{folder}"):
path = f"./{folder}/{name}" path = f"./{folder}/{name}"
if path.endswith(".py") and isfile(path): if path.endswith(".py") and isfile(path):
exec(open(path).read()) exec(open(path).read())
elif isdir(path): elif isdir(path):
files = listdir(path) files = listdir(path)
if f"{name}.py" in files: if f"{name}.py" in files:
files.remove(f"{name}.py") files.remove(f"{name}.py")
exec(open(f"{path}/{name}.py", 'r').read()) exec(open(f"{path}/{name}.py", 'r').read())
#for file in files: #for file in files:
# if file.endswith(".py"): # if file.endswith(".py"):
# exec(open(f"{path}/{file}", 'r').read()) # exec(open(f"{path}/{file}", 'r').read())
app_log("...Done. ✅️", inline=True, newline=True) app_log("...Done. ✅️", inline=True, newline=True)
app_log("💽️ Loading Configuration... ", newline=False) app_log("💽️ Loading Configuration... ", newline=False)
if isfile("./Data/Config.py"): if isfile("./Data/Config.py"):
exec(open("./Data/Config.py", 'r').read()) exec(open("./Data/Config.py", 'r').read())
else: else:
write_new_config() write_new_config()
app_log("Done. ✅️", inline=True, newline=True) app_log("Done. ✅️", inline=True, newline=True)
app_main() app_main()
except Exception:
app_log('')
app_log()
app_log("🛑 Error starting WinDog. Stopping...")
exit(1)
app_log("🐶️ WinDog Ready!")
while True:
time.sleep(9**9)
app_log("🌚️ WinDog Stopping...") app_log("🌚️ WinDog Stopping...")