Separate Discord API and command implementations

This commit is contained in:
polyfloyd 2025-05-16 20:04:18 +02:00
parent 677d4f1259
commit 78f30b19fe
3 changed files with 109 additions and 90 deletions

116
main.py → commands/__init__.py Executable file → Normal file
View 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())

View file

@ -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
View 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())