diff --git a/.idea/runConfigurations/Docs___Build_locally.xml b/.idea/runConfigurations/Docs___Build_locally.xml
new file mode 100644
index 0000000..4b93d1f
--- /dev/null
+++ b/.idea/runConfigurations/Docs___Build_locally.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/python___docs_make_py_dirhtml.xml b/.idea/runConfigurations/Docs___Open_in_browser.xml
similarity index 73%
rename from .idea/runConfigurations/python___docs_make_py_dirhtml.xml
rename to .idea/runConfigurations/Docs___Open_in_browser.xml
index 04c9886..2ac1b3a 100644
--- a/.idea/runConfigurations/python___docs_make_py_dirhtml.xml
+++ b/.idea/runConfigurations/Docs___Open_in_browser.xml
@@ -1,6 +1,6 @@
-
-
+
+
@@ -9,7 +9,7 @@
-
+
diff --git a/docs/user/config.md b/docs/user/config.md
index 672f1c7..3b0fe1c 100644
--- a/docs/user/config.md
+++ b/docs/user/config.md
@@ -7,7 +7,7 @@ As described on the landing page, the `/opt/disco/` directory should be a volume
so that you can manage the configuration files on the host system. This also makes it so that logs and databases are
persistent.
-## Secrets
+## Secrets (required)
Location of the file: `/opt/disco/run/secrets.toml`
### Secrets schema documentation
@@ -21,7 +21,21 @@ language: toml
---
```
-## Podcasts
+## Bot (optional)
+Location of the file: `/opt/disco/run/bot.toml`
+
+### Bot schema documentation
+```{include} ../../res/schemas/bot.v1.md
+```
+
+### Bot example
+```{literalinclude} ../../res/schemas/bot.v1.example.toml
+---
+language: toml
+---
+```
+
+## Podcasts (optional)
Location of the file: `/opt/disco/run/podcasts.toml`
### Podcasts schema documentation
diff --git a/res/misc/example_autoresponder.py b/res/misc/example_autoresponder.py
deleted file mode 100644
index b91d1ec..0000000
--- a/res/misc/example_autoresponder.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# mypy: ignore-errors
-"""
-Copyright © 2024 Fabian H. Schneider
-
-This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
-If a copy of the MPL was not distributed with this file,
-You can obtain one at https://mozilla.org/MPL/2.0/.
-"""
-
-# based on: https://discordpy.readthedocs.io/en/stable/quickstart.html
-import os
-import re
-import textwrap
-
-import discord
-
-# This example requires the 'message_content' intent.
-intents = discord.Intents.default()
-intents.message_content = True
-
-client = discord.Client(intents=intents)
-
-RE_MINDSET = re.compile(r"mindset", re.IGNORECASE)
-RE_RESILIENZ = re.compile(r"resilienz", re.IGNORECASE)
-
-
-@client.event
-def on_ready() -> None:
- """Logged in to Discord, let's log this very important event."""
- print(f"We have logged in as {client.user}")
-
-
-@client.event
-async def on_message(message: discord.Message) -> None:
- """
- Respond to messages that contain 'mindset' or 'resilienz'.
-
- :param message: the message to (potentially) respond to
- """
- print(message, type(message))
- if message.author == client.user:
- return
-
- is_mindset = RE_MINDSET.search(message.content)
- is_resilienz = RE_RESILIENZ.search(message.content)
-
- if is_mindset and is_resilienz:
- await message.channel.send(
- "Da versucht wohl jemand, möglichst viele Hasswörter von Wolfgang M. Schmitt zu verwenden."
- )
- elif is_mindset:
- await message.channel.send(
- textwrap.dedent("""\
- "Wer Mindset sagt, hat den Verstand schon lange verloren."
- --Wolfgang M. Schmitt""")
- )
- elif is_resilienz:
- await message.channel.send(
- textwrap.dedent("""\
- "Wer Resilienz sagt, will betrügen."
- --Wolfgang M. Schmitt""")
- )
-
-
-client.run(os.getenv("DISCORD_TOKEN"))
diff --git a/res/schemas/bot.v1.example.toml b/res/schemas/bot.v1.example.toml
new file mode 100644
index 0000000..e7ef62a
--- /dev/null
+++ b/res/schemas/bot.v1.example.toml
@@ -0,0 +1,5 @@
+"$schema" = "https://raw.githubusercontent.com/FlotterCodername/disco/refs/heads/main/res/schemas/bot.v1.schema.json"
+
+[no-reply]
+enabled = true
+message = "⚠️ I am a bot. My inbox is not monitored."
diff --git a/res/schemas/bot.v1.md b/res/schemas/bot.v1.md
new file mode 100644
index 0000000..b3c4c9e
--- /dev/null
+++ b/res/schemas/bot.v1.md
@@ -0,0 +1,14 @@
+This configuration stores everything related to the bot itself.
+
+### Bot top level
+| Property | Type | Required | Description |
+|----------|:----:|:--------:|-------------|
+| $schema | string (uri) | | Which JSONSchema the file follows. |
+| no-reply | object | | Configuration for the 'no-reply' feature. |
+
+#### `no-reply` (Optional)
+
+| Property | Type | Required | Description |
+|----------|:----:|:--------:|-------------|
+| enabled | boolean | Yes | Whether the 'no-reply' feature is enabled. |
+| message | string | | The message to send back when the bot receives a message. If empty or not set, a default message in English will be sent. |
diff --git a/res/schemas/bot.v1.schema.json b/res/schemas/bot.v1.schema.json
new file mode 100644
index 0000000..f8be8f8
--- /dev/null
+++ b/res/schemas/bot.v1.schema.json
@@ -0,0 +1,33 @@
+{
+ "$id": "https://raw.githubusercontent.com/FlotterCodername/disco/refs/heads/main/res/schemas/bot.v1.schema.json",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "description": "This configuration stores everything related to the bot itself.",
+ "properties": {
+ "$schema": {
+ "description": "Which JSONSchema the file follows.",
+ "format": "uri",
+ "type": "string"
+ },
+ "no-reply": {
+ "additionalProperties": false,
+ "description": "Configuration for the 'no-reply' feature.",
+ "properties": {
+ "enabled": {
+ "description": "Whether the 'no-reply' feature is enabled.",
+ "type": "boolean"
+ },
+ "message": {
+ "description": "The message to send back when the bot receives a message. If empty or not set, a default message in English will be sent.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "enabled"
+ ],
+ "type": "object"
+ }
+ },
+ "title": "Bot",
+ "type": "object"
+}
diff --git a/src/disco/__main__.py b/src/disco/__main__.py
index c668276..0ba05ac 100644
--- a/src/disco/__main__.py
+++ b/src/disco/__main__.py
@@ -30,6 +30,7 @@
# Discord bot setup
intents = discord.Intents.default()
intents.message_content = True
+intents.dm_messages = True
bot = commands.Bot(command_prefix="!", intents=intents)
@@ -102,6 +103,23 @@ async def on_ready() -> None: # noqa RUF029
synchronize_podcasts.start()
+@bot.event
+async def on_message(message) -> None:
+ """
+ Auto response in case of retrieving a DM.
+
+ :param message: The message that was sent to the bot by a user
+ """
+ # Check if the message is a DM and that it is not from the bot itself
+ if message.guild is None and message.author != bot.user:
+ # Send an automatic reply
+ await message.channel.send(
+ "Ich bin ein Bot. Meine Inbox wird daher nicht überwacht. Mehr Antworten bekommst du von mir nicht."
+ )
+
+ await bot.process_commands(message)
+
+
def main() -> int:
"""
Entry point for the Discord bot.
diff --git a/src/disco/configuration.py b/src/disco/configuration.py
index 89e874c..461b9f4 100644
--- a/src/disco/configuration.py
+++ b/src/disco/configuration.py
@@ -15,8 +15,8 @@
import tomli_w
from disco.helpers.atomicwrites import atomic_write
-from disco.paths import PODCASTS_TOML, SECRETS_TOML
-from disco.schemas import podcasts, secrets
+from disco.paths import BOT_TOML, PODCASTS_TOML, SECRETS_TOML
+from disco.schemas import bot, podcasts, secrets
if TYPE_CHECKING:
# noinspection PyProtectedMember
@@ -48,6 +48,7 @@ def content(self) -> dict:
class Configuration:
"""All configurations for the application."""
+ bot = _DcConfiguration(BOT_TOML, bot)
podcasts = _DcConfiguration(PODCASTS_TOML, podcasts)
secrets = _DcConfiguration(SECRETS_TOML, secrets)
diff --git a/src/disco/paths.py b/src/disco/paths.py
index a030bcd..e9a92fd 100644
--- a/src/disco/paths.py
+++ b/src/disco/paths.py
@@ -18,6 +18,7 @@
DISCO_RUN = DISCO_HOME / "run"
DISCO_RUN.mkdir(parents=True, exist_ok=True)
+BOT_TOML = DISCO_RUN / "bot.toml"
PODCASTS_TOML = DISCO_RUN / "podcasts.toml"
SECRETS_TOML = DISCO_RUN / "secrets.toml"
diff --git a/src/disco/schemas/__init__.py b/src/disco/schemas/__init__.py
index 971c531..15e8077 100644
--- a/src/disco/schemas/__init__.py
+++ b/src/disco/schemas/__init__.py
@@ -18,7 +18,7 @@
from disco.paths import RES_SCHEMAS
from disco.schemas.docgen import JsonSchemaMarkdownGenerator
-__all__ = ["podcasts", "secrets"]
+__all__ = ["bot", "podcasts", "secrets"]
_BASE_URI = "https://raw.githubusercontent.com/FlotterCodername/disco/refs/heads/main/res/schemas"
_DOC_GENERATOR = JsonSchemaMarkdownGenerator(indent_size=2)
@@ -69,6 +69,42 @@ def get_path(self, suffix: Literal["md", "schema.json", "example.toml"]) -> path
return RES_SCHEMAS / f"{self.name}.v{self.version}.{suffix}"
+bot = _DcSchema(
+ "bot",
+ 1,
+ {
+ "additionalProperties": false,
+ "description": "This configuration stores everything related to the bot itself.",
+ "properties": {
+ "$schema": {"description": "Which JSONSchema the file follows.", "format": "uri", "type": "string"},
+ "no-reply": {
+ "additionalProperties": false,
+ "description": "Configuration for the 'no-reply' feature.",
+ "properties": {
+ "enabled": {"description": "Whether the 'no-reply' feature is enabled.", "type": "boolean"},
+ "message": {
+ "description": (
+ "The message to send back when the bot receives a message. If empty or not set, a "
+ "default message in English will be sent."
+ ),
+ "type": "string",
+ },
+ },
+ "required": ["enabled"],
+ "type": "object",
+ },
+ },
+ "title": "Bot",
+ "type": "object",
+ },
+ {
+ "no-reply": {
+ "enabled": true,
+ "message": "⚠️ I am a bot. My inbox is not monitored.",
+ }
+ },
+)
+
podcasts = _DcSchema(
"podcasts",
1,
@@ -181,11 +217,11 @@ def _dump() -> int:
target.write_text(new_text)
# TOML example
target = schema.get_path("example.toml")
- old_text = target.read_text() if target.is_file() else ""
+ old_text = target.read_text("utf-8") if target.is_file() else ""
new_text = tomli_w.dumps(schema.example, multiline_strings=True)
if old_text != new_text:
changed = 1
- target.write_text(new_text)
+ target.write_text(new_text, "utf-8")
# Markdown
target = schema.get_path("md")
old_text = target.read_text("utf-8") if target.is_file() else ""
diff --git a/src/disco/schemas/docgen.py b/src/disco/schemas/docgen.py
index 1c65391..657529b 100644
--- a/src/disco/schemas/docgen.py
+++ b/src/disco/schemas/docgen.py
@@ -35,8 +35,7 @@ def generate_markdown(self, schema: dict[str, Any]) -> str:
Generate Markdown documentation from a JSONSchema.
:param schema: The JSONSchema dictionary
- Returns:
- str: Generated Markdown documentation
+ :return: Generated Markdown documentation
"""
sections: list[str] = []
if schema.get("description"):