Compare commits
6 commits
677d4f1259
...
704b623b29
Author | SHA1 | Date | |
---|---|---|---|
704b623b29 | |||
d067be38b4 | |||
2c8bda6151 | |||
24088f3ba8 | |||
5dccc2d4bb | |||
78f30b19fe |
8 changed files with 300 additions and 94 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/.ruff_cache
|
/.ruff_cache
|
||||||
/.venv
|
/.venv
|
||||||
|
__pycache__
|
||||||
|
|
116
main.py → commands/__init__.py
Executable file → Normal file
116
main.py → commands/__init__.py
Executable file → Normal file
|
@ -1,58 +1,41 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from os.path import splitext
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
import aiomqtt
|
import aiomqtt
|
||||||
import pytz
|
from discord_webhook import DiscordEmbed
|
||||||
from discord import File, Intents
|
|
||||||
from discord.ext import commands
|
|
||||||
from discord_webhook import DiscordEmbed, DiscordWebhook
|
|
||||||
|
|
||||||
import bottleclip as clip
|
import commands.bottleclip as bottleclip
|
||||||
|
|
||||||
mqtt_host = os.getenv("MQTT_HOST")
|
|
||||||
if not mqtt_host:
|
|
||||||
print("MQTT_HOST unset")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
token = os.getenv("DISCORD_TOKEN")
|
_mqtt_host = None
|
||||||
if not token:
|
|
||||||
print("DISCORD_TOKEN unset")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
webhook_url = os.getenv("DISCORD_WEBHOOK_URL")
|
def setup(bot, mqtt_host):
|
||||||
if not webhook_url:
|
global _mqtt_host
|
||||||
print("DISCORD_WEBHOOK_URL unset")
|
_mqtt_host = mqtt_host
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
timezone = pytz.timezone("Europe/Amsterdam")
|
# !state
|
||||||
|
bot.command(description="Bitlair Space State")(state)
|
||||||
# Discord bot stuff
|
# !co2
|
||||||
intents = Intents.default()
|
bot.command(description="co2 levels")(co2)
|
||||||
intents.message_content = True
|
# !temp
|
||||||
intents.members = True
|
bot.command(description="Temperature")(temp)
|
||||||
HobbyBot = commands.Bot(command_prefix="!", description="Bitlair Bot", intents=intents)
|
# !humid
|
||||||
|
bot.command(description="Humidity")(humid)
|
||||||
|
# !np
|
||||||
|
bot.command(description="Now Playing")(np)
|
||||||
|
# !bottleclip
|
||||||
|
bot.command(
|
||||||
|
name="bottleclip",
|
||||||
|
description="Generate a bottle-clip STL file suitable for printing",
|
||||||
|
)(bottleclip.command)
|
||||||
|
|
||||||
|
|
||||||
async def mqtt_get_one(topic, timeout=20):
|
async def mqtt_get_one(topic, timeout=20):
|
||||||
async with asyncio.timeout(timeout):
|
async with asyncio.timeout(timeout):
|
||||||
async with aiomqtt.Client(mqtt_host) as mq:
|
async with aiomqtt.Client(_mqtt_host) as mq:
|
||||||
await mq.subscribe(topic)
|
await mq.subscribe(topic)
|
||||||
return await anext(mq.messages)
|
return await anext(mq.messages)
|
||||||
|
|
||||||
|
|
||||||
# Define bot commands
|
|
||||||
@HobbyBot.event
|
|
||||||
async def on_ready():
|
|
||||||
print(f"Logged in as {HobbyBot.user} (ID: {HobbyBot.user.id})")
|
|
||||||
|
|
||||||
|
|
||||||
# !state
|
|
||||||
@HobbyBot.command(description="Bitlair Space State")
|
|
||||||
async def state(ctx):
|
async def state(ctx):
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
|
@ -67,8 +50,6 @@ async def state(ctx):
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
# !co2
|
|
||||||
@HobbyBot.command(description="co2 levels")
|
|
||||||
async def co2(ctx, where="hoofdruimte"):
|
async def co2(ctx, where="hoofdruimte"):
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
|
@ -79,8 +60,6 @@ async def co2(ctx, where="hoofdruimte"):
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
# !temp
|
|
||||||
@HobbyBot.command(description="Temperature")
|
|
||||||
async def temp(ctx, where="hoofdruimte"):
|
async def temp(ctx, where="hoofdruimte"):
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
|
@ -91,8 +70,6 @@ async def temp(ctx, where="hoofdruimte"):
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
# !humid
|
|
||||||
@HobbyBot.command(description="Humidity")
|
|
||||||
async def humid(ctx, where="hoofdruimte"):
|
async def humid(ctx, where="hoofdruimte"):
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
try:
|
try:
|
||||||
|
@ -103,39 +80,12 @@ async def humid(ctx, where="hoofdruimte"):
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
# !np
|
|
||||||
@HobbyBot.command(description="Now Playing")
|
|
||||||
async def np(ctx):
|
async def np(ctx):
|
||||||
async with ctx.typing():
|
async with ctx.typing():
|
||||||
await ctx.reply("Now playing: Darude - Sandstorm")
|
await ctx.reply("Now playing: Darude - Sandstorm")
|
||||||
|
|
||||||
|
|
||||||
# !bottleclip
|
async def run_events(mqtt_host):
|
||||||
@HobbyBot.command(description="Generate a bottle-clip STL file suitable for printing")
|
|
||||||
async def bottleclip(ctx, icon: str = "", ears: bool = False):
|
|
||||||
icons = clip.list_icons()
|
|
||||||
if icon not in icons:
|
|
||||||
await ctx.reply(
|
|
||||||
f"usage: `!bottleclip <icon> [<ears y|n>]`\n* `icon` must be one of {', '.join(icons)}"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
async with ctx.typing():
|
|
||||||
label = ctx.author.nick or ctx.author.global_name
|
|
||||||
stl_file = clip.create_stl(label, icon, ears)
|
|
||||||
|
|
||||||
icon_name, _ = splitext(icon)
|
|
||||||
with_ears = '_ears' if ears else ''
|
|
||||||
attach = File(stl_file.name, filename=f"{label}_{icon_name}{with_ears}.stl")
|
|
||||||
await ctx.reply("Ok! Hier is je flessenclip", file=attach)
|
|
||||||
|
|
||||||
|
|
||||||
def webhook_message(msg):
|
|
||||||
webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True, content=msg)
|
|
||||||
webhook.execute()
|
|
||||||
|
|
||||||
|
|
||||||
async def event_task():
|
|
||||||
retained = {
|
retained = {
|
||||||
"bitlair/alarm",
|
"bitlair/alarm",
|
||||||
"bitlair/photos",
|
"bitlair/photos",
|
||||||
|
@ -154,27 +104,15 @@ async def event_task():
|
||||||
payload = msg.payload.decode("ascii")
|
payload = msg.payload.decode("ascii")
|
||||||
|
|
||||||
if msg.topic.matches("bitlair/alarm"):
|
if msg.topic.matches("bitlair/alarm"):
|
||||||
webhook_message(f"Alarm: {payload}")
|
yield f"Alarm: {payload}"
|
||||||
elif msg.topic.matches("bitlair/state"):
|
elif msg.topic.matches("bitlair/state"):
|
||||||
webhook_message(f"Bitlair is now {payload.upper()}")
|
yield f"Bitlair is now {payload.upper()}"
|
||||||
elif msg.topic.matches("bitlair/state/djo"):
|
elif msg.topic.matches("bitlair/state/djo"):
|
||||||
webhook_message(f"DJO is now {payload.upper()}")
|
yield f"DJO is now {payload.upper()}"
|
||||||
elif msg.topic.matches("bitlair/photos"):
|
elif msg.topic.matches("bitlair/photos"):
|
||||||
webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True)
|
|
||||||
embed = DiscordEmbed(title="WIP Cam", color="fc5d1d")
|
embed = DiscordEmbed(title="WIP Cam", color="fc5d1d")
|
||||||
embed.set_url(f"https://bitlair.nl/fotos/view/{payload}")
|
embed.set_url(f"https://bitlair.nl/fotos/view/{payload}")
|
||||||
embed.set_image(f"https://bitlair.nl/fotos/photos/{payload}")
|
embed.set_image(f"https://bitlair.nl/fotos/photos/{payload}")
|
||||||
webhook.add_embed(embed)
|
yield embed
|
||||||
webhook.execute()
|
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
sleep(1) # Prevent triggering rate limits.
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
t1 = asyncio.create_task(HobbyBot.start(token))
|
|
||||||
t2 = asyncio.create_task(event_task())
|
|
||||||
await asyncio.gather(t1, t2)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
|
@ -1,9 +1,11 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from os.path import abspath, join
|
from os.path import abspath, join, splitext
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from discord import File
|
||||||
|
|
||||||
|
|
||||||
def resource_dir() -> str:
|
def resource_dir() -> str:
|
||||||
p = os.getenv("BOTTLECLIP_RESOURCES")
|
p = os.getenv("BOTTLECLIP_RESOURCES")
|
||||||
|
@ -42,3 +44,21 @@ def create_stl(label: str, icon: str, ears: bool) -> NamedTemporaryFile:
|
||||||
stl.seek(0)
|
stl.seek(0)
|
||||||
|
|
||||||
return stl
|
return stl
|
||||||
|
|
||||||
|
|
||||||
|
async def command(ctx, icon: str = "", ears: bool = False):
|
||||||
|
icons = list_icons()
|
||||||
|
if icon not in icons:
|
||||||
|
await ctx.reply(
|
||||||
|
f"usage: `!bottleclip <icon> [<ears y|n>]`\n* `icon` must be one of {', '.join(icons)}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with ctx.typing():
|
||||||
|
label = ctx.author.nick or ctx.author.global_name
|
||||||
|
stl_file = create_stl(label, icon, ears)
|
||||||
|
|
||||||
|
icon_name, _ = splitext(icon)
|
||||||
|
with_ears = "_ears" if ears else ""
|
||||||
|
attach = File(stl_file.name, filename=f"{label}_{icon_name}{with_ears}.stl")
|
||||||
|
await ctx.reply("Ok! Hier is je flessenclip", file=attach)
|
61
discordbot.py
Executable file
61
discordbot.py
Executable file
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from discord import Intents
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord_webhook import DiscordEmbed, DiscordWebhook
|
||||||
|
|
||||||
|
import commands as botcommands
|
||||||
|
|
||||||
|
mqtt_host = os.getenv("MQTT_HOST")
|
||||||
|
if not mqtt_host:
|
||||||
|
print("MQTT_HOST unset")
|
||||||
|
sys.exit(1)
|
||||||
|
token = os.getenv("DISCORD_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print("DISCORD_TOKEN unset")
|
||||||
|
sys.exit(1)
|
||||||
|
webhook_url = os.getenv("DISCORD_WEBHOOK_URL")
|
||||||
|
if not webhook_url:
|
||||||
|
print("DISCORD_WEBHOOK_URL unset")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
timezone = pytz.timezone("Europe/Amsterdam")
|
||||||
|
|
||||||
|
intents = Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
bot = commands.Bot(command_prefix="!", description="Bitlair Bot", intents=intents)
|
||||||
|
botcommands.setup(bot, mqtt_host)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
|
||||||
|
|
||||||
|
|
||||||
|
async def event_task():
|
||||||
|
async for message in botcommands.run_events(mqtt_host):
|
||||||
|
webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True)
|
||||||
|
if type(message) is str:
|
||||||
|
webhook.content = message
|
||||||
|
elif type(message) is DiscordEmbed:
|
||||||
|
webhook.add_embed(message)
|
||||||
|
else:
|
||||||
|
print(f"invalid message type: {str(message)}")
|
||||||
|
continue
|
||||||
|
webhook.execute()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
t1 = asyncio.create_task(bot.start(token))
|
||||||
|
t2 = asyncio.create_task(event_task())
|
||||||
|
await asyncio.gather(t1, t2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
161
ircbot.py
Executable file
161
ircbot.py
Executable file
|
@ -0,0 +1,161 @@
|
||||||
|
"""
|
||||||
|
This module implements an IRC bot that aims to be compatible with the discord.py API in order to
|
||||||
|
facilitate re-use of the commands for that bot.
|
||||||
|
|
||||||
|
The supported features are the subset of what is needed for this project and not more than that.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pydle
|
||||||
|
from discord_webhook import DiscordEmbed
|
||||||
|
|
||||||
|
import commands as botcommands
|
||||||
|
|
||||||
|
|
||||||
|
def command_args(handler, sig):
|
||||||
|
parse = []
|
||||||
|
for par in list(sig.parameters.values())[1:]:
|
||||||
|
if par.annotation in (str, inspect._empty):
|
||||||
|
parse.append(lambda v: v)
|
||||||
|
elif par.annotation is bool:
|
||||||
|
parse.append(lambda v: bool(v))
|
||||||
|
else:
|
||||||
|
raise Exception(f"unsuported type {par.annotation}")
|
||||||
|
|
||||||
|
async def _proxy(ctx):
|
||||||
|
args = shlex.split(ctx._message)[1:]
|
||||||
|
args = [parse[i](v) for i, v in enumerate(args)]
|
||||||
|
await handler(ctx, *args)
|
||||||
|
|
||||||
|
return _proxy
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordAuthor:
|
||||||
|
def __init__(self, nick):
|
||||||
|
self.nick = nick
|
||||||
|
self.global_name = nick
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordContext:
|
||||||
|
def __init__(self, bot, target, source, message):
|
||||||
|
self._bot = bot
|
||||||
|
self._target = target
|
||||||
|
self._source = source
|
||||||
|
self._message = message
|
||||||
|
self.author = DiscordAuthor(source)
|
||||||
|
|
||||||
|
def typing(self):
|
||||||
|
class NilTyping:
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *exc):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return NilTyping()
|
||||||
|
|
||||||
|
async def reply(self, m, file=None):
|
||||||
|
if file:
|
||||||
|
# TODO: Host the file somewhere so it can be downloaded.
|
||||||
|
m += f" {file.filename}"
|
||||||
|
|
||||||
|
lines = m.strip().split("\n")
|
||||||
|
lines = [f"{self._source}: {line}" for line in lines]
|
||||||
|
await self._bot.message(self._target, "\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordImplBot(pydle.Client):
|
||||||
|
def __init__(self, channel, nickname, *, push_messages=None, prefix="!"):
|
||||||
|
super().__init__(nickname, realname=nickname)
|
||||||
|
self._channel = channel
|
||||||
|
self._cmd_prefix = prefix
|
||||||
|
self._cmds = {}
|
||||||
|
self._push_messages = push_messages
|
||||||
|
self._push_messages_task = None
|
||||||
|
|
||||||
|
async def on_connect(self):
|
||||||
|
await self.join(self._channel)
|
||||||
|
if self._push_messages is not None:
|
||||||
|
self._push_messages_task = asyncio.create_task(self._handle_push_messages())
|
||||||
|
|
||||||
|
async def on_disconnect(self):
|
||||||
|
if self._push_messages_task is not None:
|
||||||
|
self._push_messages_task.cancel()
|
||||||
|
self._push_messages_task = None
|
||||||
|
|
||||||
|
async def _handle_push_messages(self):
|
||||||
|
async for message in self._push_messages():
|
||||||
|
if isinstance(message, DiscordEmbed):
|
||||||
|
await self.notice(self._channel, f"{message.title} {message.url}")
|
||||||
|
else:
|
||||||
|
await self.notice(self._channel, message)
|
||||||
|
|
||||||
|
async def on_message(self, target, source, message):
|
||||||
|
# Don't respond to our own messages, as this leads to a positive feedback loop.
|
||||||
|
if source == self.nickname:
|
||||||
|
return
|
||||||
|
if not message.startswith(self._cmd_prefix):
|
||||||
|
return
|
||||||
|
|
||||||
|
name = message.removeprefix(self._cmd_prefix).split(" ", 1)[0]
|
||||||
|
cmd_fn = self._cmds.get(name)
|
||||||
|
if not cmd_fn:
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx = DiscordContext(self, target, source, message)
|
||||||
|
await cmd_fn(ctx)
|
||||||
|
|
||||||
|
# Discord API: Register a new command
|
||||||
|
def command(self, *, name=None, description=None):
|
||||||
|
def _reg_cmd(handler):
|
||||||
|
nonlocal name
|
||||||
|
name = name or handler.__name__
|
||||||
|
|
||||||
|
sig = inspect.signature(handler)
|
||||||
|
if len(sig.parameters) == 1:
|
||||||
|
# Just the DiscordContext argument.
|
||||||
|
self._cmds[name] = handler
|
||||||
|
else:
|
||||||
|
self._cmds[name] = command_args(handler, sig)
|
||||||
|
|
||||||
|
return _reg_cmd
|
||||||
|
|
||||||
|
|
||||||
|
def main(*, server, channel, nick, mqtt_host):
|
||||||
|
def push_messages():
|
||||||
|
return botcommands.run_events(mqtt_host)
|
||||||
|
|
||||||
|
bot = DiscordImplBot(
|
||||||
|
channel,
|
||||||
|
nick,
|
||||||
|
push_messages=push_messages,
|
||||||
|
)
|
||||||
|
botcommands.setup(bot, mqtt_host)
|
||||||
|
bot.run(server, tls=True, tls_verify=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not (mqtt_host := os.getenv("MQTT_HOST")):
|
||||||
|
print("MQTT_HOST unset")
|
||||||
|
sys.exit(1)
|
||||||
|
if not (irc_server := os.getenv("IRC_SERVER")):
|
||||||
|
print("IRC_SERVER unset")
|
||||||
|
sys.exit(1)
|
||||||
|
if not (irc_channel := os.getenv("IRC_CHANNEL")):
|
||||||
|
print("IRC_CHANNEL unset")
|
||||||
|
sys.exit(1)
|
||||||
|
if not (irc_nick := os.getenv("IRC_NICK")):
|
||||||
|
print("IRC_NICK unset")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
main(
|
||||||
|
server=irc_server,
|
||||||
|
channel=irc_channel,
|
||||||
|
nick=irc_nick,
|
||||||
|
mqtt_host=mqtt_host,
|
||||||
|
)
|
|
@ -8,5 +8,9 @@ dependencies = [
|
||||||
"aiomqtt>=2.4.0",
|
"aiomqtt>=2.4.0",
|
||||||
"discord-py>=2.5.2",
|
"discord-py>=2.5.2",
|
||||||
"discord-webhook>=1.4.1",
|
"discord-webhook>=1.4.1",
|
||||||
|
"pydle[sasl]",
|
||||||
"pytz>=2025.2",
|
"pytz>=2025.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
pydle = { git = "https://github.com/TheHolyRoger/pydle", branch = "fix-py10" }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv export --format requirements-txt
|
# uv export --format=requirements.txt
|
||||||
aiohappyeyeballs==2.6.1 \
|
aiohappyeyeballs==2.6.1 \
|
||||||
--hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \
|
--hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \
|
||||||
--hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8
|
--hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8
|
||||||
|
@ -38,9 +38,7 @@ aiohttp==3.11.18 \
|
||||||
--hash=sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261 \
|
--hash=sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261 \
|
||||||
--hash=sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000 \
|
--hash=sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000 \
|
||||||
--hash=sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1
|
--hash=sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1
|
||||||
# via
|
# via discord-py
|
||||||
# discord-bot
|
|
||||||
# discord-py
|
|
||||||
aiomqtt==2.4.0 \
|
aiomqtt==2.4.0 \
|
||||||
--hash=sha256:721296e2b79df5f6c7c4dfc91700ae0166953a4127735c92637859619dbd84e4 \
|
--hash=sha256:721296e2b79df5f6c7c4dfc91700ae0166953a4127735c92637859619dbd84e4 \
|
||||||
--hash=sha256:ab0f18fc5b7ffaa57451c407417d674db837b00a9c7d953cccd02be64f046c17
|
--hash=sha256:ab0f18fc5b7ffaa57451c407417d674db837b00a9c7d953cccd02be64f046c17
|
||||||
|
@ -308,6 +306,11 @@ propcache==0.3.1 \
|
||||||
# via
|
# via
|
||||||
# aiohttp
|
# aiohttp
|
||||||
# yarl
|
# yarl
|
||||||
|
pure-sasl==0.6.2 \
|
||||||
|
--hash=sha256:53c1355f5da95e2b85b2cc9a6af435518edc20c81193faa0eea65fdc835138f4
|
||||||
|
# via pydle
|
||||||
|
pydle @ git+https://github.com/TheHolyRoger/pydle@befd10fb340a1934fbdbc9c8ccaca68620cba0f6
|
||||||
|
# via discord-bot
|
||||||
pytz==2025.2 \
|
pytz==2025.2 \
|
||||||
--hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
|
--hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
|
||||||
--hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
|
--hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
|
||||||
|
|
18
uv.lock
generated
18
uv.lock
generated
|
@ -189,6 +189,7 @@ dependencies = [
|
||||||
{ name = "aiomqtt" },
|
{ name = "aiomqtt" },
|
||||||
{ name = "discord-py" },
|
{ name = "discord-py" },
|
||||||
{ name = "discord-webhook" },
|
{ name = "discord-webhook" },
|
||||||
|
{ name = "pydle", extra = ["sasl"] },
|
||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -197,6 +198,7 @@ requires-dist = [
|
||||||
{ name = "aiomqtt", specifier = ">=2.4.0" },
|
{ name = "aiomqtt", specifier = ">=2.4.0" },
|
||||||
{ name = "discord-py", specifier = ">=2.5.2" },
|
{ name = "discord-py", specifier = ">=2.5.2" },
|
||||||
{ name = "discord-webhook", specifier = ">=1.4.1" },
|
{ name = "discord-webhook", specifier = ">=1.4.1" },
|
||||||
|
{ name = "pydle", extras = ["sasl"], git = "https://github.com/TheHolyRoger/pydle?branch=fix-py10" },
|
||||||
{ name = "pytz", specifier = ">=2025.2" },
|
{ name = "pytz", specifier = ">=2025.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -420,6 +422,22 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pure-sasl"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/83/b7/a0d688f86c869073cc28c0640899394a1cf68a6d87ee78a09565e9037da6/pure-sasl-0.6.2.tar.gz", hash = "sha256:53c1355f5da95e2b85b2cc9a6af435518edc20c81193faa0eea65fdc835138f4", size = 11617, upload-time = "2019-10-14T21:43:57.13Z" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydle"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = { git = "https://github.com/TheHolyRoger/pydle?branch=fix-py10#befd10fb340a1934fbdbc9c8ccaca68620cba0f6" }
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
sasl = [
|
||||||
|
{ name = "pure-sasl" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue