Add msc command
This commit is contained in:
parent
0a694476be
commit
b9e635f20b
14 changed files with 378 additions and 160 deletions
1
Pipfile
1
Pipfile
|
@ -12,6 +12,7 @@ httpx = "*"
|
||||||
ollama = "*"
|
ollama = "*"
|
||||||
nio-bot = {git = "git+https://github.com/nexy7574/nio-bot.git"}
|
nio-bot = {git = "git+https://github.com/nexy7574/nio-bot.git"}
|
||||||
thefuzz = "*"
|
thefuzz = "*"
|
||||||
|
redis = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
ruff = "*"
|
ruff = "*"
|
||||||
|
|
19
Pipfile.lock
generated
19
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "6da0a4d5a8f4ae40d08db21d7dae5b6c579fb9b750f3ad379f7f286bebd6ba99"
|
"sha256": "d3f9962ca8bf1f79917897ba8cfb00bce82aacb60a7443816695d01c9b1b0d31"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -671,11 +671,11 @@
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:69297d5da0cc9281c77efffb4e730254dd45943f45bbfb461de5991713989b1e",
|
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
|
||||||
"sha256:e5c5dafde284f26e9e0f28f6ea2d6400abd5ca099864a67f576f3981c6476124"
|
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.9"
|
"version": "==3.10"
|
||||||
},
|
},
|
||||||
"iso8601": {
|
"iso8601": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -826,7 +826,7 @@
|
||||||
"nio-bot": {
|
"nio-bot": {
|
||||||
"git": "git+https://github.com/nexy7574/nio-bot.git",
|
"git": "git+https://github.com/nexy7574/nio-bot.git",
|
||||||
"markers": "python_version < '3.13' and python_version >= '3.9'",
|
"markers": "python_version < '3.13' and python_version >= '3.9'",
|
||||||
"ref": "46821599e36868d97d1961aee2ea3d0108de81cf"
|
"ref": "6aec2074c24bfe07ffd44239ecd4c66b4079ab56"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1257,6 +1257,15 @@
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==3.9.7"
|
"version": "==3.9.7"
|
||||||
},
|
},
|
||||||
|
"redis": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870",
|
||||||
|
"sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==5.0.8"
|
||||||
|
},
|
||||||
"referencing": {
|
"referencing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c",
|
"sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c",
|
||||||
|
|
|
@ -10,10 +10,17 @@ log = logging.getLogger(__name__)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--log-level",
|
"--log-level",
|
||||||
default="INFO",
|
default="INFO",
|
||||||
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False),
|
type=click.Choice(
|
||||||
help="Set the log level."
|
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
|
||||||
|
),
|
||||||
|
help="Set the log level.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--log-file",
|
||||||
|
default=None,
|
||||||
|
type=click.Path(),
|
||||||
|
help="Log to a file as well as stdout",
|
||||||
)
|
)
|
||||||
@click.option("--log-file", default=None, type=click.Path(), help="Log to a file as well as stdout")
|
|
||||||
def cli(log_level: str, log_file: str | None):
|
def cli(log_level: str, log_file: str | None):
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=log_level.upper(),
|
level=log_level.upper(),
|
||||||
|
@ -23,7 +30,9 @@ def cli(log_level: str, log_file: str | None):
|
||||||
if log_file is not None:
|
if log_file is not None:
|
||||||
file_handler = logging.FileHandler(log_file)
|
file_handler = logging.FileHandler(log_file)
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
file_handler.setFormatter(
|
||||||
|
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
)
|
||||||
logging.getLogger().addHandler(file_handler)
|
logging.getLogger().addHandler(file_handler)
|
||||||
|
|
||||||
|
|
||||||
|
|
42
app/main.py
42
app/main.py
|
@ -1,14 +1,16 @@
|
||||||
# import sys
|
# import sys
|
||||||
import asyncio
|
import hashlib
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import niobot
|
import niobot
|
||||||
import tomllib
|
import tomllib
|
||||||
import platform
|
import platform
|
||||||
import logging
|
import logging
|
||||||
import tortoise
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import redis.asyncio as redis
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
logging.getLogger("nio.rooms").setLevel(logging.WARNING)
|
logging.getLogger("nio.rooms").setLevel(logging.WARNING)
|
||||||
logging.getLogger("nio.crypto.log").setLevel(logging.WARNING)
|
logging.getLogger("nio.crypto.log").setLevel(logging.WARNING)
|
||||||
|
@ -23,17 +25,31 @@ with open("config.toml", "rb") as fd:
|
||||||
(store := (Path.cwd() / "store")).mkdir(parents=True, exist_ok=True)
|
(store := (Path.cwd() / "store")).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class TortoiseIntegratedBot(niobot.NioBot):
|
class NonsenseBot(niobot.NioBot):
|
||||||
cfg: dict
|
cfg: dict
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.redis: redis.Redis | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def redis_key(*parts: str) -> str:
|
||||||
|
"""Returns a redis key for the given info"""
|
||||||
|
h = hashlib.md5()
|
||||||
|
for part in parts:
|
||||||
|
h.update(part.encode())
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self.redis.aclose()
|
||||||
|
|
||||||
async def start(self, **kwargs):
|
async def start(self, **kwargs):
|
||||||
url = config["database"].get("uri")
|
url = config["database"].get("uri")
|
||||||
if not url:
|
if url:
|
||||||
log.critical("No database URI specified in config.toml - using ephemeral sqlite.")
|
self.redis = redis.Redis.from_url(url)
|
||||||
url = "sqlite://:memory:"
|
assert await self.redis.ping()
|
||||||
|
|
||||||
sys.path.extend(("..", "."))
|
sys.path.extend(("..", "."))
|
||||||
await tortoise.Tortoise.init(db_url=url, modules={"models": ["app.models"]})
|
|
||||||
await tortoise.Tortoise.generate_schemas()
|
|
||||||
|
|
||||||
for file in (Path(__file__).parent / "./modules").glob("*.py"):
|
for file in (Path(__file__).parent / "./modules").glob("*.py"):
|
||||||
if file.name.startswith("__"):
|
if file.name.startswith("__"):
|
||||||
|
@ -50,14 +66,14 @@ class TortoiseIntegratedBot(niobot.NioBot):
|
||||||
await super().start(**kwargs)
|
await super().start(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
bot = TortoiseIntegratedBot(
|
bot = NonsenseBot(
|
||||||
homeserver=config["bot"]["homeserver"],
|
homeserver=config["bot"]["homeserver"],
|
||||||
user_id=config["bot"]["user_id"],
|
user_id=config["bot"]["user_id"],
|
||||||
device_id=config["bot"].get("device_id", platform.node()),
|
device_id=config["bot"].get("device_id", platform.node()),
|
||||||
store_path=str(store.resolve()),
|
store_path=str(store.resolve()),
|
||||||
command_prefix=config["bot"].get("prefix", "h!"),
|
command_prefix=config["bot"].get("prefix", "h!"),
|
||||||
owner_id=config["bot"].get("owner_id") or "@nex:nexy7574.co.uk",
|
owner_id=config["bot"].get("owner_id") or "@nex:nexy7574.co.uk",
|
||||||
auto_read_messages=False
|
auto_read_messages=False,
|
||||||
)
|
)
|
||||||
bot.cfg = config
|
bot.cfg = config
|
||||||
|
|
||||||
|
@ -76,11 +92,9 @@ async def on_command(ctx: niobot.Context):
|
||||||
async def on_command_error(ctx: niobot.Context, exc: Exception):
|
async def on_command_error(ctx: niobot.Context, exc: Exception):
|
||||||
exc = getattr(exc, "original", exc) or exc
|
exc = getattr(exc, "original", exc) or exc
|
||||||
log.error("Command %s failed.", ctx.command, exc_info=exc)
|
log.error("Command %s failed.", ctx.command, exc_info=exc)
|
||||||
text = "\N{warning sign} Command failed: `%s`" % exc
|
text = "\N{WARNING SIGN} Command failed: `%s`" % exc
|
||||||
text = text.replace(bot.access_token, "REDACTED")
|
text = text.replace(bot.access_token, "REDACTED")
|
||||||
await ctx.respond(
|
await ctx.respond(text)
|
||||||
text
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_event("command_complete")
|
@bot.on_event("command_complete")
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
from tortoise import Model, fields
|
|
||||||
|
|
||||||
|
|
||||||
class Account(Model):
|
|
||||||
mxid: str = fields.CharField(max_length=255, primary_key=True, generated=False)
|
|
||||||
current_balance: int = fields.IntField(default=0)
|
|
||||||
bank_balance: int = fields.IntField(default=0)
|
|
||||||
currency: str = fields.CharField(max_length=3, default="GBP")
|
|
|
@ -3,6 +3,7 @@ This module is an old mockup of an AI chatbot system using ollama and a set of s
|
||||||
|
|
||||||
It is not actively used or maintained anymore.
|
It is not actively used or maintained anymore.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
@ -17,7 +18,7 @@ from ollama import AsyncClient
|
||||||
import niobot
|
import niobot
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from ..main import TortoiseIntegratedBot
|
from ..main import NonsenseBot
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
@ -28,7 +29,7 @@ async def ollama_client(url: str) -> AsyncClient:
|
||||||
|
|
||||||
|
|
||||||
class AIModule(niobot.Module):
|
class AIModule(niobot.Module):
|
||||||
bot: "TortoiseIntegratedBot"
|
bot: "NonsenseBot"
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def find_server(self, gpu_only: bool = True) -> dict[str, str | bool] | None:
|
async def find_server(self, gpu_only: bool = True) -> dict[str, str | bool] | None:
|
||||||
|
@ -65,7 +66,9 @@ class AIModule(niobot.Module):
|
||||||
|
|
||||||
@niobot.command("whitelist.add")
|
@niobot.command("whitelist.add")
|
||||||
@niobot.is_owner()
|
@niobot.is_owner()
|
||||||
async def whitelist_add(self, ctx: niobot.Context, user_id: str, model: str = "llama3:latest"):
|
async def whitelist_add(
|
||||||
|
self, ctx: niobot.Context, user_id: str, model: str = "llama3:latest"
|
||||||
|
):
|
||||||
"""[Owner] Adds a user to the whitelist."""
|
"""[Owner] Adds a user to the whitelist."""
|
||||||
users = self.read_users()
|
users = self.read_users()
|
||||||
users[user_id] = model
|
users[user_id] = model
|
||||||
|
@ -104,7 +107,9 @@ class AIModule(niobot.Module):
|
||||||
return
|
return
|
||||||
users[ctx.message.sender] = model
|
users[ctx.message.sender] = model
|
||||||
self.write_users(users)
|
self.write_users(users)
|
||||||
await ctx.respond(f"Set model to {model}. Don't forget to pull it with `h!ollama.pull`.")
|
await ctx.respond(
|
||||||
|
f"Set model to {model}. Don't forget to pull it with `h!ollama.pull`."
|
||||||
|
)
|
||||||
|
|
||||||
@niobot.command("ollama.pull")
|
@niobot.command("ollama.pull")
|
||||||
@niobot.from_homeserver("nexy7574.co.uk", "nicroxio.co.uk", "shronk.net")
|
@niobot.from_homeserver("nexy7574.co.uk", "nicroxio.co.uk", "shronk.net")
|
||||||
|
@ -112,7 +117,9 @@ class AIModule(niobot.Module):
|
||||||
"""Pulls the model you set."""
|
"""Pulls the model you set."""
|
||||||
users = self.read_users()
|
users = self.read_users()
|
||||||
if ctx.message.sender not in users:
|
if ctx.message.sender not in users:
|
||||||
await ctx.respond("You need to set a model first. See: `h!help ollama.set-model`")
|
await ctx.respond(
|
||||||
|
"You need to set a model first. See: `h!help ollama.set-model`"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
model = users[ctx.message.sender]
|
model = users[ctx.message.sender]
|
||||||
server = await self.find_server(gpu_only=False)
|
server = await self.find_server(gpu_only=False)
|
||||||
|
@ -137,7 +144,9 @@ class AIModule(niobot.Module):
|
||||||
message = " ".join(ctx.args)
|
message = " ".join(ctx.args)
|
||||||
users = self.read_users()
|
users = self.read_users()
|
||||||
if ctx.message.sender not in users:
|
if ctx.message.sender not in users:
|
||||||
await ctx.respond("You need to set a model first. See: `h!help ollama.set-model`")
|
await ctx.respond(
|
||||||
|
"You need to set a model first. See: `h!help ollama.set-model`"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
model = users[ctx.message.sender]
|
model = users[ctx.message.sender]
|
||||||
res = await ctx.respond("Finding server...")
|
res = await ctx.respond("Finding server...")
|
||||||
|
@ -148,7 +157,9 @@ class AIModule(niobot.Module):
|
||||||
async with ollama_client(server["url"]) as client:
|
async with ollama_client(server["url"]) as client:
|
||||||
await res.edit(content=f"Generating response...")
|
await res.edit(content=f"Generating response...")
|
||||||
try:
|
try:
|
||||||
response = await client.chat(model, [{"role": "user", "content": message}])
|
response = await client.chat(
|
||||||
|
model, [{"role": "user", "content": message}]
|
||||||
|
)
|
||||||
except httpx.HTTPError as e:
|
except httpx.HTTPError as e:
|
||||||
response = {"message": {"content": f"Error: {e}"}}
|
response = {"message": {"content": f"Error: {e}"}}
|
||||||
await res.edit(content=response["message"]["content"])
|
await res.edit(content=response["message"]["content"])
|
||||||
|
@ -162,16 +173,12 @@ class AIModule(niobot.Module):
|
||||||
"""Checks which servers are online."""
|
"""Checks which servers are online."""
|
||||||
lines: dict[str, dict[str, str | None | bool]] = {}
|
lines: dict[str, dict[str, str | None | bool]] = {}
|
||||||
for name, cfg in self.bot.cfg["ollama"].items():
|
for name, cfg in self.bot.cfg["ollama"].items():
|
||||||
lines[name] = {
|
lines[name] = {"url": cfg["url"], "gpu": cfg["gpu"], "online": None}
|
||||||
"url": cfg["url"],
|
|
||||||
"gpu": cfg["gpu"],
|
|
||||||
"online": None
|
|
||||||
}
|
|
||||||
|
|
||||||
emojis = {
|
emojis = {
|
||||||
True: "\N{white heavy check mark}",
|
True: "\N{WHITE HEAVY CHECK MARK}",
|
||||||
False: "\N{cross mark}",
|
False: "\N{CROSS MARK}",
|
||||||
None: "\N{hourglass with flowing sand}"
|
None: "\N{HOURGLASS WITH FLOWING SAND}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_lines():
|
def get_lines():
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Module that provides code-evaluation commands.
|
Module that provides code-evaluation commands.
|
||||||
This entire module is locked to NioBot.owner.
|
This entire module is locked to NioBot.owner.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
import shlex
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -21,14 +22,16 @@ import functools
|
||||||
class EvalModule(niobot.Module):
|
class EvalModule(niobot.Module):
|
||||||
def __init__(self, bot: niobot.NioBot):
|
def __init__(self, bot: niobot.NioBot):
|
||||||
if not bot.owner_id:
|
if not bot.owner_id:
|
||||||
raise RuntimeError("No owner ID set in niobot. Refusing to load for security reasons.")
|
raise RuntimeError(
|
||||||
|
"No owner ID set in niobot. Refusing to load for security reasons."
|
||||||
|
)
|
||||||
super().__init__(bot)
|
super().__init__(bot)
|
||||||
|
|
||||||
async def owner_check(self, ctx: niobot.Context) -> bool:
|
async def owner_check(self, ctx: niobot.Context) -> bool:
|
||||||
"""Checks if the current user is the owner, and if not, gives them a very informative message."""
|
"""Checks if the current user is the owner, and if not, gives them a very informative message."""
|
||||||
if not self.bot.is_owner(ctx.message.sender):
|
if not self.bot.is_owner(ctx.message.sender):
|
||||||
await ctx.respond(
|
await ctx.respond(
|
||||||
"\N{cross mark} Only the owner of this bot can run evaluation commands. Nice try, though!"
|
"\N{CROSS MARK} Only the owner of this bot can run evaluation commands. Nice try, though!"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
@ -38,7 +41,14 @@ class EvalModule(niobot.Module):
|
||||||
"""Removes any code block syntax from the given string."""
|
"""Removes any code block syntax from the given string."""
|
||||||
code = code.strip()
|
code = code.strip()
|
||||||
lines = code.splitlines(False)
|
lines = code.splitlines(False)
|
||||||
if len(lines[0]) == 2 or lines[0] in ("py", "python", "python3", "sh", "shell", "bash"):
|
if len(lines[0]) == 2 or lines[0] in (
|
||||||
|
"py",
|
||||||
|
"python",
|
||||||
|
"python3",
|
||||||
|
"sh",
|
||||||
|
"shell",
|
||||||
|
"bash",
|
||||||
|
):
|
||||||
# likely a codeblock language identifier
|
# likely a codeblock language identifier
|
||||||
lines = lines[1:]
|
lines = lines[1:]
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
@ -80,11 +90,11 @@ class EvalModule(niobot.Module):
|
||||||
"niobot": niobot,
|
"niobot": niobot,
|
||||||
"pprint": pprint.pprint,
|
"pprint": pprint.pprint,
|
||||||
}
|
}
|
||||||
code = code.replace("\u00A0", " ") # 00A0 is
|
code = code.replace("\u00a0", " ") # 00A0 is
|
||||||
code = textwrap.indent(code, " ")
|
code = textwrap.indent(code, " ")
|
||||||
code = f"async def __eval():\n{code}"
|
code = f"async def __eval():\n{code}"
|
||||||
msg = await ctx.respond(f"Evaluating:\n```py\n{code}\n```")
|
msg = await ctx.respond(f"Evaluating:\n```py\n{code}\n```")
|
||||||
e = await self.client.add_reaction(ctx.room, ctx.message, "\N{hammer}")
|
e = await self.client.add_reaction(ctx.room, ctx.message, "\N{HAMMER}")
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
start = time.time() * 1000
|
start = time.time() * 1000
|
||||||
|
@ -116,10 +126,12 @@ class EvalModule(niobot.Module):
|
||||||
lines.append("Stdout:\n```\n" + stdout.getvalue() + "```")
|
lines.append("Stdout:\n```\n" + stdout.getvalue() + "```")
|
||||||
if stderr.getvalue():
|
if stderr.getvalue():
|
||||||
lines.append("Stderr:\n```\n" + stderr.getvalue() + "```")
|
lines.append("Stderr:\n```\n" + stderr.getvalue() + "```")
|
||||||
await ctx.client.add_reaction(ctx.room, ctx.message, "\N{white heavy check mark}")
|
await ctx.client.add_reaction(
|
||||||
|
ctx.room, ctx.message, "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
)
|
||||||
await msg.edit("\n".join(lines))
|
await msg.edit("\n".join(lines))
|
||||||
except Exception:
|
except Exception:
|
||||||
await ctx.client.add_reaction(ctx.room, ctx.message, "\N{cross mark}")
|
await ctx.client.add_reaction(ctx.room, ctx.message, "\N{CROSS MARK}")
|
||||||
await msg.edit(f"Error:\n```py\n{traceback.format_exc()}```")
|
await msg.edit(f"Error:\n```py\n{traceback.format_exc()}```")
|
||||||
|
|
||||||
@niobot.command("shell")
|
@niobot.command("shell")
|
||||||
|
@ -136,7 +148,7 @@ class EvalModule(niobot.Module):
|
||||||
|
|
||||||
msg = await ctx.respond(f"Running command: `{command}`")
|
msg = await ctx.respond(f"Running command: `{command}`")
|
||||||
cmd, args = command.split(" ", 1)
|
cmd, args = command.split(" ", 1)
|
||||||
await self.client.add_reaction(ctx.room, ctx.message, "\N{hammer}")
|
await self.client.add_reaction(ctx.room, ctx.message, "\N{HAMMER}")
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
@ -149,22 +161,34 @@ class EvalModule(niobot.Module):
|
||||||
cwd=tmpdir,
|
cwd=tmpdir,
|
||||||
)
|
)
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
await ctx.client.add_reaction(ctx.room, ctx.message, "\N{white heavy check mark}")
|
await ctx.client.add_reaction(
|
||||||
|
ctx.room, ctx.message, "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
)
|
||||||
lines = [f"Input:\n```sh\n$ {cmd} {args}\n```\n"]
|
lines = [f"Input:\n```sh\n$ {cmd} {args}\n```\n"]
|
||||||
stdout = (await proc.stdout.read()).decode().replace("```", "`\u200b`\u200b`")
|
stdout = (
|
||||||
stderr = (await proc.stderr.read()).decode().replace("```", "`\u200b`\u200b`")
|
(await proc.stdout.read())
|
||||||
|
.decode()
|
||||||
|
.replace("```", "`\u200b`\u200b`")
|
||||||
|
)
|
||||||
|
stderr = (
|
||||||
|
(await proc.stderr.read())
|
||||||
|
.decode()
|
||||||
|
.replace("```", "`\u200b`\u200b`")
|
||||||
|
)
|
||||||
if stdout:
|
if stdout:
|
||||||
lines.append("Stdout:\n```\n" + stdout + "```\n")
|
lines.append("Stdout:\n```\n" + stdout + "```\n")
|
||||||
if stderr:
|
if stderr:
|
||||||
lines.append("Stderr:\n```\n" + stderr + "```\n")
|
lines.append("Stderr:\n```\n" + stderr + "```\n")
|
||||||
await msg.edit("\n".join(lines))
|
await msg.edit("\n".join(lines))
|
||||||
except Exception:
|
except Exception:
|
||||||
await ctx.client.add_reaction(ctx.room, ctx.message, "\N{cross mark}")
|
await ctx.client.add_reaction(ctx.room, ctx.message, "\N{CROSS MARK}")
|
||||||
await msg.edit(f"Error:\n```py\n{traceback.format_exc()}```")
|
await msg.edit(f"Error:\n```py\n{traceback.format_exc()}```")
|
||||||
|
|
||||||
@niobot.command("thumbnail")
|
@niobot.command("thumbnail")
|
||||||
@niobot.is_owner()
|
@niobot.is_owner()
|
||||||
async def thumbnail(self, ctx: niobot.Context, url: str, width: int = 320, height: int = 240):
|
async def thumbnail(
|
||||||
|
self, ctx: niobot.Context, url: str, width: int = 320, height: int = 240
|
||||||
|
):
|
||||||
"""Get the thumbnail for a URL.
|
"""Get the thumbnail for a URL.
|
||||||
|
|
||||||
URL can be an mxc, in which case C2S will be used to get a thumbnail.
|
URL can be an mxc, in which case C2S will be used to get a thumbnail.
|
||||||
|
@ -173,10 +197,7 @@ class EvalModule(niobot.Module):
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
media_id = parsed.path.lstrip("/")
|
media_id = parsed.path.lstrip("/")
|
||||||
thumbnail = await ctx.bot.thumbnail(
|
thumbnail = await ctx.bot.thumbnail(
|
||||||
parsed.netloc,
|
parsed.netloc, media_id, width=width, height=height
|
||||||
media_id,
|
|
||||||
width=width,
|
|
||||||
height=height
|
|
||||||
)
|
)
|
||||||
if not isinstance(thumbnail, niobot.ThumbnailResponse):
|
if not isinstance(thumbnail, niobot.ThumbnailResponse):
|
||||||
return await ctx.respond("Failed to thumbnail: %r" % thumbnail)
|
return await ctx.respond("Failed to thumbnail: %r" % thumbnail)
|
||||||
|
@ -188,7 +209,9 @@ class EvalModule(niobot.Module):
|
||||||
file = await niobot.ImageAttachment.from_file(body, fn)
|
file = await niobot.ImageAttachment.from_file(body, fn)
|
||||||
return await ctx.respond("Downloaded from %r" % url, file=file)
|
return await ctx.respond("Downloaded from %r" % url, file=file)
|
||||||
|
|
||||||
async with aiohttp.ClientSession(headers={"User-Agent": niobot.__user_agent__}) as client:
|
async with aiohttp.ClientSession(
|
||||||
|
headers={"User-Agent": niobot.__user_agent__}
|
||||||
|
) as client:
|
||||||
async with client.get(url) as response:
|
async with client.get(url) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
await ctx.respond("Error: %d" % response.status)
|
await ctx.respond("Error: %d" % response.status)
|
||||||
|
@ -200,7 +223,9 @@ class EvalModule(niobot.Module):
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
thumb.save("file.webp", "webp")
|
thumb.save("file.webp", "webp")
|
||||||
attachment = await niobot.ImageAttachment.from_file("file.webp", generate_blurhash=True)
|
attachment = await niobot.ImageAttachment.from_file(
|
||||||
|
"file.webp", generate_blurhash=True
|
||||||
|
)
|
||||||
await ctx.respond("thumbnail.webp", file=attachment)
|
await ctx.respond("thumbnail.webp", file=attachment)
|
||||||
|
|
||||||
@niobot.command()
|
@niobot.command()
|
||||||
|
@ -232,7 +257,9 @@ class EvalModule(niobot.Module):
|
||||||
if room.startswith("#"):
|
if room.startswith("#"):
|
||||||
resolved = await self.bot.room_resolve_alias(room)
|
resolved = await self.bot.room_resolve_alias(room)
|
||||||
if not isinstance(resolved, niobot.RoomResolveAliasResponse):
|
if not isinstance(resolved, niobot.RoomResolveAliasResponse):
|
||||||
return await ctx.respond("Failed to resolve room ID for: %r", room)
|
return await ctx.respond(
|
||||||
|
"Failed to resolve room ID for: %r", room
|
||||||
|
)
|
||||||
room = resolved.room_id
|
room = resolved.room_id
|
||||||
response = await self.bot.room_leave(room)
|
response = await self.bot.room_leave(room)
|
||||||
return await ctx.respond(repr(response))
|
return await ctx.respond(repr(response))
|
||||||
|
|
|
@ -4,18 +4,20 @@ This module takes fediverse links (e.g.) and provides a preview in a reply.
|
||||||
By default, this will only work with fedi.transgender.ing, but if you set `[fediverse_preview.urls]` in your config,
|
By default, this will only work with fedi.transgender.ing, but if you set `[fediverse_preview.urls]` in your config,
|
||||||
you can add more. This module will use the Mastodon protocol, however it is designed to be used with misskey.
|
you can add more. This module will use the Mastodon protocol, however it is designed to be used with misskey.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import niobot
|
import niobot
|
||||||
import httpx
|
import httpx
|
||||||
import typing
|
import typing
|
||||||
import textwrap
|
import textwrap
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from ..main import TortoiseIntegratedBot
|
from ..main import NonsenseBot
|
||||||
|
|
||||||
|
|
||||||
class FediversePreviewModule(niobot.Module):
|
class FediversePreviewModule(niobot.Module):
|
||||||
bot: "TortoiseIntegratedBot"
|
bot: "NonsenseBot"
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@niobot.event("message")
|
@niobot.event("message")
|
||||||
|
@ -25,12 +27,18 @@ class FediversePreviewModule(niobot.Module):
|
||||||
ignore = config.get("ignore", [])
|
ignore = config.get("ignore", [])
|
||||||
if not isinstance(event, niobot.RoomMessageText):
|
if not isinstance(event, niobot.RoomMessageText):
|
||||||
return
|
return
|
||||||
if event.sender in ignore or room.room_id in ignore or event.sender == self.bot.user_id:
|
if (
|
||||||
|
event.sender in ignore
|
||||||
|
or room.room_id in ignore
|
||||||
|
or event.sender == self.bot.user_id
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
sent = []
|
sent = []
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0"}
|
headers={
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0"
|
||||||
|
}
|
||||||
) as client:
|
) as client:
|
||||||
for item in event.body.split():
|
for item in event.body.split():
|
||||||
if not item.startswith(tuple(supported_prefixes)):
|
if not item.startswith(tuple(supported_prefixes)):
|
||||||
|
@ -43,7 +51,9 @@ class FediversePreviewModule(niobot.Module):
|
||||||
elif len(sent) >= 5:
|
elif len(sent) >= 5:
|
||||||
self.log.info("Already sent 5 posts, stopping")
|
self.log.info("Already sent 5 posts, stopping")
|
||||||
break
|
break
|
||||||
resp = await client.get("https://%s/api/v1/statuses/%s" % (parsed.netloc, post_id))
|
resp = await client.get(
|
||||||
|
"https://%s/api/v1/statuses/%s" % (parsed.netloc, post_id)
|
||||||
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
self.log.error("Got HTTP %d from %s", resp.status_code, resp.url)
|
self.log.error("Got HTTP %d from %s", resp.status_code, resp.url)
|
||||||
continue
|
continue
|
||||||
|
@ -57,19 +67,21 @@ class FediversePreviewModule(niobot.Module):
|
||||||
self.log.info("Detected fediverse post %s: %r", post_id, text)
|
self.log.info("Detected fediverse post %s: %r", post_id, text)
|
||||||
rendered = await self.bot._markdown_to_html(text)
|
rendered = await self.bot._markdown_to_html(text)
|
||||||
text_body = "<blockquote>%s</blockquote>" % rendered
|
text_body = "<blockquote>%s</blockquote>" % rendered
|
||||||
body = "<a href=\"%s\">@%s:</a><br>%s" % (
|
body = '<a href="%s">@%s:</a><br>%s' % (
|
||||||
"https://%s/@%s" % (parsed.netloc, username),
|
"https://%s/@%s" % (parsed.netloc, username),
|
||||||
username,
|
username,
|
||||||
text_body,
|
text_body,
|
||||||
)
|
)
|
||||||
if data.get("media_attachments"):
|
if data.get("media_attachments"):
|
||||||
body += "<p>{:,} attachments</p>".format(len(data["media_attachments"]))
|
body += "<p>{:,} attachments</p>".format(
|
||||||
|
len(data["media_attachments"])
|
||||||
|
)
|
||||||
self.log.info("Sending fediverse post %s", post_id)
|
self.log.info("Sending fediverse post %s", post_id)
|
||||||
await self.bot.send_message(
|
await self.bot.send_message(
|
||||||
room,
|
room,
|
||||||
body,
|
body,
|
||||||
reply_to=event,
|
reply_to=event,
|
||||||
content_type="html.raw",
|
content_type="html.raw",
|
||||||
override={"body": f"@{username}: {data['content']!r}"}
|
override={"body": f"@{username}: {data['content']!r}"},
|
||||||
)
|
)
|
||||||
sent.append(post_id)
|
sent.append(post_id)
|
||||||
|
|
|
@ -21,10 +21,7 @@ class InfoModule(niobot.Module):
|
||||||
resolved: niobot.RoomResolveAliasError
|
resolved: niobot.RoomResolveAliasError
|
||||||
return await x.edit(
|
return await x.edit(
|
||||||
"Sorry, I was unable to resolve that room alias.\n\n{!r}".format(
|
"Sorry, I was unable to resolve that room alias.\n\n{!r}".format(
|
||||||
str(resolved).replace(
|
str(resolved).replace(self.bot.access_token, "[REDACTED]")
|
||||||
self.bot.access_token,
|
|
||||||
"[REDACTED]"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
room = resolved.room_id
|
room = resolved.room_id
|
||||||
|
@ -42,10 +39,7 @@ class InfoModule(niobot.Module):
|
||||||
self.log.debug("Ratio for %r: %r", room_obj.display_name, ratio)
|
self.log.debug("Ratio for %r: %r", room_obj.display_name, ratio)
|
||||||
if ratio > 1:
|
if ratio > 1:
|
||||||
matches.append((room_obj, ratio))
|
matches.append((room_obj, ratio))
|
||||||
matches.sort(
|
matches.sort(key=lambda v: v[1], reverse=True)
|
||||||
key=lambda v: v[1],
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
if not matches:
|
if not matches:
|
||||||
return await ctx.respond(
|
return await ctx.respond(
|
||||||
"[room] was not a room ID or alias, and I do not know of any mutual rooms that have"
|
"[room] was not a room ID or alias, and I do not know of any mutual rooms that have"
|
||||||
|
@ -54,9 +48,7 @@ class InfoModule(niobot.Module):
|
||||||
room = matches[0][0].room_id
|
room = matches[0][0].room_id
|
||||||
|
|
||||||
if room not in ctx.bot.rooms:
|
if room not in ctx.bot.rooms:
|
||||||
return await ctx.respond(
|
return await ctx.respond("Unknown room %r. Am I in it?" % room)
|
||||||
"Unknown room %r. Am I in it?" % room
|
|
||||||
)
|
|
||||||
|
|
||||||
room_obj: niobot.MatrixRoom = ctx.bot.rooms[room]
|
room_obj: niobot.MatrixRoom = ctx.bot.rooms[room]
|
||||||
lines = [
|
lines = [
|
||||||
|
@ -70,11 +62,11 @@ class InfoModule(niobot.Module):
|
||||||
"* History visibility: {obj.history_visibility!r}",
|
"* History visibility: {obj.history_visibility!r}",
|
||||||
"* User count: {user_count:,} ({invited_count:,} invited)",
|
"* User count: {user_count:,} ({invited_count:,} invited)",
|
||||||
"* Avatar URL: `{obj.room_avatar_url!s}`",
|
"* Avatar URL: `{obj.room_avatar_url!s}`",
|
||||||
"* Canonical alias: {obj.canonical_alias}"
|
"* Canonical alias: {obj.canonical_alias}",
|
||||||
]
|
]
|
||||||
body = "\n".join(lines).format(
|
body = "\n".join(lines).format(
|
||||||
obj=room_obj,
|
obj=room_obj,
|
||||||
user_count=room_obj.member_count,
|
user_count=room_obj.member_count,
|
||||||
invited_count=room_obj.invited_count
|
invited_count=room_obj.invited_count,
|
||||||
)
|
)
|
||||||
return await ctx.respond(body)
|
return await ctx.respond(body)
|
||||||
|
|
|
@ -3,6 +3,7 @@ This module simply returns the latency between a message being sent and the bot
|
||||||
|
|
||||||
It also supports a rudimentary federation latency check, by sending a request to the target homeserver.
|
It also supports a rudimentary federation latency check, by sending a request to the target homeserver.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import niobot
|
import niobot
|
||||||
|
@ -11,7 +12,6 @@ import httpx
|
||||||
|
|
||||||
|
|
||||||
class LatencyModule(niobot.Module):
|
class LatencyModule(niobot.Module):
|
||||||
|
|
||||||
@niobot.command("latency", aliases=["ping"])
|
@niobot.command("latency", aliases=["ping"])
|
||||||
async def latency(self, ctx: niobot.Context, homeserver: str = None):
|
async def latency(self, ctx: niobot.Context, homeserver: str = None):
|
||||||
"""
|
"""
|
||||||
|
@ -23,10 +23,7 @@ class LatencyModule(niobot.Module):
|
||||||
|
|
||||||
response = await ctx.respond(
|
response = await ctx.respond(
|
||||||
"Latency: {:,.2f}ms{}".format(
|
"Latency: {:,.2f}ms{}".format(
|
||||||
latency,
|
latency, " (pinging homeserver...)" if homeserver else ""
|
||||||
" (pinging homeserver...)"
|
|
||||||
if homeserver
|
|
||||||
else ''
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if homeserver:
|
if homeserver:
|
||||||
|
@ -43,16 +40,22 @@ class LatencyModule(niobot.Module):
|
||||||
end_resolv = time.perf_counter()
|
end_resolv = time.perf_counter()
|
||||||
resolv = round(end_resolv - start_resolv, 2)
|
resolv = round(end_resolv - start_resolv, 2)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await ctx.respond("\N{WARNING SIGN} Failed to resolve homeserver: " + str(e))
|
await ctx.respond(
|
||||||
|
"\N{WARNING SIGN} Failed to resolve homeserver: " + str(e)
|
||||||
|
)
|
||||||
homeserver = loc
|
homeserver = loc
|
||||||
resolv = -1
|
resolv = -1
|
||||||
|
|
||||||
async with httpx.AsyncClient(headers={"User-Agent": niobot.__user_agent__}) as client:
|
async with httpx.AsyncClient(
|
||||||
|
headers={"User-Agent": niobot.__user_agent__}
|
||||||
|
) as client:
|
||||||
timings = []
|
timings = []
|
||||||
for rnd in range(5):
|
for rnd in range(5):
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
res = await client.get(f"{homeserver}/_matrix/federation/v1/version")
|
res = await client.get(
|
||||||
|
f"{homeserver}/_matrix/federation/v1/version"
|
||||||
|
)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError:
|
||||||
timings.append(-1)
|
timings.append(-1)
|
||||||
|
@ -70,9 +73,6 @@ class LatencyModule(niobot.Module):
|
||||||
hs_target = homeserver[8:]
|
hs_target = homeserver[8:]
|
||||||
return await ctx.respond(
|
return await ctx.respond(
|
||||||
"Latency: {:,.2f}ms ({:,.2f}ms to resolve homeserver, federation latency to {!r}: {:,.2f}ms)".format(
|
"Latency: {:,.2f}ms ({:,.2f}ms to resolve homeserver, federation latency to {!r}: {:,.2f}ms)".format(
|
||||||
latency,
|
latency, resolv * 1000, hs_target, fed_latency
|
||||||
resolv * 1000,
|
|
||||||
hs_target,
|
|
||||||
fed_latency
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,78 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from pathlib import Path
|
||||||
import niobot
|
import niobot
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from ..main import NonsenseBot
|
||||||
|
|
||||||
|
|
||||||
class MSCGetter(niobot.Module):
|
class MSCGetter(niobot.Module):
|
||||||
|
bot: NonsenseBot
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
super().__init__(bot)
|
||||||
|
self.latest_msc = None
|
||||||
|
self.msc_cache = Path.cwd() / ".msc-cache"
|
||||||
|
self.msc_cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
async def get_msc_with_cache(self, number: int):
|
||||||
|
if number not in range(1, 10_000):
|
||||||
|
return {"error": "Invalid MSC ID"}
|
||||||
|
|
||||||
|
file = self.msc_cache / ("%d.json" % number)
|
||||||
|
content = None
|
||||||
|
if file.exists():
|
||||||
|
try:
|
||||||
|
content = json.loads(file.read_text("utf-8", "replace"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
file.unlink()
|
||||||
|
|
||||||
|
if content:
|
||||||
|
return content
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(
|
||||||
|
"https://api.github.com/repos/matrix-org/matrix-spec-proposals/pulls/%d"
|
||||||
|
% number
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return {"error": "HTTP %d" % response.status_code}
|
||||||
|
content = response.json()
|
||||||
|
file.write_text(json.dumps(content))
|
||||||
|
return content
|
||||||
|
|
||||||
|
@niobot.event("message")
|
||||||
|
async def on_message(self, room: niobot.MatrixRoom, message: niobot.RoomMessage):
|
||||||
|
if self.bot.is_old(message):
|
||||||
|
return
|
||||||
|
if message.sender == self.bot.user:
|
||||||
|
return
|
||||||
|
if not isinstance(message, niobot.RoomMessageText):
|
||||||
|
return
|
||||||
|
if "m.in_reply_to" in message.source.get("m.relates_to", []):
|
||||||
|
return
|
||||||
|
if await self.bot.redis.get(
|
||||||
|
self.bot.redis_key(room.room_id, "auto_msc.enabled")
|
||||||
|
):
|
||||||
|
matches = re.finditer("[MmSsCc]\W?([0-9]{1,4})", message.body)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for m in matches:
|
||||||
|
no = m.group(1)
|
||||||
|
if no:
|
||||||
|
data = await self.get_msc_with_cache(int(no))
|
||||||
|
if data.get("error"):
|
||||||
|
continue
|
||||||
|
lines.append(f"[{data['title']}]({data['html_url']})")
|
||||||
|
if lines:
|
||||||
|
return await self.bot.send_message(
|
||||||
|
room, "\n".join((f"* {ln}" for ln in lines)), reply_to=message
|
||||||
|
)
|
||||||
|
|
||||||
@niobot.command()
|
@niobot.command()
|
||||||
async def msc(self, ctx: niobot.Context, number: str):
|
async def msc(self, ctx: niobot.Context, number: str):
|
||||||
"""Fetches the given MSC"""
|
"""Fetches the given MSC"""
|
||||||
|
@ -9,8 +80,39 @@ class MSCGetter(niobot.Module):
|
||||||
number = number[3:]
|
number = number[3:]
|
||||||
elif number.startswith("#"):
|
elif number.startswith("#"):
|
||||||
number = number[1:]
|
number = number[1:]
|
||||||
if not number.isdigit():
|
if not number.isdigit() or len(number) != 4:
|
||||||
return await ctx.respond("Invalid MXC number.")
|
return await ctx.respond("Invalid MXC number.")
|
||||||
return await ctx.respond(
|
|
||||||
"https://github.com/matrix-org/matrix-spec-proposals/pull/" + number
|
data: dict = await self.get_msc_with_cache(int(number))
|
||||||
|
if data.get("error"):
|
||||||
|
return await ctx.respond(data["error"])
|
||||||
|
return await ctx.respond(f"[{data['title']}]({data['html_url']})")
|
||||||
|
|
||||||
|
@niobot.command("automsc.enable")
|
||||||
|
@niobot.sender_has_power(50)
|
||||||
|
async def auto_msc_enable(self, ctx: niobot.Context):
|
||||||
|
"""Automatically enables MSC linking. Requires a power level of at least 50."""
|
||||||
|
key = self.bot.redis_key(ctx.room.room_id, "auto_msc.enabled")
|
||||||
|
exists = await self.bot.redis.get(key)
|
||||||
|
if exists:
|
||||||
|
return await ctx.respond("AutoMSC is already enabled in this room.")
|
||||||
|
await self.bot.redis.set(key, 1)
|
||||||
|
await self.bot.redis.save()
|
||||||
|
return await self.bot.add_reaction(
|
||||||
|
ctx.room, ctx.message, "\N{WHITE HEAVY CHECK MARK}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@niobot.command("automsc.disable")
|
||||||
|
@niobot.sender_has_power(50)
|
||||||
|
async def auto_msc_disable(self, ctx: niobot.Context):
|
||||||
|
"""Disables automatic MSC linking. Requires a power level of at least 50."""
|
||||||
|
key = self.bot.redis_key(ctx.room.room_id, "auto_msc.enabled")
|
||||||
|
exists = await self.bot.redis.get(key)
|
||||||
|
if exists:
|
||||||
|
await self.bot.redis.delete(key)
|
||||||
|
await self.bot.redis.save()
|
||||||
|
await self.bot.add_reaction(
|
||||||
|
ctx.room, ctx.message, "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await ctx.respond("AutoMSC is already disabled in this room.")
|
||||||
|
|
|
@ -7,6 +7,7 @@ clients cannot play.
|
||||||
|
|
||||||
This can easily be adapted to other rooms and users, or even to other types of media, by changing the USER_IDS list.
|
This can easily be adapted to other rooms and users, or even to other types of media, by changing the USER_IDS list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -20,11 +21,7 @@ from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class TruthSocialTranscode(niobot.Module):
|
class TruthSocialTranscode(niobot.Module):
|
||||||
USER_IDS = {
|
USER_IDS = {"@trumpbot:shronk.net", "@tatebot:shronk.net", "@nex:nexy7574.co.uk"}
|
||||||
"@trumpbot:shronk.net",
|
|
||||||
"@tatebot:shronk.net",
|
|
||||||
"@nex:nexy7574.co.uk"
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, bot: niobot.NioBot):
|
def __init__(self, bot: niobot.NioBot):
|
||||||
super().__init__(bot)
|
super().__init__(bot)
|
||||||
|
@ -43,7 +40,9 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
self.bot.event_callbacks.remove(callback)
|
self.bot.event_callbacks.remove(callback)
|
||||||
super().__teardown__()
|
super().__teardown__()
|
||||||
|
|
||||||
async def on_message_internal(self, room: niobot.MatrixRoom, event: niobot.RoomMessageVideo) -> str:
|
async def on_message_internal(
|
||||||
|
self, room: niobot.MatrixRoom, event: niobot.RoomMessageVideo
|
||||||
|
) -> str:
|
||||||
self.log.info("Processing event: %r (%r)", event, room)
|
self.log.info("Processing event: %r (%r)", event, room)
|
||||||
# if not isinstance(event, niobot.RoomMessageVideo):
|
# if not isinstance(event, niobot.RoomMessageVideo):
|
||||||
# # We are not interested in non-videos.
|
# # We are not interested in non-videos.
|
||||||
|
@ -58,7 +57,9 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
# Videos sent in the truth room are not encrypted, so that makes this easier.
|
# Videos sent in the truth room are not encrypted, so that makes this easier.
|
||||||
http_url = await self.bot.mxc_to_http(
|
http_url = await self.bot.mxc_to_http(
|
||||||
event.url,
|
event.url,
|
||||||
homeserver=event.sender.split(":", 1)[1] # use the sender homeserver for download
|
homeserver=event.sender.split(":", 1)[
|
||||||
|
1
|
||||||
|
], # use the sender homeserver for download
|
||||||
)
|
)
|
||||||
http_url = "https://" + http_url
|
http_url = "https://" + http_url
|
||||||
name = event.flattened()["content.filename"]
|
name = event.flattened()["content.filename"]
|
||||||
|
@ -73,26 +74,35 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
await self.bot.send_message(
|
await self.bot.send_message(
|
||||||
room,
|
room,
|
||||||
"Error downloading video, unable to transcode: %r" % dl,
|
"Error downloading video, unable to transcode: %r" % dl,
|
||||||
reply_to=event.event_id
|
reply_to=event.event_id,
|
||||||
)
|
)
|
||||||
return "ERROR_DOWNLOAD"
|
return "ERROR_DOWNLOAD"
|
||||||
|
|
||||||
# First, probe the video to see if it has H265 content in it.
|
# First, probe the video to see if it has H265 content in it.
|
||||||
probe = await asyncio.create_subprocess_exec(
|
probe = await asyncio.create_subprocess_exec(
|
||||||
"ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
|
"ffprobe",
|
||||||
"stream=codec_name", "-of", "default=noprint_wrappers=1:nokey=1",
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=codec_name",
|
||||||
|
"-of",
|
||||||
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
temp_fd.name,
|
temp_fd.name,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
stdout, stderr = await probe.communicate()
|
stdout, stderr = await probe.communicate()
|
||||||
if probe.returncode != 0:
|
if probe.returncode != 0:
|
||||||
# Something went wrong, let's not continue.
|
# Something went wrong, let's not continue.
|
||||||
self.log.error("Error probing video:\n%s", (stderr or b"EOF").decode("utf-8"))
|
self.log.error(
|
||||||
|
"Error probing video:\n%s", (stderr or b"EOF").decode("utf-8")
|
||||||
|
)
|
||||||
await self.bot.send_message(
|
await self.bot.send_message(
|
||||||
room,
|
room,
|
||||||
"Error probing video, unable to determine whether a transcode is required.",
|
"Error probing video, unable to determine whether a transcode is required.",
|
||||||
reply_to=event.event_id
|
reply_to=event.event_id,
|
||||||
)
|
)
|
||||||
return "ERROR_PROBE"
|
return "ERROR_PROBE"
|
||||||
|
|
||||||
|
@ -107,12 +117,10 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
# compression.
|
# compression.
|
||||||
# We will also limit the bitrate to about 5 megabit, because Trump has a habit of posting raw footage
|
# We will also limit the bitrate to about 5 megabit, because Trump has a habit of posting raw footage
|
||||||
# that ends up being something stupid like 20 megabit at 60fps.
|
# that ends up being something stupid like 20 megabit at 60fps.
|
||||||
with tempfile.NamedTemporaryFile(suffix="-transcoded-" + name + ".mp4") as out_fd:
|
with tempfile.NamedTemporaryFile(
|
||||||
self.log.info(
|
suffix="-transcoded-" + name + ".mp4"
|
||||||
"transcoding %s -> %s",
|
) as out_fd:
|
||||||
temp_fd.name,
|
self.log.info("transcoding %s -> %s", temp_fd.name, out_fd.name)
|
||||||
out_fd.name
|
|
||||||
)
|
|
||||||
args = [
|
args = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-v",
|
"-v",
|
||||||
|
@ -148,12 +156,12 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
# "vfr",
|
# "vfr",
|
||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
"mp4",
|
||||||
out_fd.name
|
out_fd.name,
|
||||||
]
|
]
|
||||||
transcode = await asyncio.create_subprocess_exec(
|
transcode = await asyncio.create_subprocess_exec(
|
||||||
*args,
|
*args,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
self.log.info("Starting transcode...")
|
self.log.info("Starting transcode...")
|
||||||
self.log.debug("Running command: %r", shlex.join(args))
|
self.log.debug("Running command: %r", shlex.join(args))
|
||||||
|
@ -163,21 +171,20 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
self.log.error(
|
self.log.error(
|
||||||
"Error transcoding video:\n%s\n\n%s",
|
"Error transcoding video:\n%s\n\n%s",
|
||||||
stderr.decode("utf-8"),
|
stderr.decode("utf-8"),
|
||||||
stdout.decode("utf-8")
|
stdout.decode("utf-8"),
|
||||||
)
|
)
|
||||||
await self.bot.send_message(
|
await self.bot.send_message(
|
||||||
room,
|
room,
|
||||||
"Error transcoding video, unable to transcode: got return code %d" % transcode.returncode,
|
"Error transcoding video, unable to transcode: got return code %d"
|
||||||
reply_to=event.event_id
|
% transcode.returncode,
|
||||||
|
reply_to=event.event_id,
|
||||||
)
|
)
|
||||||
return "ERROR_TRANSCODE"
|
return "ERROR_TRANSCODE"
|
||||||
# We transcoded! Now we need to upload it.
|
# We transcoded! Now we need to upload it.
|
||||||
file = await niobot.VideoAttachment.from_file(out_fd.name)
|
file = await niobot.VideoAttachment.from_file(out_fd.name)
|
||||||
self.log.info("Uploading transcoded video")
|
self.log.info("Uploading transcoded video")
|
||||||
await self.bot.send_message(
|
await self.bot.send_message(
|
||||||
room.room_id,
|
room.room_id, file=file, reply_to=event.event_id
|
||||||
file=file,
|
|
||||||
reply_to=event.event_id
|
|
||||||
)
|
)
|
||||||
self.log.info("Uploaded transcoded video")
|
self.log.info("Uploaded transcoded video")
|
||||||
return "OK"
|
return "OK"
|
||||||
|
@ -191,18 +198,31 @@ class TruthSocialTranscode(niobot.Module):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_allowed(ctx: niobot.Context) -> bool:
|
def is_allowed(ctx: niobot.Context) -> bool:
|
||||||
return ctx.message.sender.split(":", 1)[1] in {"shronk.net", "nicroxio.co.uk", "nexy7574.co.uk"}
|
return ctx.message.sender.split(":", 1)[1] in {
|
||||||
|
"shronk.net",
|
||||||
|
"nicroxio.co.uk",
|
||||||
|
"nexy7574.co.uk",
|
||||||
|
}
|
||||||
|
|
||||||
@niobot.command("transcode")
|
@niobot.command("transcode")
|
||||||
@niobot.from_homeserver("nexy7574.co.uk", "shronk.net", "nicroxio.co.uk", "transgender.ing")
|
@niobot.from_homeserver(
|
||||||
async def do_transcode(self, ctx: niobot.Context, message_link: typing.Annotated[niobot.RoomMessage, niobot.EventParser("m.room.message")]):
|
"nexy7574.co.uk", "shronk.net", "nicroxio.co.uk", "transgender.ing"
|
||||||
|
)
|
||||||
|
async def do_transcode(
|
||||||
|
self,
|
||||||
|
ctx: niobot.Context,
|
||||||
|
message_link: typing.Annotated[
|
||||||
|
niobot.RoomMessage, niobot.EventParser("m.room.message")
|
||||||
|
],
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Transcodes a video to H264/AAC.
|
Transcodes a video to H264/AAC.
|
||||||
"""
|
"""
|
||||||
if not isinstance(message_link, niobot.RoomMessageVideo):
|
if not isinstance(message_link, niobot.RoomMessageVideo):
|
||||||
await ctx.respond("That's not a video. %r" % message_link)
|
await ctx.respond("That's not a video. %r" % message_link)
|
||||||
return
|
return
|
||||||
res = await ctx.respond("\N{hourglass} Dispatching, please wait...")
|
res = await ctx.respond("\N{HOURGLASS} Dispatching, please wait...")
|
||||||
ret = await self.on_message(ctx.room, message_link)
|
ret = await self.on_message(ctx.room, message_link)
|
||||||
await res.edit(f"\N{white heavy check mark} Transcode complete. Result: `{ret}`")
|
await res.edit(
|
||||||
|
f"\N{WHITE HEAVY CHECK MARK} Transcode complete. Result: `{ret}`"
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Displays versions for the bot and its dependencies
|
Displays versions for the bot and its dependencies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import niobot
|
import niobot
|
||||||
import asyncio
|
import asyncio
|
||||||
import tomllib
|
import tomllib
|
||||||
|
@ -15,11 +16,7 @@ class VersionModule(niobot.Module):
|
||||||
pipfile = tomllib.load(pipfile_fd)
|
pipfile = tomllib.load(pipfile_fd)
|
||||||
|
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
"git",
|
"git", "rev-parse", "--short", "HEAD", stdout=asyncio.subprocess.PIPE
|
||||||
"rev-parse",
|
|
||||||
"--short",
|
|
||||||
"HEAD",
|
|
||||||
stdout=asyncio.subprocess.PIPE
|
|
||||||
)
|
)
|
||||||
stdout, _ = await process.communicate()
|
stdout, _ = await process.communicate()
|
||||||
git_version = stdout.decode().strip()
|
git_version = stdout.decode().strip()
|
||||||
|
@ -28,9 +25,6 @@ class VersionModule(niobot.Module):
|
||||||
for dependency in pipfile["packages"].keys():
|
for dependency in pipfile["packages"].keys():
|
||||||
dep_version = version(dependency)
|
dep_version = version(dependency)
|
||||||
lines.append(
|
lines.append(
|
||||||
"* {0}: [{1}](https://pypi.org/p/{0})".format(
|
"* {0}: [{1}](https://pypi.org/p/{0})".format(dependency, dep_version)
|
||||||
dependency,
|
|
||||||
dep_version
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return await ctx.respond("\n".join(lines))
|
return await ctx.respond("\n".join(lines))
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
What it says on the tin - a command wrapper around yt-dlp.
|
What it says on the tin - a command wrapper around yt-dlp.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import math
|
import math
|
||||||
|
@ -94,7 +95,9 @@ class YoutubeDLModule(niobot.Module):
|
||||||
str(new_file),
|
str(new_file),
|
||||||
]
|
]
|
||||||
self.log.debug("Running command: ffmpeg %s", " ".join(args))
|
self.log.debug("Running command: ffmpeg %s", " ".join(args))
|
||||||
process = subprocess.run(["ffmpeg", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
process = subprocess.run(
|
||||||
|
["ffmpeg", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise RuntimeError(process.stderr.decode())
|
raise RuntimeError(process.stderr.decode())
|
||||||
return new_file
|
return new_file
|
||||||
|
@ -102,11 +105,15 @@ class YoutubeDLModule(niobot.Module):
|
||||||
return await asyncio.to_thread(inner)
|
return await asyncio.to_thread(inner)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def upload_to_0x0(name: str, data: typing.IO[bytes], mime_type: str | None = None) -> str:
|
async def upload_to_0x0(
|
||||||
|
name: str, data: typing.IO[bytes], mime_type: str | None = None
|
||||||
|
) -> str:
|
||||||
if not mime_type:
|
if not mime_type:
|
||||||
import magic
|
import magic
|
||||||
|
|
||||||
mime_type = await asyncio.to_thread(magic.from_buffer, data.read(4096), mime=True)
|
mime_type = await asyncio.to_thread(
|
||||||
|
magic.from_buffer, data.read(4096), mime=True
|
||||||
|
)
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
|
@ -123,7 +130,11 @@ class YoutubeDLModule(niobot.Module):
|
||||||
|
|
||||||
@niobot.command()
|
@niobot.command()
|
||||||
async def ytdl(
|
async def ytdl(
|
||||||
self, ctx: niobot.Context, url: str, snip: Optional[str] = None, download_format: Optional[str] = None
|
self,
|
||||||
|
ctx: niobot.Context,
|
||||||
|
url: str,
|
||||||
|
snip: Optional[str] = None,
|
||||||
|
download_format: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Downloads a video from YouTube or other source
|
Downloads a video from YouTube or other source
|
||||||
|
@ -161,7 +172,9 @@ class YoutubeDLModule(niobot.Module):
|
||||||
await response.edit(content="Fetching metadata (step 1/10)")
|
await response.edit(content="Fetching metadata (step 1/10)")
|
||||||
try:
|
try:
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
extracted_info = await asyncio.to_thread(downloader.extract_info, url, download=False)
|
extracted_info = await asyncio.to_thread(
|
||||||
|
downloader.extract_info, url, download=False
|
||||||
|
)
|
||||||
except DownloadError as e:
|
except DownloadError as e:
|
||||||
extracted_info = {
|
extracted_info = {
|
||||||
"title": "error",
|
"title": "error",
|
||||||
|
@ -185,22 +198,38 @@ class YoutubeDLModule(niobot.Module):
|
||||||
title = textwrap.shorten(title, 100)
|
title = textwrap.shorten(title, 100)
|
||||||
webpage_url = extracted_info.get("webpage_url", url)
|
webpage_url = extracted_info.get("webpage_url", url)
|
||||||
|
|
||||||
chosen_format = extracted_info.get("format") or chosen_format or str(uuid.uuid4())
|
chosen_format = (
|
||||||
chosen_format_id = extracted_info.get("format_id") or str(uuid.uuid4())
|
extracted_info.get("format")
|
||||||
|
or chosen_format
|
||||||
|
or str(uuid.uuid4())
|
||||||
|
)
|
||||||
|
chosen_format_id = extracted_info.get("format_id") or str(
|
||||||
|
uuid.uuid4()
|
||||||
|
)
|
||||||
final_extension = extracted_info.get("ext") or "mp4"
|
final_extension = extracted_info.get("ext") or "mp4"
|
||||||
format_note = extracted_info.get("format_note", "%s (%s)" % (chosen_format, chosen_format_id)) or ""
|
format_note = (
|
||||||
|
extracted_info.get(
|
||||||
|
"format_note", "%s (%s)" % (chosen_format, chosen_format_id)
|
||||||
|
)
|
||||||
|
or ""
|
||||||
|
)
|
||||||
resolution = extracted_info.get("resolution") or "1x1"
|
resolution = extracted_info.get("resolution") or "1x1"
|
||||||
fps = extracted_info.get("fps", 0.0) or 0.0
|
fps = extracted_info.get("fps", 0.0) or 0.0
|
||||||
vcodec = extracted_info.get("vcodec") or "h264"
|
vcodec = extracted_info.get("vcodec") or "h264"
|
||||||
acodec = extracted_info.get("acodec") or "aac"
|
acodec = extracted_info.get("acodec") or "aac"
|
||||||
filesize = extracted_info.get("filesize", extracted_info.get("filesize_approx", 1))
|
filesize = extracted_info.get(
|
||||||
likes = extracted_info.get("like_count", extracted_info.get("average_rating", 0))
|
"filesize", extracted_info.get("filesize_approx", 1)
|
||||||
|
)
|
||||||
|
likes = extracted_info.get(
|
||||||
|
"like_count", extracted_info.get("average_rating", 0)
|
||||||
|
)
|
||||||
views = extracted_info.get("view_count", 0)
|
views = extracted_info.get("view_count", 0)
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
if chosen_format and chosen_format_id:
|
if chosen_format and chosen_format_id:
|
||||||
lines.append(
|
lines.append(
|
||||||
"* Chosen format: `%s` (`%s`)" % (chosen_format, chosen_format_id),
|
"* Chosen format: `%s` (`%s`)"
|
||||||
|
% (chosen_format, chosen_format_id),
|
||||||
)
|
)
|
||||||
if format_note:
|
if format_note:
|
||||||
lines.append("* Format note: %r" % format_note)
|
lines.append("* Format note: %r" % format_note)
|
||||||
|
@ -225,14 +254,18 @@ class YoutubeDLModule(niobot.Module):
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.to_thread(functools.partial(downloader.download, [url]))
|
await asyncio.to_thread(
|
||||||
|
functools.partial(downloader.download, [url])
|
||||||
|
)
|
||||||
except DownloadError as e:
|
except DownloadError as e:
|
||||||
logging.error(e, exc_info=True)
|
logging.error(e, exc_info=True)
|
||||||
return await response.edit(
|
return await response.edit(
|
||||||
f"# Error!\n\nDownload failed:\n```\n{e}\n```",
|
f"# Error!\n\nDownload failed:\n```\n{e}\n```",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
file: Path = next(temp_dir.glob("*." + extracted_info.get("ext", "*")))
|
file: Path = next(
|
||||||
|
temp_dir.glob("*." + extracted_info.get("ext", "*"))
|
||||||
|
)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
ext = extracted_info.get("ext", "*")
|
ext = extracted_info.get("ext", "*")
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
|
@ -252,7 +285,9 @@ class YoutubeDLModule(niobot.Module):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
trim_start, trim_end = snip, None
|
trim_start, trim_end = snip, None
|
||||||
trim_start = trim_start or "00:00:00"
|
trim_start = trim_start or "00:00:00"
|
||||||
trim_end = trim_end or extracted_info.get("duration_string", "00:30:00")
|
trim_end = trim_end or extracted_info.get(
|
||||||
|
"duration_string", "00:30:00"
|
||||||
|
)
|
||||||
new_file = temp_dir / ("output" + file.suffix)
|
new_file = temp_dir / ("output" + file.suffix)
|
||||||
args = [
|
args = [
|
||||||
"-hwaccel",
|
"-hwaccel",
|
||||||
|
@ -308,7 +343,9 @@ class YoutubeDLModule(niobot.Module):
|
||||||
|
|
||||||
size_megabits = (size_bytes * 8) / 1024 / 1024
|
size_megabits = (size_bytes * 8) / 1024 / 1024
|
||||||
eta_seconds = size_megabits / 20
|
eta_seconds = size_megabits / 20
|
||||||
await response.edit(content=f"Uploading (ETA: {humanize.naturaldelta(eta_seconds)})...")
|
await response.edit(
|
||||||
|
content=f"Uploading (ETA: {humanize.naturaldelta(eta_seconds)})..."
|
||||||
|
)
|
||||||
views = views or 0
|
views = views or 0
|
||||||
likes = likes or 0
|
likes = likes or 0
|
||||||
try:
|
try:
|
||||||
|
@ -329,7 +366,9 @@ class YoutubeDLModule(niobot.Module):
|
||||||
httpx.HTTPStatusError,
|
httpx.HTTPStatusError,
|
||||||
) as e:
|
) as e:
|
||||||
self.log.error(e, exc_info=True)
|
self.log.error(e, exc_info=True)
|
||||||
await response.edit(content=f"# Error\n\nUpload failed:\n```\n{e}\n```")
|
await response.edit(
|
||||||
|
content=f"# Error\n\nUpload failed:\n```\n{e}\n```"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await response.edit(
|
await response.edit(
|
||||||
content=f"# [Downloaded {title}!]({webpage_url})\n\nViews: {views:,} | Likes: {likes:,}"
|
content=f"# [Downloaded {title}!]({webpage_url})\n\nViews: {views:,} | Likes: {likes:,}"
|
||||||
|
|
Loading…
Reference in a new issue