Compare commits
No commits in common. "1b925293735fea0b33b12c277b5ed57dab64888c" and "2d3b3573075d13a1dd16c69fc549fc004f0512fd" have entirely different histories.
1b92529373
...
2d3b357307
14 changed files with 6 additions and 1393 deletions
|
@ -1,7 +1,7 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||
{
|
||||
"name": "LEVbot",
|
||||
"name": "Python 3",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..",
|
||||
|
@ -9,7 +9,7 @@
|
|||
// Update 'VARIANT' to pick a Python version.
|
||||
// Append -bookworm, -bullseye or -buster to pin to an OS version.
|
||||
// Use -bookworm or -bullseye variants on arm64/Apple Silicon.
|
||||
"VARIANT": "3.11",
|
||||
"VARIANT": "3.12",
|
||||
// Options
|
||||
"NODE_VERSION": "none"
|
||||
}
|
||||
|
|
2
.flake8
2
.flake8
|
@ -1,4 +1,4 @@
|
|||
[flake8]
|
||||
max-line-length = 80
|
||||
select = C,E,F,I,W,B,B950
|
||||
extend-ignore = E203, E501, W503
|
||||
extend-ignore = E203, E501
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -158,5 +158,3 @@ cython_debug/
|
|||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
lenaverse-bot.toml
|
||||
|
|
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
|
||||
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
|
||||
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Lenaverse Bot",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "lenaverse_bot.main",
|
||||
"justMyCode": true,
|
||||
"env": {
|
||||
"LOG_LEVEL": "DEBUG",
|
||||
"CONFIG_PATH": "lenaverse-bot.toml",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Lenaisten.de
|
||||
Copyright (c) 2023 Yavook!de
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
# lenaverse-bot
|
||||
# vscode-python3
|
||||
|
||||
lenaverse-bot ist ein Discord-Bot, der den Lenaisten e.V. im LENAVERSE Discord vertritt.
|
||||
|
||||
Berechtigte Mitglieder können über den `/post`-Befehl Ankündigungen in ausgewählten Channels veröffentlichen.
|
||||
Außerdem gibt es einige öffentliche Befehle, mit denen die Benutzer Aktuelles über den Verein erfahren können.
|
||||
|
||||
Allen Befehlen kann ein Präfix vorangestellt werden, sodass `/post` zB. als `/ev_post`.
|
||||
Use this template to jumpstart python development using Visual Studio Code!
|
|
@ -1,9 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
import discord
|
||||
|
||||
discord.utils.setup_logging()
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.setLevel(os.getenv("LOG_LEVEL", logging.WARN))
|
|
@ -1,25 +0,0 @@
|
|||
import logging
|
||||
|
||||
import discord
|
||||
|
||||
from . import post, verein
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LenaverseBot(discord.Client):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(intents=discord.Intents.default())
|
||||
|
||||
self.tree = discord.app_commands.CommandTree(self)
|
||||
commands = post.COMMANDS + verein.COMMANDS
|
||||
for command in commands:
|
||||
self.tree.add_command(command)
|
||||
|
||||
async def setup_hook(self):
|
||||
await self.tree.sync()
|
||||
_logger.info("Commands synced")
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
assert self.user is not None
|
||||
_logger.info(f"{self.user.name} has connected to Discord!")
|
|
@ -1,91 +0,0 @@
|
|||
import os
|
||||
import tomllib
|
||||
from io import BytesIO
|
||||
from typing import Annotated, Self
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from pydantic import BaseModel, StringConstraints
|
||||
|
||||
StrippedStr = Annotated[str, StringConstraints(strip_whitespace=True)]
|
||||
|
||||
|
||||
class Post(BaseModel):
|
||||
# users authorized to posts
|
||||
users: list[int]
|
||||
# where to put posts
|
||||
channel: int
|
||||
|
||||
def get_channel(
|
||||
self,
|
||||
client: discord.Client,
|
||||
) -> discord.Thread | discord.TextChannel:
|
||||
"""
|
||||
Zielkanal für Posts finden
|
||||
"""
|
||||
|
||||
channel = client.get_channel(self.channel)
|
||||
assert isinstance(channel, discord.Thread | discord.TextChannel)
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
class InfoCommand(BaseModel):
|
||||
name: StrippedStr
|
||||
description: StrippedStr = "..."
|
||||
content: StrippedStr = ""
|
||||
|
||||
|
||||
class FileCommand(InfoCommand):
|
||||
file_url: str = ""
|
||||
file_name: str = ""
|
||||
|
||||
@property
|
||||
async def as_bytes(self) -> bytes | None:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(self.file_url) as response:
|
||||
if response.status != 200:
|
||||
return None
|
||||
|
||||
return await response.read()
|
||||
|
||||
@property
|
||||
async def as_discord_file(self) -> discord.File | None:
|
||||
if (as_bytes := await self.as_bytes) is not None:
|
||||
return discord.File(
|
||||
fp=BytesIO(as_bytes),
|
||||
filename=self.file_name,
|
||||
)
|
||||
|
||||
|
||||
class ClubInfo(BaseModel):
|
||||
channels: list[int]
|
||||
|
||||
info: InfoCommand
|
||||
vorstand: InfoCommand
|
||||
linktree: InfoCommand
|
||||
beitreten: InfoCommand
|
||||
fest: InfoCommand
|
||||
aktion: InfoCommand
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
discord_token: str
|
||||
command_prefix: str
|
||||
command_failed: str
|
||||
|
||||
post: Post
|
||||
ev_info: ClubInfo
|
||||
|
||||
@classmethod
|
||||
def get(cls) -> Self:
|
||||
cfg_path = os.getenv(
|
||||
key="CONFIG_PATH",
|
||||
default="/usr/local/etc/lenaverse-bot/lenaverse-bot.toml",
|
||||
)
|
||||
|
||||
with open(cfg_path, "rb") as cfg_file:
|
||||
return cls.model_validate(tomllib.load(cfg_file))
|
||||
|
||||
|
||||
CONFIG = Config.get()
|
|
@ -1,122 +0,0 @@
|
|||
import logging
|
||||
from enum import Enum, auto
|
||||
|
||||
import discord
|
||||
from discord import ui
|
||||
|
||||
from .config import CONFIG
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
"""
|
||||
Was soll mit dem Post geschehen?
|
||||
"""
|
||||
|
||||
PUBLISH = auto()
|
||||
ABORT = auto()
|
||||
TIMEOUT = auto()
|
||||
|
||||
|
||||
class PostConfirm(ui.View):
|
||||
"""
|
||||
Buttons zum Veröffentlichen oder Verwerfen eines Posts
|
||||
"""
|
||||
|
||||
action: Action = Action.TIMEOUT
|
||||
|
||||
async def __resolve(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
msg: str,
|
||||
action: Action,
|
||||
) -> None:
|
||||
await interaction.response.edit_message(content=msg, view=None)
|
||||
self.action = action
|
||||
self.stop()
|
||||
|
||||
@ui.button(label="Veröffentlichen", style=discord.ButtonStyle.success)
|
||||
async def publish(self, interaction: discord.Interaction, button: ui.Button):
|
||||
await self.__resolve(interaction, "Post wird veröffentlicht.", Action.PUBLISH)
|
||||
|
||||
@ui.button(label="Verwerfen", style=discord.ButtonStyle.danger)
|
||||
async def abort(self, interaction: discord.Interaction, button: ui.Button):
|
||||
await self.__resolve(interaction, "Post wird verworfen.", Action.ABORT)
|
||||
|
||||
|
||||
class PostModal(ui.Modal, title="Post verfassen"):
|
||||
"""
|
||||
Eingabefeld zum Verfassen eines Postings
|
||||
"""
|
||||
|
||||
content = ui.TextInput(
|
||||
label="Inhalt",
|
||||
style=discord.TextStyle.long,
|
||||
placeholder="Post-Inhalt hier einfügen ...",
|
||||
)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
post_content = f"{self.content.value}\n> Gepostet von <@{interaction.user.id}>"
|
||||
|
||||
# Vorschau mit Buttons
|
||||
view = PostConfirm()
|
||||
await interaction.response.send_message(
|
||||
content=f"## Post-Vorschau\n\n{post_content}",
|
||||
view=view,
|
||||
# nur für ausführenden User
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
# warten auf User-Entscheidung
|
||||
if await view.wait() is True:
|
||||
# Entscheidungszeit abgelaufen
|
||||
await interaction.delete_original_response()
|
||||
await interaction.user.send("Zeit abgelaufen, Post verworfen")
|
||||
_logger.info(
|
||||
f"User {interaction.user.name}({interaction.user.id}) timeout during /post"
|
||||
)
|
||||
|
||||
elif view.action is Action.PUBLISH:
|
||||
# Post veröffentlichen
|
||||
_logger.info(
|
||||
f"User {interaction.user.name}({interaction.user.id}) published a /post"
|
||||
)
|
||||
target = CONFIG.post.get_channel(interaction.client)
|
||||
await target.send(post_content)
|
||||
|
||||
elif view.action is Action.ABORT:
|
||||
# Post abbrechen
|
||||
_logger.info(
|
||||
f"User {interaction.user.name}({interaction.user.id}) aborted a /post"
|
||||
)
|
||||
|
||||
|
||||
@discord.app_commands.command(name=CONFIG.command_prefix + "post")
|
||||
async def post(interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Einen Post im Lenaisten-Bereich verfassen (nur für ausgewählte Mitglieder verfügbar)
|
||||
"""
|
||||
|
||||
if interaction.user.id in CONFIG.post.users:
|
||||
# Verfassen-Dialog anzeigen
|
||||
_logger.debug(
|
||||
f"User {interaction.user.name}({interaction.user.id}) started a /post"
|
||||
)
|
||||
await interaction.response.send_modal(PostModal())
|
||||
|
||||
else:
|
||||
# Zugriff verweigern
|
||||
_logger.info(
|
||||
f"User {interaction.user.name}({interaction.user.id}) tried to /post"
|
||||
)
|
||||
await interaction.response.send_message(
|
||||
content="Du bist nicht berechtigt, den `/post`-Befehl zu benutzen!",
|
||||
# nur für ausführenden User
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
|
||||
COMMANDS = [
|
||||
post,
|
||||
]
|
|
@ -1,65 +0,0 @@
|
|||
import functools
|
||||
import logging
|
||||
|
||||
import discord
|
||||
|
||||
from .config import CONFIG, FileCommand, InfoCommand
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def reply_private(interaction: discord.Interaction, name: str) -> bool:
|
||||
_logger.debug(f"User {interaction.user.name}({interaction.user.id}) used /{name}")
|
||||
return interaction.channel_id not in CONFIG.ev_info.channels
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def make_command(command) -> discord.app_commands.Command:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@make_command.register
|
||||
def _(command: FileCommand) -> discord.app_commands.Command:
|
||||
@discord.app_commands.command(
|
||||
name=CONFIG.command_prefix + command.name,
|
||||
description=command.description,
|
||||
)
|
||||
async def cmd(interaction: discord.Interaction) -> None:
|
||||
if (file := await command.as_discord_file) is not None:
|
||||
await interaction.response.send_message(
|
||||
content=command.content,
|
||||
suppress_embeds=True,
|
||||
ephemeral=reply_private(interaction, command.name),
|
||||
file=file,
|
||||
)
|
||||
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
content=CONFIG.command_failed,
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
@make_command.register
|
||||
def _(command: InfoCommand) -> discord.app_commands.Command:
|
||||
@discord.app_commands.command(
|
||||
name=CONFIG.command_prefix + command.name,
|
||||
description=command.description,
|
||||
)
|
||||
async def cmd(interaction: discord.Interaction) -> None:
|
||||
await interaction.response.send_message(
|
||||
content=command.content,
|
||||
suppress_embeds=True,
|
||||
ephemeral=reply_private(interaction, command.name),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
COMMANDS = [
|
||||
make_command(attr)
|
||||
for name in CONFIG.ev_info.model_dump().keys()
|
||||
if isinstance(attr := getattr(CONFIG.ev_info, name), InfoCommand)
|
||||
]
|
|
@ -1,13 +0,0 @@
|
|||
from .core.bot import LenaverseBot
|
||||
from .core.config import CONFIG
|
||||
|
||||
|
||||
def main() -> None:
|
||||
LenaverseBot().run(
|
||||
token=CONFIG.discord_token,
|
||||
log_handler=None,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1013
poetry.lock
generated
1013
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,23 +0,0 @@
|
|||
[tool.poetry]
|
||||
authors = ["Jörn-Michael Miehe <joern-michael.miehe@lenaisten.de>"]
|
||||
description = ""
|
||||
name = "lenaverse_bot"
|
||||
readme = "README.md"
|
||||
version = "0.1.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
aiohttp = {extras = ["speedups"], version = "^3.9.0"}
|
||||
discord-py = "^2.3.2"
|
||||
pydantic = "^2.5.1"
|
||||
python = "^3.11"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.11.0"
|
||||
flake8 = "^6.1.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
lenaverse-bot = "lenaverse_bot.main:main"
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["poetry-core"]
|
Loading…
Reference in a new issue