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 os
import sys
from os.path import splitext
from time import sleep
import aiomqtt
import pytz
from discord import File, Intents
from discord.ext import commands
from discord_webhook import DiscordEmbed, DiscordWebhook
from discord_webhook import DiscordEmbed
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")
if not token:
print("DISCORD_TOKEN unset")
sys.exit(1)
_mqtt_host = None
webhook_url = os.getenv("DISCORD_WEBHOOK_URL")
if not webhook_url:
print("DISCORD_WEBHOOK_URL unset")
sys.exit(1)
def setup(bot, mqtt_host):
global _mqtt_host
_mqtt_host = mqtt_host
timezone = pytz.timezone("Europe/Amsterdam")
# Discord bot stuff
intents = Intents.default()
intents.message_content = True
intents.members = True
HobbyBot = commands.Bot(command_prefix="!", description="Bitlair Bot", intents=intents)
# !state
bot.command(description="Bitlair Space State")(state)
# !co2
bot.command(description="co2 levels")(co2)
# !temp
bot.command(description="Temperature")(temp)
# !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 with asyncio.timeout(timeout):
async with aiomqtt.Client(mqtt_host) as mq:
async with aiomqtt.Client(_mqtt_host) as mq:
await mq.subscribe(topic)
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 with ctx.typing():
try:
@ -67,8 +50,6 @@ async def state(ctx):
raise err
# !co2
@HobbyBot.command(description="co2 levels")
async def co2(ctx, where="hoofdruimte"):
async with ctx.typing():
try:
@ -79,8 +60,6 @@ async def co2(ctx, where="hoofdruimte"):
raise err
# !temp
@HobbyBot.command(description="Temperature")
async def temp(ctx, where="hoofdruimte"):
async with ctx.typing():
try:
@ -91,8 +70,6 @@ async def temp(ctx, where="hoofdruimte"):
raise err
# !humid
@HobbyBot.command(description="Humidity")
async def humid(ctx, where="hoofdruimte"):
async with ctx.typing():
try:
@ -103,39 +80,12 @@ async def humid(ctx, where="hoofdruimte"):
raise err
# !np
@HobbyBot.command(description="Now Playing")
async def np(ctx):
async with ctx.typing():
await ctx.reply("Now playing: Darude - Sandstorm")
# !bottleclip
@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():
async def run_events(mqtt_host):
retained = {
"bitlair/alarm",
"bitlair/photos",
@ -154,27 +104,15 @@ async def event_task():
payload = msg.payload.decode("ascii")
if msg.topic.matches("bitlair/alarm"):
webhook_message(f"Alarm: {payload}")
yield f"Alarm: {payload}"
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"):
webhook_message(f"DJO is now {payload.upper()}")
yield f"DJO is now {payload.upper()}"
elif msg.topic.matches("bitlair/photos"):
webhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True)
embed = DiscordEmbed(title="WIP Cam", color="fc5d1d")
embed.set_url(f"https://bitlair.nl/fotos/view/{payload}")
embed.set_image(f"https://bitlair.nl/fotos/photos/{payload}")
webhook.add_embed(embed)
webhook.execute()
yield embed
else:
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 subprocess
from os.path import abspath, join
from os.path import abspath, join, splitext
from tempfile import NamedTemporaryFile
from typing import List
from discord import File
def resource_dir() -> str:
p = os.getenv("BOTTLECLIP_RESOURCES")
@ -42,3 +44,21 @@ def create_stl(label: str, icon: str, ears: bool) -> NamedTemporaryFile:
stl.seek(0)
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())