Showing preview only (302K chars total). Download the full file or copy to clipboard to get everything.
Repository: msgaxzzz/Milo-discord-fun-bot
Branch: main
Commit: 3e9d328496f5
Files: 47
Total size: 286.9 KB
Directory structure:
gitextract_ia4j90bs/
├── .env.example
├── .flake8
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── SUPPORT.md
├── cogs/
│ ├── chat.py
│ ├── community.py
│ ├── economy.py
│ ├── farming.py
│ ├── fun.py
│ ├── games.py
│ ├── interactions.py
│ ├── media.py
│ ├── moderation.py
│ └── utility.py
├── config.example.json
├── config_loader.py
├── docs/
│ ├── commands.md
│ ├── configuration.md
│ ├── deployment.md
│ ├── faq.md
│ └── operations.md
├── install.bat
├── install.sh
├── main.py
├── requirements-dev.txt
├── requirements.txt
├── site/
│ ├── 404.html
│ ├── _headers
│ ├── index.html
│ ├── script.js
│ └── styles.css
└── tests/
├── conftest.py
├── test_config_loader.py
├── test_duration_parsing.py
├── test_issue_fixes.py
└── test_retry_delays.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .env.example
================================================
# Milo configuration
# Copy this file to `.env` and replace placeholder values.
# Required
DISCORD_TOKEN=your_discord_bot_token_here
# Optional: default AI provider settings
OPENAI_API_KEY=your_openai_api_key_here
OPENAI_API_BASE=https://api.openai.com/v1
ALLOW_USER_KEYS=true
DEFAULT_CHAT_MODEL=gpt-4o-mini
ALLOWED_CHAT_MODELS=gpt-4o-mini,gpt-4o
# Optional: Google Custom Search for `/chat --search_web`
# These values may incur cost depending on your Google setup.
GOOGLE_API_KEY=your_google_api_key_here
GOOGLE_CSE_ID=your_custom_search_engine_id_here
================================================
FILE: .flake8
================================================
[flake8]
max-line-length = 120
exclude =
.git,
__pycache__,
.venv,
venv,
database
ignore = E203,E501,W503
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a reproducible problem in Milo
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Use this form for reproducible defects. Do not report security issues here. Follow `SECURITY.md` for private reporting.
- type: textarea
id: summary
attributes:
label: Summary
description: What broke?
placeholder: A short description of the problem
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps To Reproduce
description: List the exact steps needed to trigger the issue
placeholder: |
1. Run ...
2. Use command ...
3. See error ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
placeholder: What should have happened?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
placeholder: What happened instead?
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs Or Screenshots
description: Paste relevant logs or screenshots. Remove secrets before submitting.
render: shell
- type: textarea
id: environment
attributes:
label: Environment
description: Runtime details that may matter
placeholder: |
Python version:
OS:
discord.py version:
Hosting provider:
- type: dropdown
id: scope
attributes:
label: Impacted Area
options:
- Commands
- Economy
- Farming
- AI Chat
- Install / Setup
- Docs
- Other
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest an improvement or new feature for Milo
title: "[Feature]: "
labels:
- enhancement
body:
- type: textarea
id: problem
attributes:
label: Problem
description: What limitation or pain point are you trying to solve?
placeholder: The current behavior makes it hard to ...
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed Solution
description: Describe the feature you want
placeholder: Add a command / setting / behavior that ...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any other approaches you considered?
- type: dropdown
id: breaking
attributes:
label: Breaking Change?
options:
- "No"
- "Maybe"
- "Yes"
validations:
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Commands
- Economy
- Farming
- AI Chat
- Moderation
- Developer Experience
- Docs
- Other
validations:
required: true
================================================
FILE: .github/pull_request_template.md
================================================
## Summary
- What problem does this change solve?
## Changes
- What changed?
## Verification
- How was this tested?
## Risk
- Any schema, config, permission, or external API impact?
## Checklist
- [ ] No real secrets were committed
- [ ] Manual verification was completed
- [ ] README or docs were updated if behavior changed
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
compile:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-asyncio
- name: Run tests
run: pytest
- name: Compile project
run: python -m compileall .
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
prerelease: ${{ contains(github.ref_name, '-') }}
================================================
FILE: .gitignore
================================================
config.json
.env
.env.*
!.env.example
__pycache__/
*.py[cod]
.wrangler/
.venv/
database/*.db
/Milo-discord-fun-bot/
================================================
FILE: CHANGELOG.md
================================================
## Changelog
**v1.0.4** – 2026-03-20
- Added bounded reminder and scheduled announcement polling, plus admin diagnostics for failed announcements.
- Added cleanup for stale chat cooldown state and retained message logs with periodic pruning.
- Hardened media commands against upstream API failures and clarified automod handling for thread channels.
- Reduced economy lock contention to per-guild scope and added length guards for reminders and announcements.
- Prevented `/help all` embed overflows and added regression tests covering the issue backlog fixes.
**v1.0.3** – 2026-03-14
- Isolated guild chat history per user and added server-side AI chat safety controls.
- Added basic automod, warning history commands, and better moderation logging support.
- Added scheduled announcements, welcome/goodbye previews, recurring reminders, and reminder snoozing.
- Fixed reminder delivery loss, guild economy cooldown scope, and anti-spam isolation issues.
**v1.0.2** – 2025-07-28
- Improved the installation script with added support for Windows environments.
**v1.0.1** – 2025-07-23
- Initial public release
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
## Our Standard
Participation in this project should be respectful, direct, and constructive.
Examples of expected behavior:
- Give technical feedback without personal attacks
- Assume mistakes can be corrected
- Keep disagreement focused on code, design, and evidence
- Respect maintainers' time and scope decisions
Examples of unacceptable behavior:
- Harassment, insults, or intimidation
- Doxxing or sharing private information
- Spam, trolling, or deliberately disruptive behavior
- Repeatedly pressuring maintainers after a decision has been made
## Enforcement
Project maintainers may remove comments, close discussions, or block participation when behavior is harmful to the project or its contributors.
## Reporting
For conduct issues, contact the maintainer privately through the security contact path in [SECURITY.md](./SECURITY.md) or through GitHub private contact if available.
## Scope
This policy applies to repository discussions, issues, pull requests, and project-related community spaces.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Milo
Thanks for contributing. Keep changes small, reviewable, and easy to test.
## Before You Start
- Read [README.md](./README.md) for setup instructions.
- Do not commit real secrets, tokens, or API keys.
- Prefer opening an issue before large feature work or schema changes.
## Development Setup
1. Clone the repository.
2. Create and activate a virtual environment.
3. Install dependencies:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
4. Configure secrets with `.env` or a local `config.json`.
5. Start the bot:
```bash
python3 main.py
```
Milo targets Python 3.9+.
## Project Expectations
- Target Python 3.10+ when possible, while preserving current runtime compatibility.
- Keep commands responsive. Avoid blocking I/O in command handlers.
- Treat database updates as stateful operations. For economy and farming changes, avoid race-prone read/modify/write patterns.
- Server-specific features must stay isolated per guild unless the behavior is explicitly global.
- Any feature that touches secrets, external APIs, or moderation behavior should include a short note in the PR description explaining the risk.
## Code Style
- Follow the existing module layout under `cogs/`.
- Use clear names and keep functions focused.
- Add comments only when the code is not obvious from the implementation.
- Reuse shared config and HTTP session handling instead of creating new global clients per cog.
## Pull Requests
Open a pull request with:
- A short summary of the problem
- The approach you took
- Any database or config changes
- Manual test steps
- Screenshots or command examples if user-facing behavior changed
Good PRs are narrow. Avoid mixing refactors, feature work, and formatting-only changes in one branch.
## Testing
There is no full automated test suite yet, so every PR should include manual verification.
At minimum:
- Run a syntax check:
```bash
python3 -m compileall .
```
- Exercise the changed slash commands in a test server.
- Verify that no secrets were added to tracked files.
## Security
Do not report security issues in public issues or pull requests. Follow [SECURITY.md](./SECURITY.md).
================================================
FILE: LICENSE
================================================
Copyright 2026 Sentinel Team and contributors
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Milo
Milo is an open-source Discord bot for community operations, support, and lightweight moderation, built with `discord.py`, `aiosqlite`, and `aiohttp`.
It combines:
- AI-assisted community help with optional web search
- Moderation, announcements, reminders, and server utility workflows
- Per-server progression systems and engagement features
- Self-hosted infrastructure for small online communities
## Why This Project Exists
Milo is intended to be a practical bot for real online communities, not a one-command demo or a closed hosted service. The project focuses on tools that help small servers stay active, organized, and easier to support:
- AI-assisted help for common community questions
- reminders, announcements, and lightweight operations workflows
- simple moderation and safety helpers
- self-hosted engagement features that communities can adapt to their own needs
## Project Model
- License: MIT
- Runtime target: Python 3.9+
- Storage: SQLite
- Secrets: environment variables first, then local `config.json`
- Distribution: free and open source
- Maintenance model: community-maintained and intended for self-hosted, non-closed deployments
## Quick Start
Use the installer script if you want the fastest local setup:
### Linux / macOS
```bash
curl -fsSL https://raw.githubusercontent.com/msgaxzzz/Milo-discord-fun-bot/main/install.sh -o install.sh
bash install.sh
```
### Windows
```powershell
powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/msgaxzzz/Milo-discord-fun-bot/main/install.bat -OutFile install.bat"
.\install.bat
```
The installer creates a local `.venv`, installs dependencies, and generates a local `config.json`. On Windows it can fall back to downloading the repository zip if `git` is not installed.
If you prefer a manual setup, use:
```bash
git clone https://github.com/msgaxzzz/Milo-discord-fun-bot.git
cd Milo-discord-fun-bot
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python3 main.py
```
Fill in `.env` before starting the bot.
## Who It Is For
- open-source project servers
- small online communities that need self-hosted tooling
- learning groups, support communities, and volunteer-run servers
- maintainers who want a compact `discord.py` codebase to extend
## What Milo Includes
- AI chat with configurable model allowlists and optional Google Custom Search
- Chat safety controls for cooldowns, channel rules, role allowlists, and daily usage caps
- Utility commands for persisted reminders, recurring reminders, AFK management, help, and server info
- Community tooling for welcome messages, leave messages, scheduled announcements, and mod logs
- Moderation tooling for warnings, invite/link filters, bad word filters, and channel whitelists
- Economy commands with per-guild balances and leaderboards
- Admin tools for managing server economy balances
- Farming progression tied to the server economy
- Games like `/guess`, `/tictactoe`, `/roll`, and `/rps`
- Fun and media commands for polls, memes, avatars, GIF interactions, and image generation
## Important Behavior
- Economy and farming data are isolated per guild
- AI chat works in servers and DMs, but server configuration commands are guild-only
- Guild chat history is isolated per user instead of being shared by the whole channel
- Reminders are persisted in SQLite, survive restarts, and can be recurring
- AFK status is stored per guild and cleared on your next message in that server
- Real secrets should never be committed to git
## Configuration
Milo loads config in this order:
1. Environment variables from the current shell or `.env`
2. Local `config.json`
Required:
- `DISCORD_TOKEN`
Optional:
- `OPENAI_API_KEY`
- `OPENAI_API_BASE`
- `ALLOW_USER_KEYS`
- `DEFAULT_CHAT_MODEL`
- `ALLOWED_CHAT_MODELS`
- `GOOGLE_API_KEY`
- `GOOGLE_CSE_ID`
See:
- [Configuration Guide](./docs/configuration.md)
- [.env.example](./.env.example)
## Documentation
- [Command Reference](./docs/commands.md)
- [Configuration Guide](./docs/configuration.md)
- [Deployment Guide](./docs/deployment.md)
- [Operations Notes](./docs/operations.md)
- [FAQ](./docs/faq.md)
- [Contributing](./CONTRIBUTING.md)
- [Code of Conduct](./CODE_OF_CONDUCT.md)
- [Security Policy](./SECURITY.md)
- [Support](./SUPPORT.md)
## Contributors
Milo is community-maintained. See the full contributor list on GitHub:
- [Contributors Graph](https://github.com/msgaxzzz/Milo-discord-fun-bot/graphs/contributors)
- Sascha Buehrle ([`@saschabuehrle`](https://github.com/saschabuehrle)) has submitted community fixes for interaction message formatting and poll permission handling through pull requests [#39](https://github.com/msgaxzzz/Milo-discord-fun-bot/pull/39) and [#40](https://github.com/msgaxzzz/Milo-discord-fun-bot/pull/40).
## Installation Scripts
The repository includes:
- [install.sh](./install.sh) for Linux/macOS-style environments
- [install.bat](./install.bat) for Windows
Both installers are intended for local setup and will generate a local `config.json`.
## Manual Setup
### Linux / macOS
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
cp .env.example .env
python3 main.py
```
### Windows
```bat
py -3.9 -m venv .venv
.venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
copy .env.example .env
python main.py
```
## Deployment
For a small self-hosted setup, any machine that can:
- run Python 3.9+
- keep a long-lived process online
- write to local disk
- access Discord and optional external APIs
is enough.
Common options:
- a VPS
- a home server
- a cloud VM
- a container host
See [Deployment Guide](./docs/deployment.md) for process management and environment notes.
## FAQ
Common questions:
- Does Milo support DMs for AI chat? Yes.
- Is the economy global across all servers? No, it is isolated per guild.
- Do reminders survive restarts? Yes.
- Can I schedule recurring reminders and server announcements? Yes.
- Do I need OpenAI credentials to run the bot? Only for AI chat features.
See the full [FAQ](./docs/faq.md).
## Development Notes
- Slash commands are loaded from modules in `cogs/`
- Shared HTTP access is managed centrally by the bot process
- SQLite schema is created and migrated at startup
- The project currently relies on manual verification rather than a full automated test suite
## Security
- Never commit real Discord, OpenAI, or Google API credentials
- Use `.env` or a gitignored local `config.json`
- Report vulnerabilities privately according to [SECURITY.md](./SECURITY.md)
## Project Structure
```text
main.py
config_loader.py
cogs/
docs/
```
## Roadmap
Near-term improvements that would strengthen the project:
- automated tests for economy, farming, reminder, and automod flows
- richer reporting around reminder delivery failures and scheduled announcement failures
- structured logging and better runtime error reporting
- richer deployment examples
- command reference generation from source metadata
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
Only the latest code on `main` should be assumed to receive security fixes.
## Reporting a Vulnerability
Do not open a public issue for security problems.
Report vulnerabilities privately with:
- A clear description of the issue
- Steps to reproduce
- Impact
- Any suggested fix or mitigation
## What to Report
Examples:
- Token or API key exposure
- Permission bypass
- Unsafe handling of user-provided API keys
- Remote code execution or command injection
- Database corruption or multi-guild data leakage
- Abuse paths in moderation features
## Response Goals
The maintainer should aim to:
- Acknowledge valid reports promptly
- Reproduce and assess impact
- Patch privately when reasonable
- Credit the reporter if they want attribution
## Secret Handling
- Never commit real `DISCORD_TOKEN`, OpenAI keys, or Google API keys
- Use `.env` or a gitignored local `config.json`
- Rotate any credential immediately if it is exposed in git history, logs, or screenshots
================================================
FILE: SUPPORT.md
================================================
# Support
## Getting Help
Use the repository issue tracker for:
- Reproducible bugs
- Feature requests
- Documentation gaps
Before opening an issue:
- Read [README.md](./README.md)
- Read [CONTRIBUTING.md](./CONTRIBUTING.md)
- Check whether the issue is already open
## What To Include
Good support requests include:
- What you were trying to do
- The exact command or workflow involved
- Your runtime environment
- Logs or screenshots with secrets removed
## Security Issues
Do not request support for vulnerabilities in public issues. Use [SECURITY.md](./SECURITY.md) instead.
## Scope
This repository is maintained on a best-effort basis. Setup help, bug reports, and targeted feature requests are in scope. Custom hosting, private deployment support, and urgent SLA-style support are not guaranteed.
================================================
FILE: cogs/chat.py
================================================
import json
import logging
from collections import defaultdict
from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple
import discord
from discord import app_commands
from discord.ext import commands, tasks
logger = logging.getLogger(__name__)
DEFAULT_API_BASE = "https://api.openai.com/v1"
DEFAULT_MODEL = "gpt-3.5-turbo"
MAX_CONVERSATION_HISTORY = 10
MAX_PERSONA_LENGTH = 500
MAX_EMBED_FIELD_LENGTH = 1024
MAX_EMBED_DESCRIPTION_LENGTH = 4096
CONVERSATION_TTL = timedelta(hours=6)
COOLDOWN_RETENTION = timedelta(days=1)
DEFAULT_PERSONA = (
"You are Milo, a friendly and helpful Discord bot. "
"You can access real-time information using the 'google_search' tool for current events or specific data. "
"Keep your answers concise and engaging."
)
class Chat(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.conversations: Dict[Tuple[Any, ...], List[Dict[str, Any]]] = defaultdict(list)
self.chat_cooldowns: Dict[Tuple[int, int], discord.utils.utcnow] = {}
self.conversation_last_used: Dict[Tuple[Any, ...], discord.utils.utcnow] = {}
self.load_config()
self.bot.loop.create_task(self.setup_database())
self._prune_cooldowns.start()
def cog_unload(self):
self._prune_cooldowns.cancel()
def load_config(self):
config = getattr(self.bot, "config", {})
self.default_api_key = config.get("OPENAI_API_KEY")
self.api_base = config.get("OPENAI_API_BASE", DEFAULT_API_BASE)
self.allow_user_keys = config.get("ALLOW_USER_KEYS", True)
self.default_model = config.get("DEFAULT_CHAT_MODEL", DEFAULT_MODEL)
self.allowed_models = config.get("ALLOWED_CHAT_MODELS", [DEFAULT_MODEL])
self.google_api_key = config.get("GOOGLE_API_KEY")
self.google_cse_id = config.get("GOOGLE_CSE_ID")
self.enable_web_search = bool(self.google_api_key and self.google_cse_id)
@tasks.loop(minutes=10)
async def _prune_cooldowns(self):
now = discord.utils.utcnow()
expired = [key for key, ts in self.chat_cooldowns.items() if (now - ts) > COOLDOWN_RETENTION]
for key in expired:
del self.chat_cooldowns[key]
async def setup_database(self):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS guild_configs (
guild_id INTEGER PRIMARY KEY,
openai_key TEXT,
persona TEXT
)
"""
)
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS chat_policies (
guild_id INTEGER PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 1,
cooldown_seconds INTEGER NOT NULL DEFAULT 8,
daily_usage_limit INTEGER,
allowed_channel_ids TEXT,
blocked_channel_ids TEXT,
allowed_role_ids TEXT
)
"""
)
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS chat_usage (
guild_id INTEGER NOT NULL,
usage_date TEXT NOT NULL,
usage_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (guild_id, usage_date)
)
"""
)
columns = await self._table_columns(cursor, "guild_configs")
if "persona" not in columns:
await cursor.execute("ALTER TABLE guild_configs ADD COLUMN persona TEXT")
await self.bot.db.commit()
async def _table_columns(self, cursor, table_name: str) -> List[str]:
await cursor.execute(f"PRAGMA table_info({table_name})")
return [row[1] for row in await cursor.fetchall()]
@property
def session(self):
return self.bot.http_session
def _context_key(self, interaction: discord.Interaction) -> Tuple[Any, ...]:
if interaction.guild_id:
return ("guild", interaction.guild_id, interaction.channel_id, interaction.user.id)
return ("dm", interaction.user.id)
def _serialize_ids(self, ids: List[int]) -> Optional[str]:
cleaned = sorted({int(item) for item in ids})
return json.dumps(cleaned) if cleaned else None
def _deserialize_ids(self, raw: Optional[str]) -> List[int]:
if not raw:
return []
try:
values = json.loads(raw)
except json.JSONDecodeError:
return []
return [int(item) for item in values if str(item).isdigit()]
async def get_guild_config(self, guild_id: Optional[int]):
if not guild_id:
return None, None
async with self.bot.db.cursor() as cursor:
await cursor.execute("SELECT openai_key, persona FROM guild_configs WHERE guild_id = ?", (guild_id,))
return await cursor.fetchone()
async def set_guild_key(self, guild_id: int, key: Optional[str] = None):
async with self.bot.db.cursor() as cursor:
await cursor.execute("INSERT OR IGNORE INTO guild_configs (guild_id) VALUES (?)", (guild_id,))
await cursor.execute("UPDATE guild_configs SET openai_key = ? WHERE guild_id = ?", (key, guild_id))
await self.bot.db.commit()
async def set_guild_persona(self, guild_id: int, persona: Optional[str] = None):
async with self.bot.db.cursor() as cursor:
await cursor.execute("INSERT OR IGNORE INTO guild_configs (guild_id) VALUES (?)", (guild_id,))
await cursor.execute("UPDATE guild_configs SET persona = ? WHERE guild_id = ?", (persona, guild_id))
await self.bot.db.commit()
async def get_policy(self, guild_id: Optional[int]) -> Dict[str, Any]:
default_policy = {
"enabled": True,
"cooldown_seconds": 8,
"daily_usage_limit": None,
"allowed_channel_ids": [],
"blocked_channel_ids": [],
"allowed_role_ids": [],
}
if not guild_id:
return default_policy
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT enabled, cooldown_seconds, daily_usage_limit, allowed_channel_ids,
blocked_channel_ids, allowed_role_ids
FROM chat_policies
WHERE guild_id = ?
""",
(guild_id,),
)
row = await cursor.fetchone()
if row is None:
return default_policy
enabled, cooldown_seconds, daily_usage_limit, allowed_channel_ids, blocked_channel_ids, allowed_role_ids = row
return {
"enabled": bool(enabled),
"cooldown_seconds": (
default_policy["cooldown_seconds"] if cooldown_seconds is None else int(cooldown_seconds)
),
"daily_usage_limit": daily_usage_limit,
"allowed_channel_ids": self._deserialize_ids(allowed_channel_ids),
"blocked_channel_ids": self._deserialize_ids(blocked_channel_ids),
"allowed_role_ids": self._deserialize_ids(allowed_role_ids),
}
async def update_policy(self, guild_id: int, **fields):
async with self.bot.db.cursor() as cursor:
await cursor.execute("INSERT OR IGNORE INTO chat_policies (guild_id) VALUES (?)", (guild_id,))
for field, value in fields.items():
await cursor.execute(f"UPDATE chat_policies SET {field} = ? WHERE guild_id = ?", (value, guild_id))
await self.bot.db.commit()
async def mutate_id_list(self, guild_id: int, field: str, value: int, add: bool):
policy = await self.get_policy(guild_id)
current = set(policy[field])
if add:
current.add(value)
else:
current.discard(value)
await self.update_policy(guild_id, **{field: self._serialize_ids(list(current))})
return sorted(current)
async def get_usage_count(self, guild_id: int) -> int:
usage_date = discord.utils.utcnow().date().isoformat()
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"SELECT usage_count FROM chat_usage WHERE guild_id = ? AND usage_date = ?",
(guild_id, usage_date),
)
row = await cursor.fetchone()
return row[0] if row else 0
async def increment_usage(self, guild_id: int):
usage_date = discord.utils.utcnow().date().isoformat()
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
INSERT INTO chat_usage (guild_id, usage_date, usage_count)
VALUES (?, ?, 1)
ON CONFLICT(guild_id, usage_date)
DO UPDATE SET usage_count = usage_count + 1
""",
(guild_id, usage_date),
)
await self.bot.db.commit()
async def validate_api_key(self, api_key: str) -> Tuple[bool, str]:
headers = {"Authorization": f"Bearer {api_key}"}
try:
async with self.session.get(f"{self.api_base}/models", headers=headers) as response:
if response.status == 200:
return True, "API key is valid."
try:
error_data = await response.json()
error_message = error_data.get("error", {}).get("message", "Unknown API error.")
except Exception:
error_message = "Unknown API error."
return False, f"{response.status}: {error_message}"
except Exception as error:
return False, str(error)
async def enforce_policy(self, interaction: discord.Interaction, policy: Dict[str, Any]) -> Optional[str]:
if not interaction.guild_id:
return None
if not policy["enabled"]:
return "AI chat is disabled in this server."
if interaction.channel_id in policy["blocked_channel_ids"]:
return "AI chat is disabled in this channel."
if policy["allowed_channel_ids"] and interaction.channel_id not in policy["allowed_channel_ids"]:
return "AI chat is only allowed in specific channels configured by the server admins."
if policy["allowed_role_ids"]:
member = interaction.user if isinstance(interaction.user, discord.Member) else None
if member is None:
return "AI chat is restricted to specific roles in this server."
member_role_ids = {role.id for role in member.roles}
if not member_role_ids.intersection(policy["allowed_role_ids"]):
return "You do not have one of the roles required to use AI chat here."
cooldown_seconds = max(int(policy["cooldown_seconds"]), 0)
if cooldown_seconds > 0:
cooldown_key = (interaction.guild_id, interaction.user.id)
now = discord.utils.utcnow()
last_used = self.chat_cooldowns.get(cooldown_key)
if last_used and (now - last_used).total_seconds() < cooldown_seconds:
remaining = cooldown_seconds - int((now - last_used).total_seconds())
return f"You're on cooldown for this server. Try again in {remaining}s."
usage_limit = policy["daily_usage_limit"]
if usage_limit:
usage_count = await self.get_usage_count(interaction.guild_id)
if usage_count >= usage_limit:
return "This server has reached its daily AI chat usage cap."
return None
def define_tools(self):
return [
{
"type": "function",
"function": {
"name": "google_search",
"description": "Get real-time information from the web for recent events or specific data.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string", "description": "The search query."}},
"required": ["query"],
},
},
}
]
async def execute_google_search(self, query: str):
url = "https://www.googleapis.com/customsearch/v1"
params = {"key": self.google_api_key, "cx": self.google_cse_id, "q": query, "num": 5}
try:
async with self.session.get(url, params=params) as response:
if response.status == 200:
data = await response.json()
items = data.get("items", [])
snippets = [item.get("snippet", "") for item in items]
return json.dumps({"results": snippets}) if snippets else json.dumps({"error": "No results found."})
error_data = await response.json()
error_message = error_data.get("error", {}).get("message", "Unknown error.")
return json.dumps({"error": f"Failed to fetch search results. Status: {response.status} - {error_message}"})
except Exception as error:
return json.dumps({"error": f"An error occurred during search: {error}"})
async def model_autocomplete(self, interaction: discord.Interaction, current: str):
current_lower = current.lower()
return [
app_commands.Choice(name=model, value=model)
for model in self.allowed_models
if current_lower in model.lower()
]
def _channel_labels(self, guild: discord.Guild, ids: List[int]) -> str:
if not ids:
return "Not set"
labels = []
for channel_id in ids:
channel = guild.get_channel(channel_id)
labels.append(channel.mention if channel else f"`{channel_id}`")
return ", ".join(labels)
def _role_labels(self, guild: discord.Guild, ids: List[int]) -> str:
if not ids:
return "Not set"
labels = []
for role_id in ids:
role = guild.get_role(role_id)
labels.append(role.mention if role else f"`{role_id}`")
return ", ".join(labels)
def _truncate_field_value(self, value: str, limit: int = MAX_EMBED_FIELD_LENGTH) -> str:
if len(value) <= limit:
return value
return f"{value[: limit - 1].rstrip()}…"
def _trim_history(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
if len(messages) <= MAX_CONVERSATION_HISTORY:
return messages
return messages[0:1] + messages[-(MAX_CONVERSATION_HISTORY - 1) :]
def _prune_runtime_state(self) -> None:
now = discord.utils.utcnow()
expired_conversations = [
key for key, last_used in self.conversation_last_used.items() if now - last_used > CONVERSATION_TTL
]
for key in expired_conversations:
self.conversation_last_used.pop(key, None)
self.conversations.pop(key, None)
expired_cooldowns = [
key for key, last_used in self.chat_cooldowns.items() if now - last_used > COOLDOWN_RETENTION
]
for key in expired_cooldowns:
self.chat_cooldowns.pop(key, None)
def _set_conversation_state(self, context_key: Tuple[Any, ...], messages: List[Dict[str, Any]]) -> None:
if messages:
self.conversations[context_key] = messages
self.conversation_last_used[context_key] = discord.utils.utcnow()
return
self.conversations.pop(context_key, None)
self.conversation_last_used.pop(context_key, None)
chat_config = app_commands.Group(
name="chat-config",
description="Configure the AI chat settings for a server.",
guild_only=True,
)
@chat_config.command(name="set-key", description="Set a custom OpenAI API key for this server.")
@app_commands.describe(key="Your OpenAI API key. Use 'reset' to remove.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_key(self, interaction: discord.Interaction, key: str):
if not self.allow_user_keys:
await interaction.response.send_message("The bot owner has disabled custom API keys.", ephemeral=True)
return
if key.lower() == "reset":
await self.set_guild_key(interaction.guild.id)
await interaction.response.send_message("Server API key removed.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
valid, detail = await self.validate_api_key(key)
if not valid:
await interaction.followup.send(f"API key validation failed: {detail}", ephemeral=True)
return
await self.set_guild_key(interaction.guild.id, key)
await interaction.followup.send("Server API key validated and saved.", ephemeral=True)
@chat_config.command(name="set-persona", description="Set a custom personality for the AI.")
@app_commands.describe(persona="A description of the AI's personality. Use 'reset' to remove.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_persona(self, interaction: discord.Interaction, persona: str):
if len(persona) > MAX_PERSONA_LENGTH:
await interaction.response.send_message("Persona is too long (max 500 chars).", ephemeral=True)
return
if persona.lower() == "reset":
await self.set_guild_persona(interaction.guild.id)
await interaction.response.send_message("AI persona reset to default.", ephemeral=True)
return
await self.set_guild_persona(interaction.guild.id, persona)
await interaction.response.send_message("AI persona updated.", ephemeral=True)
@chat_config.command(name="set-enabled", description="Enable or disable AI chat in this server.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_enabled(self, interaction: discord.Interaction, enabled: bool):
await self.update_policy(interaction.guild.id, enabled=int(enabled))
state = "enabled" if enabled else "disabled"
await interaction.response.send_message(f"AI chat is now {state} for this server.", ephemeral=True)
@chat_config.command(name="set-cooldown", description="Set the per-user chat cooldown for this server.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(seconds="Cooldown in seconds. Use 0 to disable.")
async def set_cooldown(self, interaction: discord.Interaction, seconds: app_commands.Range[int, 0, 600]):
await self.update_policy(interaction.guild.id, cooldown_seconds=int(seconds))
await interaction.response.send_message(f"Chat cooldown set to {seconds}s.", ephemeral=True)
@chat_config.command(name="set-usage-cap", description="Set the per-day chat usage cap for this server.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(limit="Maximum successful chat requests per day. Use 0 to remove the cap.")
async def set_usage_cap(self, interaction: discord.Interaction, limit: app_commands.Range[int, 0, 5000]):
value = None if limit == 0 else int(limit)
await self.update_policy(interaction.guild.id, daily_usage_limit=value)
label = "removed" if value is None else str(value)
await interaction.response.send_message(f"Daily usage cap set to {label}.", ephemeral=True)
@chat_config.command(name="allow-channel", description="Allow AI chat only in a specific channel.")
@app_commands.checks.has_permissions(manage_guild=True)
async def allow_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
channels = await self.mutate_id_list(interaction.guild.id, "allowed_channel_ids", channel.id, add=True)
await interaction.response.send_message(
f"Allowed channels updated. {len(channels)} channel(s) are now allowlisted.",
ephemeral=True,
)
@chat_config.command(name="block-channel", description="Block AI chat in a specific channel.")
@app_commands.checks.has_permissions(manage_guild=True)
async def block_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
channels = await self.mutate_id_list(interaction.guild.id, "blocked_channel_ids", channel.id, add=True)
await interaction.response.send_message(
f"Blocked channels updated. {len(channels)} channel(s) are now blocked.",
ephemeral=True,
)
@chat_config.command(name="clear-channel-rules", description="Clear channel allow/block rules for AI chat.")
@app_commands.checks.has_permissions(manage_guild=True)
async def clear_channel_rules(self, interaction: discord.Interaction):
await self.update_policy(interaction.guild.id, allowed_channel_ids=None, blocked_channel_ids=None)
await interaction.response.send_message("Channel rules cleared.", ephemeral=True)
@chat_config.command(name="allow-role", description="Restrict AI chat to members with a specific role.")
@app_commands.checks.has_permissions(manage_guild=True)
async def allow_role(self, interaction: discord.Interaction, role: discord.Role):
roles = await self.mutate_id_list(interaction.guild.id, "allowed_role_ids", role.id, add=True)
await interaction.response.send_message(
f"Allowed roles updated. {len(roles)} role(s) are now allowlisted.",
ephemeral=True,
)
@chat_config.command(name="remove-role", description="Remove a role from the AI chat allowlist.")
@app_commands.checks.has_permissions(manage_guild=True)
async def remove_role(self, interaction: discord.Interaction, role: discord.Role):
roles = await self.mutate_id_list(interaction.guild.id, "allowed_role_ids", role.id, add=False)
await interaction.response.send_message(
f"Allowed roles updated. {len(roles)} role(s) remain allowlisted.",
ephemeral=True,
)
@chat_config.command(name="clear-role-rules", description="Remove all role-based AI chat restrictions.")
@app_commands.checks.has_permissions(manage_guild=True)
async def clear_role_rules(self, interaction: discord.Interaction):
await self.update_policy(interaction.guild.id, allowed_role_ids=None)
await interaction.response.send_message("Role restrictions cleared.", ephemeral=True)
@chat_config.command(name="view", description="View the current chat configuration.")
@app_commands.checks.has_permissions(manage_guild=True)
async def view_config(self, interaction: discord.Interaction):
self._prune_runtime_state()
guild_config = await self.get_guild_config(interaction.guild.id) or (None, None)
guild_key, guild_persona = guild_config
policy = await self.get_policy(interaction.guild.id)
usage_count = await self.get_usage_count(interaction.guild.id)
embed = discord.Embed(title=f"Chat Configuration for {interaction.guild.name}", color=discord.Color.blue())
key_status = "Not configured"
if guild_key:
key_status = f"`{guild_key[:5]}...{guild_key[-4:]}` (custom)"
elif self.default_api_key:
key_status = "Using bot default key"
embed.add_field(name="Server API Key", value=key_status, inline=False)
embed.add_field(
name="AI Persona",
value=self._truncate_field_value(guild_persona or DEFAULT_PERSONA),
inline=False,
)
embed.add_field(name="Web Search", value="Enabled" if self.enable_web_search else "Disabled", inline=False)
embed.add_field(name="API Base URL", value=self._truncate_field_value(f"`{self.api_base}`"), inline=False)
embed.add_field(
name="Allowed Models",
value=self._truncate_field_value(", ".join(f"`{model}`" for model in self.allowed_models)),
inline=False,
)
embed.add_field(name="Chat Enabled", value="Yes" if policy["enabled"] else "No", inline=True)
embed.add_field(name="Cooldown", value=f"{policy['cooldown_seconds']}s", inline=True)
embed.add_field(
name="Daily Usage Cap",
value="Not set" if policy["daily_usage_limit"] is None else f"{usage_count}/{policy['daily_usage_limit']}",
inline=True,
)
embed.add_field(
name="Allowed Channels",
value=self._truncate_field_value(self._channel_labels(interaction.guild, policy["allowed_channel_ids"])),
inline=False,
)
embed.add_field(
name="Blocked Channels",
value=self._truncate_field_value(self._channel_labels(interaction.guild, policy["blocked_channel_ids"])),
inline=False,
)
embed.add_field(
name="Allowed Roles",
value=self._truncate_field_value(self._role_labels(interaction.guild, policy["allowed_role_ids"])),
inline=False,
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@chat_config.command(name="test", description="Validate the effective API key for this server.")
@app_commands.checks.has_permissions(manage_guild=True)
async def test_config(self, interaction: discord.Interaction):
guild_config = await self.get_guild_config(interaction.guild.id) or (None, None)
guild_key, _ = guild_config
api_key = guild_key or self.default_api_key
if not api_key:
await interaction.response.send_message("No API key is configured to test.", ephemeral=True)
return
await interaction.response.defer(ephemeral=True)
valid, detail = await self.validate_api_key(api_key)
if valid:
await interaction.followup.send("API key validation succeeded.", ephemeral=True)
else:
await interaction.followup.send(f"API key validation failed: {detail}", ephemeral=True)
@chat_config.command(name="models", description="Show the configured default and allowed chat models.")
@app_commands.checks.has_permissions(manage_guild=True)
async def list_models(self, interaction: discord.Interaction):
embed = discord.Embed(title="Chat Models", color=discord.Color.blurple())
embed.add_field(name="Default Model", value=f"`{self.default_model}`", inline=False)
embed.add_field(
name="Allowed Models",
value=self._truncate_field_value("\n".join(f"`{model}`" for model in self.allowed_models)),
inline=False,
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@app_commands.command(name="chat", description="Chat with the AI, with optional live web search.")
@app_commands.describe(
prompt="What to talk about?",
model="Choose a specific AI model.",
search_web="Set to true to allow the AI to search the web for current info.",
)
@app_commands.autocomplete(model=model_autocomplete)
async def chat(self, interaction: discord.Interaction, prompt: str, model: Optional[str] = None, search_web: bool = False):
self._prune_runtime_state()
guild_id = interaction.guild.id if interaction.guild else None
chosen_model = model or self.default_model
policy = await self.get_policy(guild_id)
if chosen_model not in self.allowed_models:
await interaction.response.send_message("That model is not allowed for this bot.", ephemeral=True)
return
if search_web and not self.enable_web_search:
await interaction.response.send_message(
"Web search is not configured by the bot owner.",
ephemeral=True,
)
return
if interaction.guild_id:
policy_error = await self.enforce_policy(interaction, policy)
if policy_error:
await interaction.response.send_message(policy_error, ephemeral=True)
return
guild_config = await self.get_guild_config(guild_id) or (None, None)
api_key, persona = guild_config
api_key = api_key or self.default_api_key
if not api_key:
await interaction.response.send_message(
"AI chat is not configured. An admin must set an API key.",
ephemeral=True,
)
return
await interaction.response.defer()
context_key = self._context_key(interaction)
existing_messages = list(self.conversations.get(context_key, []))
if not existing_messages:
existing_messages = [{"role": "system", "content": persona or DEFAULT_PERSONA}]
original_messages = list(existing_messages)
working_messages = list(existing_messages)
working_messages.append({"role": "user", "content": prompt})
self._set_conversation_state(context_key, working_messages)
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {"model": chosen_model, "messages": working_messages}
if self.enable_web_search and search_web:
payload["tools"] = self.define_tools()
payload["tool_choice"] = "auto"
final_answer = None
try:
async with self.session.post(f"{self.api_base}/chat/completions", headers=headers, json=payload) as response:
if response.status != 200:
error_data = await response.json()
error_message = error_data.get("error", {}).get("message", "An unknown API error occurred.")
self._set_conversation_state(context_key, original_messages)
await interaction.followup.send(f"API Error: {response.status} - {error_message}", ephemeral=True)
return
data = await response.json()
ai_message = data["choices"][0]["message"]
if ai_message.get("tool_calls") and self.enable_web_search and search_web:
thinking_message = await interaction.followup.send("Searching the web...", wait=True)
working_messages.append(ai_message)
self._set_conversation_state(context_key, working_messages)
tool_call = ai_message["tool_calls"][0]
function_args = json.loads(tool_call["function"]["arguments"])
query = function_args.get("query", "")
tool_response = await self.execute_google_search(query)
working_messages.append(
{
"tool_call_id": tool_call["id"],
"role": "tool",
"name": tool_call["function"]["name"],
"content": tool_response,
}
)
self._set_conversation_state(context_key, working_messages)
final_payload = {"model": chosen_model, "messages": working_messages}
async with self.session.post(
f"{self.api_base}/chat/completions",
headers=headers,
json=final_payload,
) as final_response:
if final_response.status != 200:
self._set_conversation_state(context_key, original_messages)
await thinking_message.edit(content="The model failed after web search. Please try again.")
return
final_data = await final_response.json()
final_answer = final_data["choices"][0]["message"]["content"]
await thinking_message.edit(
content=self._truncate_field_value(final_answer, MAX_EMBED_DESCRIPTION_LENGTH)
)
else:
final_answer = ai_message.get("content") or "The model returned an empty response."
await interaction.edit_original_response(content=final_answer)
working_messages.append({"role": "assistant", "content": final_answer})
self._set_conversation_state(context_key, self._trim_history(working_messages))
if interaction.guild_id:
cooldown_key = (interaction.guild_id, interaction.user.id)
self.chat_cooldowns[cooldown_key] = discord.utils.utcnow()
await self.increment_usage(interaction.guild_id)
except Exception as error:
logger.exception("Chat processing error: %s", error)
self._set_conversation_state(context_key, original_messages)
await interaction.followup.send("An unexpected error occurred. Please try again later.", ephemeral=True)
@app_commands.command(name="chat-reset", description="Reset your conversation history with the AI.")
async def chat_reset(self, interaction: discord.Interaction):
self._prune_runtime_state()
context_key = self._context_key(interaction)
self._set_conversation_state(context_key, [])
await interaction.response.send_message("Your conversation history has been reset.", ephemeral=True)
async def setup(bot: commands.Bot):
await bot.add_cog(Chat(bot))
================================================
FILE: cogs/community.py
================================================
import string
from datetime import timedelta
from typing import Dict, Optional
import discord
from discord import app_commands
from discord.ext import commands, tasks
DEFAULT_WELCOME_MESSAGE = "Welcome to {guild}, {member.mention}!"
DEFAULT_GOODBYE_MESSAGE = "{member} left {guild}."
SCHEDULE_POLL_SECONDS = 30
SCHEDULE_BATCH_SIZE = 20
TEMPLATE_FIELDS = {"member", "member.mention", "guild"}
MAX_SCHEDULE_DELIVERY_FAILURES = 5
MAX_ANNOUNCEMENT_LENGTH = 4000
SCHEDULE_RETRY_BASE_SECONDS = 300
SCHEDULE_RETRY_MAX_SECONDS = 21600
def parse_duration(value: str) -> Optional[int]:
if not value:
return None
amount = value[:-1]
unit = value[-1].lower()
if not amount.isdigit() or unit not in {"m", "h", "d"}:
return None
return int(amount) * {"m": 60, "h": 3600, "d": 86400}[unit]
class Community(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.schedule_loop.start()
def cog_unload(self):
self.schedule_loop.cancel()
async def setup_database(self):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS guild_settings (
guild_id INTEGER PRIMARY KEY,
welcome_channel_id INTEGER,
goodbye_channel_id INTEGER,
announcement_channel_id INTEGER,
modlog_channel_id INTEGER,
welcome_message TEXT,
goodbye_message TEXT
)
"""
)
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS scheduled_announcements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id INTEGER NOT NULL,
channel_id INTEGER NOT NULL,
author_id INTEGER NOT NULL,
message TEXT NOT NULL,
send_at TEXT NOT NULL,
interval_seconds INTEGER
)
"""
)
await cursor.execute("PRAGMA table_info(scheduled_announcements)")
announcement_columns = [row[1] for row in await cursor.fetchall()]
if "delivery_failures" not in announcement_columns:
await cursor.execute(
"ALTER TABLE scheduled_announcements ADD COLUMN delivery_failures INTEGER NOT NULL DEFAULT 0"
)
if "disabled" not in announcement_columns:
await cursor.execute(
"ALTER TABLE scheduled_announcements ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0"
)
if "last_error" not in announcement_columns:
await cursor.execute("ALTER TABLE scheduled_announcements ADD COLUMN last_error TEXT")
await cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_scheduled_announcements_due ON scheduled_announcements(disabled, send_at)"
)
await cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_scheduled_announcements_guild_send_at ON scheduled_announcements(guild_id, send_at)"
)
await self.bot.db.commit()
async def get_settings(self, guild_id: int) -> Dict[str, Optional[int]]:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT guild_id, welcome_channel_id, goodbye_channel_id, announcement_channel_id,
modlog_channel_id, welcome_message, goodbye_message
FROM guild_settings
WHERE guild_id = ?
""",
(guild_id,),
)
row = await cursor.fetchone()
if row is None:
return {
"guild_id": guild_id,
"welcome_channel_id": None,
"goodbye_channel_id": None,
"announcement_channel_id": None,
"modlog_channel_id": None,
"welcome_message": None,
"goodbye_message": None,
}
keys = [
"guild_id",
"welcome_channel_id",
"goodbye_channel_id",
"announcement_channel_id",
"modlog_channel_id",
"welcome_message",
"goodbye_message",
]
return dict(zip(keys, row))
async def update_setting(self, guild_id: int, field: str, value):
async with self.bot.db.cursor() as cursor:
await cursor.execute("INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)", (guild_id,))
await cursor.execute(f"UPDATE guild_settings SET {field} = ? WHERE guild_id = ?", (value, guild_id))
await self.bot.db.commit()
def validate_template(self, template: str) -> Optional[str]:
formatter = string.Formatter()
try:
for _, field_name, _, _ in formatter.parse(template):
if field_name and field_name not in TEMPLATE_FIELDS:
return (
f"Unsupported placeholder `{field_name}`. "
"Use only `{member}`, `{member.mention}`, and `{guild}`."
)
except ValueError as error:
return f"Invalid template syntax: {error}"
return None
def render_template(self, template: Optional[str], member: discord.abc.User, guild: discord.Guild, default: str) -> str:
text = template or default
return text.format(member=member, guild=guild.name)
def try_render_template(
self, template: Optional[str], member: discord.abc.User, guild: discord.Guild, default: str
) -> tuple[Optional[str], Optional[str]]:
try:
return self.render_template(template, member, guild, default), None
except Exception as error:
return None, str(error)
def schedule_retry_delay(self, failures: int) -> timedelta:
seconds = min(SCHEDULE_RETRY_BASE_SECONDS * (2 ** max(failures - 1, 0)), SCHEDULE_RETRY_MAX_SECONDS)
return timedelta(seconds=seconds)
async def _resolve_channel(self, channel_id: Optional[int]):
if channel_id is None:
return None
channel = self.bot.get_channel(channel_id)
if channel is None:
try:
channel = await self.bot.fetch_channel(channel_id)
except discord.HTTPException:
return None
return channel
async def _send_to_channel(
self,
channel_id: Optional[int],
embed: Optional[discord.Embed] = None,
content: Optional[str] = None,
) -> bool:
channel = await self._resolve_channel(channel_id)
if channel is None:
return False
try:
if content is not None:
await channel.send(content)
elif embed is not None:
await channel.send(embed=embed)
return True
except (discord.Forbidden, discord.HTTPException):
return False
async def _log_to_modlog(self, guild: discord.Guild, title: str, description: str, color: discord.Color):
settings = await self.get_settings(guild.id)
channel_id = settings["modlog_channel_id"]
if channel_id is None:
return
embed = discord.Embed(title=title, description=description, color=color, timestamp=discord.utils.utcnow())
await self._send_to_channel(channel_id, embed=embed)
def _channel_value(self, channel_id: Optional[int]) -> str:
return f"<#{channel_id}>" if channel_id else "Not set"
async def _schedule_announcement(
self,
guild_id: int,
channel_id: int,
author_id: int,
message: str,
send_at,
interval_seconds: Optional[int] = None,
) -> int:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
INSERT INTO scheduled_announcements (guild_id, channel_id, author_id, message, send_at, interval_seconds)
VALUES (?, ?, ?, ?, ?, ?)
""",
(guild_id, channel_id, author_id, message, send_at.isoformat(), interval_seconds),
)
announcement_id = cursor.lastrowid
await self.bot.db.commit()
return announcement_id
server_config = app_commands.Group(
name="server-config",
description="Configure community features for this server.",
guild_only=True,
)
announcements = app_commands.Group(
name="announcements",
description="Manage scheduled announcements for this server.",
guild_only=True,
)
@server_config.command(name="view", description="View the current server community settings.")
@app_commands.checks.has_permissions(manage_guild=True)
async def view(self, interaction: discord.Interaction):
settings = await self.get_settings(interaction.guild_id)
embed = discord.Embed(title=f"Server Config: {interaction.guild.name}", color=discord.Color.green())
embed.add_field(name="Welcome Channel", value=self._channel_value(settings["welcome_channel_id"]), inline=False)
embed.add_field(name="Goodbye Channel", value=self._channel_value(settings["goodbye_channel_id"]), inline=False)
embed.add_field(
name="Announcement Channel",
value=self._channel_value(settings["announcement_channel_id"]),
inline=False,
)
embed.add_field(name="Mod Log Channel", value=self._channel_value(settings["modlog_channel_id"]), inline=False)
embed.add_field(
name="Welcome Message",
value=settings["welcome_message"] or DEFAULT_WELCOME_MESSAGE,
inline=False,
)
embed.add_field(
name="Goodbye Message",
value=settings["goodbye_message"] or DEFAULT_GOODBYE_MESSAGE,
inline=False,
)
await interaction.response.send_message(embed=embed, ephemeral=True)
@server_config.command(name="set-welcome-channel", description="Set the welcome channel.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_welcome_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
await self.update_setting(interaction.guild_id, "welcome_channel_id", channel.id)
await interaction.response.send_message(f"Welcome channel set to {channel.mention}.", ephemeral=True)
@server_config.command(name="set-goodbye-channel", description="Set the goodbye channel.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_goodbye_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
await self.update_setting(interaction.guild_id, "goodbye_channel_id", channel.id)
await interaction.response.send_message(f"Goodbye channel set to {channel.mention}.", ephemeral=True)
@server_config.command(name="set-announcement-channel", description="Set the announcement channel.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_announcement_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
await self.update_setting(interaction.guild_id, "announcement_channel_id", channel.id)
await interaction.response.send_message(f"Announcement channel set to {channel.mention}.", ephemeral=True)
@server_config.command(name="set-modlog-channel", description="Set the moderation log channel.")
@app_commands.checks.has_permissions(manage_guild=True)
async def set_modlog_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
await self.update_setting(interaction.guild_id, "modlog_channel_id", channel.id)
await interaction.response.send_message(f"Mod log channel set to {channel.mention}.", ephemeral=True)
@server_config.command(name="set-welcome-message", description="Set the welcome message template.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(message="Use {member}, {member.mention}, and {guild}. Type reset to clear.")
async def set_welcome_message(self, interaction: discord.Interaction, message: str):
if message.lower() != "reset":
error = self.validate_template(message)
if error:
await interaction.response.send_message(error, ephemeral=True)
return
value = None if message.lower() == "reset" else message
await self.update_setting(interaction.guild_id, "welcome_message", value)
await interaction.response.send_message("Welcome message updated.", ephemeral=True)
@server_config.command(name="set-goodbye-message", description="Set the goodbye message template.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(message="Use {member}, {member.mention}, and {guild}. Type reset to clear.")
async def set_goodbye_message(self, interaction: discord.Interaction, message: str):
if message.lower() != "reset":
error = self.validate_template(message)
if error:
await interaction.response.send_message(error, ephemeral=True)
return
value = None if message.lower() == "reset" else message
await self.update_setting(interaction.guild_id, "goodbye_message", value)
await interaction.response.send_message("Goodbye message updated.", ephemeral=True)
@server_config.command(name="preview-welcome", description="Preview the welcome message template.")
@app_commands.checks.has_permissions(manage_guild=True)
async def preview_welcome(self, interaction: discord.Interaction):
settings = await self.get_settings(interaction.guild_id)
content, error = self.try_render_template(
settings["welcome_message"], interaction.user, interaction.guild, DEFAULT_WELCOME_MESSAGE
)
if error:
await interaction.response.send_message(
"The saved welcome template is invalid. Reset or update it before using previews.",
ephemeral=True,
)
return
await interaction.response.send_message(content, ephemeral=True)
@server_config.command(name="preview-goodbye", description="Preview the goodbye message template.")
@app_commands.checks.has_permissions(manage_guild=True)
async def preview_goodbye(self, interaction: discord.Interaction):
settings = await self.get_settings(interaction.guild_id)
content, error = self.try_render_template(
settings["goodbye_message"], interaction.user, interaction.guild, DEFAULT_GOODBYE_MESSAGE
)
if error:
await interaction.response.send_message(
"The saved goodbye template is invalid. Reset or update it before using previews.",
ephemeral=True,
)
return
await interaction.response.send_message(content, ephemeral=True)
@server_config.command(name="reset-message", description="Reset one message template to its default.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.choices(
target=[
app_commands.Choice(name="Welcome", value="welcome_message"),
app_commands.Choice(name="Goodbye", value="goodbye_message"),
]
)
async def reset_message(self, interaction: discord.Interaction, target: app_commands.Choice[str]):
await self.update_setting(interaction.guild_id, target.value, None)
await interaction.response.send_message(f"{target.name} message reset.", ephemeral=True)
@server_config.command(name="reset-channel", description="Reset one configured channel slot.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.choices(
target=[
app_commands.Choice(name="Welcome", value="welcome_channel_id"),
app_commands.Choice(name="Goodbye", value="goodbye_channel_id"),
app_commands.Choice(name="Announcement", value="announcement_channel_id"),
app_commands.Choice(name="Mod Log", value="modlog_channel_id"),
]
)
async def reset_channel(self, interaction: discord.Interaction, target: app_commands.Choice[str]):
await self.update_setting(interaction.guild_id, target.value, None)
await interaction.response.send_message(f"{target.name} channel reset.", ephemeral=True)
@app_commands.command(name="announce", description="Send an announcement to the configured announcement channel.")
@app_commands.guild_only()
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(message="Announcement content.")
async def announce(self, interaction: discord.Interaction, message: str):
if len(message) > MAX_ANNOUNCEMENT_LENGTH:
await interaction.response.send_message(
f"Announcement content is too long ({len(message)} chars). Maximum is {MAX_ANNOUNCEMENT_LENGTH}.",
ephemeral=True,
)
return
settings = await self.get_settings(interaction.guild_id)
target_channel_id = settings["announcement_channel_id"] or interaction.channel_id
channel = await self._resolve_channel(target_channel_id)
if channel is None:
await interaction.response.send_message(
"The configured announcement channel no longer exists. Set a new one or use this command in another channel.",
ephemeral=True,
)
return
embed = discord.Embed(description=message, color=discord.Color.blurple())
embed.set_author(name=f"Announcement from {interaction.guild.name}")
embed.set_footer(text=f"Sent by {interaction.user.display_name}")
try:
await channel.send(embed=embed)
except discord.Forbidden:
await interaction.response.send_message(
"I do not have permission to send messages in the configured announcement channel.",
ephemeral=True,
)
return
except discord.HTTPException:
await interaction.response.send_message(
"I could not send the announcement right now. Please try again later.",
ephemeral=True,
)
return
if target_channel_id == interaction.channel_id:
await interaction.response.send_message("Announcement sent in this channel.", ephemeral=True)
else:
await interaction.response.send_message(f"Announcement sent to {channel.mention}.", ephemeral=True)
@announcements.command(name="schedule", description="Schedule a future announcement.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
when="When to send it, like 10m, 2h, or 1d.",
message="Announcement content.",
repeat="Optional repeat interval like 1d. Leave empty for one-time.",
channel="Optional target channel. Defaults to the configured announcement channel or current channel.",
)
async def schedule(
self,
interaction: discord.Interaction,
when: str,
message: str,
repeat: Optional[str] = None,
channel: Optional[discord.TextChannel] = None,
):
delay_seconds = parse_duration(when)
if delay_seconds is None:
await interaction.response.send_message("Invalid `when` value. Use formats like `10m`, `2h`, or `1d`.", ephemeral=True)
return
if len(message) > MAX_ANNOUNCEMENT_LENGTH:
await interaction.response.send_message(
f"Announcement content is too long ({len(message)} chars). Maximum is {MAX_ANNOUNCEMENT_LENGTH}.",
ephemeral=True,
)
return
interval_seconds = parse_duration(repeat) if repeat else None
if repeat and interval_seconds is None:
await interaction.response.send_message("Invalid repeat interval. Use formats like `1h` or `1d`.", ephemeral=True)
return
settings = await self.get_settings(interaction.guild_id)
target_channel_id = channel.id if channel else settings["announcement_channel_id"] or interaction.channel_id
target_channel = await self._resolve_channel(target_channel_id)
if target_channel is None:
await interaction.response.send_message("The target announcement channel is unavailable.", ephemeral=True)
return
send_at = discord.utils.utcnow() + timedelta(seconds=delay_seconds)
announcement_id = await self._schedule_announcement(
interaction.guild_id,
target_channel_id,
interaction.user.id,
message,
send_at,
interval_seconds=interval_seconds,
)
schedule_text = discord.utils.format_dt(send_at, style="F")
repeat_text = "" if interval_seconds is None else f" Repeats every `{repeat}`."
await interaction.response.send_message(
f"Scheduled announcement `{announcement_id}` for {schedule_text} in {target_channel.mention}.{repeat_text}",
ephemeral=True,
)
@announcements.command(name="list", description="List scheduled announcements for this server.")
@app_commands.checks.has_permissions(manage_guild=True)
async def list_scheduled(self, interaction: discord.Interaction):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT id, channel_id, send_at, interval_seconds, message, delivery_failures, disabled, last_error
FROM scheduled_announcements
WHERE guild_id = ?
ORDER BY send_at ASC
LIMIT 20
""",
(interaction.guild_id,),
)
rows = await cursor.fetchall()
if not rows:
await interaction.response.send_message("No scheduled announcements for this server.", ephemeral=True)
return
lines = []
for announcement_id, channel_id, send_at, interval_seconds, message, delivery_failures, disabled, last_error in rows:
schedule = discord.utils.format_dt(discord.utils.parse_time(send_at), style="R")
repeat_text = "" if interval_seconds is None else f" every `{interval_seconds}s`"
status_text = " • paused" if disabled else ""
failure_text = "" if not delivery_failures else f" • failures: {delivery_failures}"
error_text = "" if not last_error else f"\nError: {last_error[:100]}"
lines.append(f"`{announcement_id}` • <#{channel_id}> • {schedule}{repeat_text}{status_text}{failure_text}\n{message[:120]}{error_text}")
embed = discord.Embed(title="Scheduled Announcements", description="\n\n".join(lines), color=discord.Color.blurple())
await interaction.response.send_message(embed=embed, ephemeral=True)
@announcements.command(name="diagnose", description="Show details and error info for a scheduled announcement.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(announcement_id="The announcement id from /announcements list.")
async def diagnose_scheduled(self, interaction: discord.Interaction, announcement_id: int):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT id, channel_id, author_id, message, send_at, interval_seconds,
delivery_failures, disabled, last_error
FROM scheduled_announcements
WHERE id = ? AND guild_id = ?
""",
(announcement_id, interaction.guild_id),
)
row = await cursor.fetchone()
if not row:
await interaction.response.send_message("Scheduled announcement not found.", ephemeral=True)
return
ann_id, channel_id, author_id, message, send_at, interval_seconds, failures, disabled, last_error = row
embed = discord.Embed(
title=f"Announcement `{ann_id}` Details",
color=discord.Color.orange() if disabled else discord.Color.blurple(),
)
embed.add_field(name="Channel", value=f"<#{channel_id}>", inline=True)
embed.add_field(name="Scheduled By", value=f"<@{author_id}>", inline=True)
embed.add_field(name="Next Send", value=discord.utils.format_dt(discord.utils.parse_time(send_at), style="F"), inline=False)
if interval_seconds:
embed.add_field(name="Repeat Interval", value=f"{interval_seconds}s", inline=True)
embed.add_field(name="Status", value="Paused (too many failures)" if disabled else "Active", inline=True)
embed.add_field(name="Delivery Failures", value=str(failures), inline=True)
if last_error:
embed.add_field(name="Last Error", value=last_error[:1024], inline=False)
embed.add_field(name="Content", value=message[:1024], inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
@announcements.command(name="cancel", description="Cancel a scheduled announcement.")
@app_commands.checks.has_permissions(manage_guild=True)
async def cancel_scheduled(self, interaction: discord.Interaction, announcement_id: int):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"DELETE FROM scheduled_announcements WHERE id = ? AND guild_id = ?",
(announcement_id, interaction.guild_id),
)
deleted = cursor.rowcount
await self.bot.db.commit()
if deleted:
await interaction.response.send_message(f"Scheduled announcement `{announcement_id}` canceled.", ephemeral=True)
else:
await interaction.response.send_message("Scheduled announcement not found.", ephemeral=True)
@tasks.loop(seconds=SCHEDULE_POLL_SECONDS)
async def schedule_loop(self):
now = discord.utils.utcnow().isoformat()
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT id, guild_id, channel_id, author_id, message, send_at, interval_seconds, delivery_failures
FROM scheduled_announcements
WHERE disabled = 0 AND send_at <= ?
ORDER BY send_at ASC
LIMIT ?
""",
(now, SCHEDULE_BATCH_SIZE),
)
rows = await cursor.fetchall()
for announcement_id, guild_id, channel_id, author_id, message, send_at, interval_seconds, delivery_failures in rows:
guild = self.bot.get_guild(guild_id)
channel = await self._resolve_channel(channel_id)
delivered = False
failure_reason = None
if guild is not None and channel is not None:
embed = discord.Embed(description=message, color=discord.Color.blurple())
embed.set_author(name=f"Scheduled announcement from {guild.name}")
user = guild.get_member(author_id)
footer_name = user.display_name if user else str(author_id)
embed.set_footer(text=f"Scheduled by {footer_name}")
try:
await channel.send(embed=embed)
delivered = True
except (discord.Forbidden, discord.HTTPException):
delivered = False
failure_reason = "failed to send the scheduled announcement in the configured channel"
elif guild is None:
failure_reason = "the target guild is unavailable to the bot"
else:
failure_reason = "the configured announcement channel is unavailable"
async with self.bot.db.cursor() as cursor:
if delivered and interval_seconds:
next_time = discord.utils.parse_time(send_at)
while next_time <= discord.utils.utcnow():
next_time += timedelta(seconds=interval_seconds)
await cursor.execute(
"""
UPDATE scheduled_announcements
SET send_at = ?, delivery_failures = 0, disabled = 0, last_error = NULL
WHERE id = ?
""",
(next_time.isoformat(), announcement_id),
)
elif delivered:
await cursor.execute("DELETE FROM scheduled_announcements WHERE id = ?", (announcement_id,))
else:
next_failures = delivery_failures + 1
if next_failures >= MAX_SCHEDULE_DELIVERY_FAILURES:
await cursor.execute(
"""
UPDATE scheduled_announcements
SET delivery_failures = ?, disabled = 1, last_error = ?
WHERE id = ?
""",
(next_failures, failure_reason, announcement_id),
)
if guild is not None:
await self._log_to_modlog(
guild,
"Scheduled Announcement Disabled",
f"Announcement `{announcement_id}` was paused after repeated delivery failures.\nReason: {failure_reason}",
discord.Color.orange(),
)
else:
retry_at = discord.utils.utcnow() + self.schedule_retry_delay(next_failures)
await cursor.execute(
"""
UPDATE scheduled_announcements
SET send_at = ?, delivery_failures = ?, last_error = ?
WHERE id = ?
""",
(retry_at.isoformat(), next_failures, failure_reason, announcement_id),
)
await self.bot.db.commit()
@schedule_loop.before_loop
async def before_schedule_loop(self):
await self.bot.wait_until_ready()
await self.setup_database()
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
settings = await self.get_settings(member.guild.id)
if settings["welcome_channel_id"]:
try:
text = self.render_template(settings["welcome_message"], member, member.guild, DEFAULT_WELCOME_MESSAGE)
await self._send_to_channel(settings["welcome_channel_id"], content=text)
except Exception:
await self._log_to_modlog(
member.guild,
"Welcome Message Error",
"A welcome template could not be rendered. Reset or update the welcome message template.",
discord.Color.red(),
)
await self._log_to_modlog(
member.guild,
"Member Joined",
f"{member.mention} joined the server.",
discord.Color.green(),
)
@commands.Cog.listener()
async def on_member_remove(self, member: discord.Member):
settings = await self.get_settings(member.guild.id)
if settings["goodbye_channel_id"]:
try:
text = self.render_template(settings["goodbye_message"], member, member.guild, DEFAULT_GOODBYE_MESSAGE)
await self._send_to_channel(settings["goodbye_channel_id"], content=text)
except Exception:
await self._log_to_modlog(
member.guild,
"Goodbye Message Error",
"A goodbye template could not be rendered. Reset or update the goodbye message template.",
discord.Color.red(),
)
await self._log_to_modlog(
member.guild,
"Member Left",
f"{member} left the server.",
discord.Color.orange(),
)
@commands.Cog.listener()
async def on_message_delete(self, message: discord.Message):
if message.guild is None or message.author.bot:
return
content = message.content.strip()
if not content:
content = "[no text content]"
elif len(content) > 500:
content = f"{content[:497]}..."
await self._log_to_modlog(
message.guild,
"Message Deleted",
f"Author: {message.author.mention}\nChannel: {message.channel.mention}\nContent: {content}",
discord.Color.red(),
)
async def setup(bot: commands.Bot):
await bot.add_cog(Community(bot))
================================================
FILE: cogs/economy.py
================================================
import asyncio
import logging
import random
import discord
from discord import app_commands
from discord.ext import commands
logger = logging.getLogger(__name__)
# Constants
DEFAULT_BALANCE = 100
DAILY_MIN = 100
DAILY_MAX = 500
DAILY_COOLDOWN = 86400 # 24 hours
FREELANCE_MIN = 25
FREELANCE_MAX = 75
FREELANCE_COOLDOWN = 900 # 15 minutes
REGULAR_MIN = 100
REGULAR_MAX = 300
REGULAR_COOLDOWN = 3600 # 1 hour
CRIME_MIN = 500
CRIME_MAX = 1500
CRIME_FINE_MIN = 200
CRIME_FINE_MAX = 750
CRIME_SUCCESS_RATE = 0.50
CRIME_COOLDOWN = 21600 # 6 hours
ROB_SUCCESS_RATE = 0.40
ROB_MIN_VICTIM_BALANCE = 200
ROB_COOLDOWN = 1800 # 30 minutes
SLOTS_MIN_BET = 10
class Economy(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self._guild_locks: dict[int, asyncio.Lock] = {}
self.bot.loop.create_task(self.setup_database())
def _guild_lock(self, guild_id: int) -> asyncio.Lock:
lock = self._guild_locks.get(guild_id)
if lock is None:
lock = asyncio.Lock()
self._guild_locks[guild_id] = lock
return lock
async def setup_database(self):
columns = []
async with self.bot.db.execute("PRAGMA table_info(users)") as cursor:
columns = [row[1] for row in await cursor.fetchall()]
if columns and "guild_id" not in columns:
async with self.bot.db.cursor() as cursor:
await cursor.execute("ALTER TABLE users RENAME TO users_legacy")
await cursor.execute(
"""
CREATE TABLE users (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
balance INTEGER NOT NULL DEFAULT 100,
PRIMARY KEY (guild_id, user_id)
)
"""
)
await cursor.execute(
"""
INSERT OR IGNORE INTO users (guild_id, user_id, balance)
SELECT DISTINCT COALESCE(messages.guild_id, 0), legacy.user_id, legacy.balance
FROM users_legacy AS legacy
LEFT JOIN messages
ON messages.user_id = legacy.user_id
AND messages.guild_id IS NOT NULL
"""
)
logger.info("Migrated global economy balances to per-guild records.")
else:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS users (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
balance INTEGER NOT NULL DEFAULT 100,
PRIMARY KEY (guild_id, user_id)
)
"""
)
await self.bot.db.commit()
def _get_guild_id(self, interaction: discord.Interaction) -> int:
if interaction.guild_id is None:
raise app_commands.CheckFailure("This command can only be used in a server.")
return interaction.guild_id
async def _get_or_create_user_unlocked(self, guild_id: int, user_id: int) -> int:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"SELECT balance FROM users WHERE guild_id = ? AND user_id = ?",
(guild_id, user_id),
)
result = await cursor.fetchone()
if result is None:
await cursor.execute(
"INSERT INTO users (guild_id, user_id, balance) VALUES (?, ?, ?)",
(guild_id, user_id, DEFAULT_BALANCE),
)
await self.bot.db.commit()
return DEFAULT_BALANCE
return result[0]
async def get_or_create_user(self, guild_id: int, user_id: int) -> int:
async with self._guild_lock(guild_id):
return await self._get_or_create_user_unlocked(guild_id, user_id)
async def change_balance(self, guild_id: int, user_id: int, delta: int) -> int:
async with self._guild_lock(guild_id):
balance = await self._get_or_create_user_unlocked(guild_id, user_id)
new_balance = balance + delta
if new_balance < 0:
raise ValueError("Balance cannot be negative.")
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(new_balance, guild_id, user_id),
)
await self.bot.db.commit()
return new_balance
async def transfer_balance(self, guild_id: int, sender_id: int, receiver_id: int, amount: int) -> tuple[int, int]:
async with self._guild_lock(guild_id):
sender_balance = await self._get_or_create_user_unlocked(guild_id, sender_id)
if sender_balance < amount:
raise ValueError("You do not have enough coins to make this transfer.")
receiver_balance = await self._get_or_create_user_unlocked(guild_id, receiver_id)
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(sender_balance - amount, guild_id, sender_id),
)
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(receiver_balance + amount, guild_id, receiver_id),
)
await self.bot.db.commit()
return sender_balance - amount, receiver_balance + amount
async def set_balance(self, guild_id: int, user_id: int, amount: int) -> int:
async with self._guild_lock(guild_id):
await self._get_or_create_user_unlocked(guild_id, user_id)
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(amount, guild_id, user_id),
)
await self.bot.db.commit()
return amount
jobs = app_commands.Group(name="jobs", description="Perform various jobs to earn coins.", guild_only=True)
admin = app_commands.Group(name="economy-admin", description="Administer the server economy.", guild_only=True)
@app_commands.command(name="balance", description="Check your or another member's coin balance.")
@app_commands.guild_only()
@app_commands.describe(member="The member whose balance you want to see.")
@app_commands.checks.cooldown(1, 10, key=lambda i: (i.guild_id, i.user.id))
async def balance(self, interaction: discord.Interaction, member: discord.Member = None):
guild_id = self._get_guild_id(interaction)
target_member = member or interaction.user
balance = await self.get_or_create_user(guild_id, target_member.id)
embed = discord.Embed(title=f"{target_member.name}'s Balance", color=discord.Color.green())
embed.add_field(name="Coins", value=f"🪙 {balance}")
await interaction.response.send_message(embed=embed)
@app_commands.command(name="daily", description="Claim your daily reward.")
@app_commands.guild_only()
@app_commands.checks.cooldown(1, DAILY_COOLDOWN, key=lambda i: (i.guild_id, i.user.id))
async def daily(self, interaction: discord.Interaction):
guild_id = self._get_guild_id(interaction)
daily_amount = random.randint(DAILY_MIN, DAILY_MAX)
await self.change_balance(guild_id, interaction.user.id, daily_amount)
embed = discord.Embed(
title="Daily Reward!",
description=f"You have claimed your daily reward of 🪙 **{daily_amount}** coins!",
color=discord.Color.gold(),
)
await interaction.response.send_message(embed=embed)
@daily.error
async def daily_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
seconds = error.retry_after
hours, remainder = divmod(int(seconds), 3600)
minutes, _ = divmod(remainder, 60)
await interaction.response.send_message(
f"You've already claimed your daily reward. Please try again in **{hours}h {minutes}m**.",
ephemeral=True,
)
@jobs.command(name="freelance", description="Do a quick freelance job for some extra cash.")
@app_commands.checks.cooldown(1, FREELANCE_COOLDOWN, key=lambda i: (i.guild_id, i.user.id))
async def jobs_freelance(self, interaction: discord.Interaction):
guild_id = self._get_guild_id(interaction)
amount = random.randint(FREELANCE_MIN, FREELANCE_MAX)
await self.change_balance(guild_id, interaction.user.id, amount)
messages = [
f"You designed a logo for a local startup and earned 🪙 **{amount}**.",
f"You wrote a short article for a blog and got paid 🪙 **{amount}**.",
f"You helped someone with their homework and they tipped you 🪙 **{amount}**.",
]
await interaction.response.send_message(random.choice(messages))
@jobs_freelance.error
async def freelance_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
minutes = int(error.retry_after / 60)
await interaction.response.send_message(
f"You need a break. You can do another freelance job in **{minutes}** minutes.", ephemeral=True
)
@jobs.command(name="regular", description="Work your regular shift for a steady income.")
@app_commands.checks.cooldown(1, REGULAR_COOLDOWN, key=lambda i: (i.guild_id, i.user.id))
async def jobs_regular(self, interaction: discord.Interaction):
guild_id = self._get_guild_id(interaction)
amount = random.randint(REGULAR_MIN, REGULAR_MAX)
await self.change_balance(guild_id, interaction.user.id, amount)
messages = [
f"You completed your shift as a programmer and earned 🪙 **{amount}**.",
f"You spent the day as a server janitor and got paid 🪙 **{amount}**.",
f"You delivered pizzas all afternoon and made 🪙 **{amount}**.",
]
await interaction.response.send_message(random.choice(messages))
@jobs_regular.error
async def regular_work_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
minutes = int(error.retry_after / 60)
await interaction.response.send_message(
f"You're tired from your shift. You can work again in **{minutes}** minutes.", ephemeral=True
)
@jobs.command(name="crime", description="Commit a crime for a high reward, but with high risk.")
@app_commands.checks.cooldown(1, CRIME_COOLDOWN, key=lambda i: (i.guild_id, i.user.id))
async def jobs_crime(self, interaction: discord.Interaction):
guild_id = self._get_guild_id(interaction)
balance = await self.get_or_create_user(guild_id, interaction.user.id)
if random.random() < CRIME_SUCCESS_RATE:
payout = random.randint(CRIME_MIN, CRIME_MAX)
await self.change_balance(guild_id, interaction.user.id, payout)
await interaction.response.send_message(
f"🚨 **Success!** Your high-stakes bank heist went perfectly. You got away with 🪙 **{payout}**!"
)
else:
fine = min(balance, random.randint(CRIME_FINE_MIN, CRIME_FINE_MAX))
await self.change_balance(guild_id, interaction.user.id, -fine)
await interaction.response.send_message(
f"👮♂️ **BUSTED!** The silent alarm tripped during your operation. You were caught and fined 🪙 **{fine}**."
)
@jobs_crime.error
async def crime_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
hours = int(error.retry_after / 3600)
await interaction.response.send_message(
f"You need to lay low for a while. You can try another crime in **{hours}** hours.", ephemeral=True
)
@app_commands.command(name="gamble", description="Gamble your coins for a chance to win big.")
@app_commands.guild_only()
@app_commands.describe(amount="The amount of coins you want to gamble.")
@app_commands.checks.cooldown(1, 10, key=lambda i: (i.guild_id, i.user.id))
async def gamble(self, interaction: discord.Interaction, amount: app_commands.Range[int, 1]):
guild_id = self._get_guild_id(interaction)
balance = await self.get_or_create_user(guild_id, interaction.user.id)
if amount > balance:
await interaction.response.send_message("You don't have enough coins to gamble that much.", ephemeral=True)
return
win = random.choice([True, False, False])
delta = amount if win else -amount
new_balance = await self.change_balance(guild_id, interaction.user.id, delta)
if win:
await interaction.response.send_message(
f"🎉 **You won!** You gambled {amount} and won {amount} coins! Your new balance is 🪙 {new_balance}."
)
else:
await interaction.response.send_message(
f"💀 **You lost!** You gambled {amount} and lost it all. Your new balance is 🪙 {new_balance}."
)
@app_commands.command(name="leaderboard", description="Shows the top 10 richest users in the server.")
@app_commands.guild_only()
@app_commands.checks.cooldown(1, 30, key=lambda i: i.guild_id)
async def leaderboard(self, interaction: discord.Interaction):
guild_id = self._get_guild_id(interaction)
await interaction.response.defer()
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"SELECT user_id, balance FROM users WHERE guild_id = ? ORDER BY balance DESC LIMIT 10",
(guild_id,),
)
top_users = await cursor.fetchall()
if not top_users:
await interaction.followup.send("The leaderboard is empty!")
return
embed = discord.Embed(title="🏆 Server Coin Leaderboard 🏆", color=discord.Color.gold())
leaderboard_text = ""
for rank, (user_id, balance) in enumerate(top_users, start=1):
member = interaction.guild.get_member(user_id)
if member is None:
user_name = f"Unknown User (ID: {user_id})"
else:
user_name = member.display_name
leaderboard_text += f"{rank}. {user_name} - 🪙 {balance}\n"
embed.description = leaderboard_text
await interaction.followup.send(embed=embed)
@app_commands.command(name="transfer", description="Transfer coins to another member.")
@app_commands.guild_only()
@app_commands.describe(
member="The member you want to transfer coins to.", amount="The amount of coins to transfer."
)
@app_commands.checks.cooldown(1, 15, key=lambda i: (i.guild_id, i.user.id))
async def transfer(
self, interaction: discord.Interaction, member: discord.Member, amount: app_commands.Range[int, 1]
):
guild_id = self._get_guild_id(interaction)
sender_id = interaction.user.id
receiver_id = member.id
if sender_id == receiver_id:
await interaction.response.send_message("You cannot transfer coins to yourself.", ephemeral=True)
return
if member.bot:
await interaction.response.send_message("You cannot transfer coins to bots.", ephemeral=True)
return
try:
await self.transfer_balance(guild_id, sender_id, receiver_id, amount)
except ValueError as error:
await interaction.response.send_message(str(error), ephemeral=True)
return
await interaction.response.send_message(
f"💸 You have successfully transferred 🪙 **{amount}** coins to {member.mention}!"
)
@app_commands.command(name="rob", description="Attempt to rob coins from another member.")
@app_commands.guild_only()
@app_commands.describe(member="The member you want to rob.")
@app_commands.checks.cooldown(1, ROB_COOLDOWN, key=lambda i: (i.guild_id, i.user.id))
async def rob(self, interaction: discord.Interaction, member: discord.Member):
guild_id = self._get_guild_id(interaction)
robber_id = interaction.user.id
victim_id = member.id
if robber_id == victim_id:
await interaction.response.send_message("You can't rob yourself, you silly goose!", ephemeral=True)
return
if member.bot:
await interaction.response.send_message("Bots are not part of the economy.", ephemeral=True)
return
async with self._guild_lock(guild_id):
robber_balance = await self._get_or_create_user_unlocked(guild_id, robber_id)
victim_balance = await self._get_or_create_user_unlocked(guild_id, victim_id)
if victim_balance < ROB_MIN_VICTIM_BALANCE:
self.rob.reset_cooldown(interaction)
await interaction.response.send_message(
f"{member.name} is too poor to be worth robbing.", ephemeral=True
)
return
async with self.bot.db.cursor() as cursor:
if random.random() < ROB_SUCCESS_RATE:
robbed_amount = random.randint(int(victim_balance * 0.1), int(victim_balance * 0.25))
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(robber_balance + robbed_amount, guild_id, robber_id),
)
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(victim_balance - robbed_amount, guild_id, victim_id),
)
message = f"🚨 Success! You discreetly robbed 🪙 **{robbed_amount}** from {member.mention}!"
else:
fine_amount = min(robber_balance, random.randint(int(robber_balance * 0.1), int(robber_balance * 0.2)))
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(robber_balance - fine_amount, guild_id, robber_id),
)
message = (
f"👮♂️ Busted! Your robbery attempt on {member.mention} failed and you were fined 🪙 "
f"**{fine_amount}**."
)
await self.bot.db.commit()
await interaction.response.send_message(message)
@rob.error
async def rob_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.CommandOnCooldown):
minutes = int(error.retry_after / 60)
await interaction.response.send_message(
f"You're on a cooldown. You can attempt another robbery in **{minutes}** minutes.", ephemeral=True
)
@app_commands.command(name="slots", description="Play the slot machine.")
@app_commands.guild_only()
@app_commands.describe(bet="The amount of coins you want to bet.")
@app_commands.checks.cooldown(1, 5, key=lambda i: (i.guild_id, i.user.id))
async def slots(self, interaction: discord.Interaction, bet: app_commands.Range[int, SLOTS_MIN_BET]):
guild_id = self._get_guild_id(interaction)
balance = await self.get_or_create_user(guild_id, interaction.user.id)
if bet > balance:
await interaction.response.send_message("You don't have enough coins to bet that much.", ephemeral=True)
return
reels = ["🍒", "🍊", "🍋", "🔔", "⭐", "💎"]
spin = [random.choice(reels) for _ in range(3)]
result_text = f"**[ {spin[0]} | {spin[1]} | {spin[2]} ]**\n\n"
if spin[0] == spin[1] == spin[2]:
if spin[0] == "💎":
winnings = bet * 20
result_text += f"💎 JACKPOT! 💎 You won **{winnings}** coins!"
else:
winnings = bet * 10
result_text += f"🎉 BIG WIN! 🎉 You won **{winnings}** coins!"
elif spin[0] == spin[1] or spin[1] == spin[2]:
winnings = bet * 2
result_text += f"👍 Nice! You won **{winnings}** coins!"
else:
winnings = -bet
result_text += "☠️ Aw, tough luck! You lost your bet."
new_balance = await self.change_balance(guild_id, interaction.user.id, winnings)
embed = discord.Embed(title="🎰 Slot Machine 🎰", description=result_text, color=discord.Color.dark_magenta())
embed.set_footer(text=f"Your new balance is 🪙 {new_balance}")
await interaction.response.send_message(embed=embed)
@admin.command(name="add", description="Add coins to a member.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(member="The target member.", amount="How many coins to add.")
async def admin_add(self, interaction: discord.Interaction, member: discord.Member, amount: app_commands.Range[int, 1]):
guild_id = self._get_guild_id(interaction)
balance = await self.change_balance(guild_id, member.id, amount)
await interaction.response.send_message(
f"Added 🪙 **{amount}** to {member.mention}. New balance: 🪙 {balance}.",
ephemeral=True,
)
@admin.command(name="remove", description="Remove coins from a member.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(member="The target member.", amount="How many coins to remove.")
async def admin_remove(
self, interaction: discord.Interaction, member: discord.Member, amount: app_commands.Range[int, 1]
):
guild_id = self._get_guild_id(interaction)
current = await self.get_or_create_user(guild_id, member.id)
removed = min(current, amount)
balance = await self.change_balance(guild_id, member.id, -removed)
await interaction.response.send_message(
f"Removed 🪙 **{removed}** from {member.mention}. New balance: 🪙 {balance}.",
ephemeral=True,
)
@admin.command(name="set", description="Set a member's balance exactly.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(member="The target member.", amount="The exact balance to set.")
async def admin_set(self, interaction: discord.Interaction, member: discord.Member, amount: app_commands.Range[int, 0]):
guild_id = self._get_guild_id(interaction)
balance = await self.set_balance(guild_id, member.id, amount)
await interaction.response.send_message(
f"Set {member.mention}'s balance to 🪙 **{balance}**.",
ephemeral=True,
)
@admin.command(name="reset-guild", description="Reset the server economy for all users.")
@app_commands.checks.has_permissions(administrator=True)
async def admin_reset_guild(self, interaction: discord.Interaction):
guild_id = self._get_guild_id(interaction)
async with self._guild_lock(guild_id):
async with self.bot.db.cursor() as cursor:
await cursor.execute("DELETE FROM users WHERE guild_id = ?", (guild_id,))
await self.bot.db.commit()
await interaction.response.send_message("The server economy has been reset.", ephemeral=True)
async def setup(bot: commands.Bot):
await bot.add_cog(Economy(bot))
================================================
FILE: cogs/farming.py
================================================
import logging
import math
import random
from datetime import datetime, timedelta
import discord
from discord import app_commands
from discord.ext import commands
logger = logging.getLogger(__name__)
# Constants
PEST_EVENT_CHANCE = 0.10
PEST_REWARD_MULTIPLIER = 0.5
BOUNTIFUL_EVENT_CHANCE = 0.95
BOUNTIFUL_REWARD_MULTIPLIER = 1.5
BOUNTIFUL_XP_MULTIPLIER = 1.5
class Farming(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.bot.loop.create_task(self.setup_database())
self.crops = {
"wheat": {"name": "Wheat 🌾", "cost": 10, "growth": 600, "reward": 25, "xp": 5, "level": 1},
"potato": {"name": "Potato 🥔", "cost": 25, "growth": 1800, "reward": 75, "xp": 15, "level": 2},
"carrot": {"name": "Carrot 🥕", "cost": 50, "growth": 3600, "reward": 180, "xp": 30, "level": 5},
"strawberry": {"name": "Strawberry 🍓", "cost": 150, "growth": 7200, "reward": 500, "xp": 100, "level": 10},
}
self.land_types = {
1: {"name": "Barren Land", "mod": 1.0, "cost": 0},
2: {"name": "Decent Soil", "mod": 0.9, "cost": 1000},
3: {"name": "Fertile Land", "mod": 0.75, "cost": 5000},
}
async def setup_database(self):
columns = []
async with self.bot.db.execute("PRAGMA table_info(farms)") as cursor:
columns = [row[1] for row in await cursor.fetchall()]
if columns and "guild_id" not in columns:
async with self.bot.db.cursor() as cursor:
await cursor.execute("ALTER TABLE farms RENAME TO farms_legacy")
await cursor.execute(
"""
CREATE TABLE farms (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
crop TEXT,
plant_time TEXT,
land_type INTEGER DEFAULT 1,
level INTEGER DEFAULT 1,
xp INTEGER DEFAULT 0,
PRIMARY KEY (guild_id, user_id)
)
"""
)
await cursor.execute(
"""
INSERT OR IGNORE INTO farms (guild_id, user_id, crop, plant_time, land_type, level, xp)
SELECT DISTINCT
COALESCE(messages.guild_id, 0),
legacy.user_id,
legacy.crop,
legacy.plant_time,
legacy.land_type,
legacy.level,
legacy.xp
FROM farms_legacy AS legacy
LEFT JOIN messages
ON messages.user_id = legacy.user_id
AND messages.guild_id IS NOT NULL
"""
)
logger.info("Migrated global farm data to per-guild records.")
else:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS farms (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
crop TEXT,
plant_time TEXT,
land_type INTEGER DEFAULT 1,
level INTEGER DEFAULT 1,
xp INTEGER DEFAULT 0,
PRIMARY KEY (guild_id, user_id)
)
"""
)
await self.bot.db.commit()
async def get_farm_data(self, guild_id: int, user_id: int):
async with self.bot.db.cursor() as cursor:
await cursor.execute("SELECT * FROM farms WHERE guild_id = ? AND user_id = ?", (guild_id, user_id))
result = await cursor.fetchone()
if not result:
await cursor.execute("INSERT INTO farms (guild_id, user_id) VALUES (?, ?)", (guild_id, user_id))
await self.bot.db.commit()
await cursor.execute("SELECT * FROM farms WHERE guild_id = ? AND user_id = ?", (guild_id, user_id))
result = await cursor.fetchone()
keys = ["guild_id", "user_id", "crop", "plant_time", "land_type", "level", "xp"]
return dict(zip(keys, result))
def get_xp_for_next_level(self, level: int):
return math.floor(100 * (level**1.5))
farm = app_commands.Group(name="farm", description="Manage your farm, grow crops, and level up.", guild_only=True)
@farm.command(name="profile", description="View your farm profile, including level and status.")
async def profile(self, interaction: discord.Interaction):
await interaction.response.defer()
farm_data = await self.get_farm_data(interaction.guild_id, interaction.user.id)
embed = discord.Embed(title=f"{interaction.user.name}'s Farm Profile", color=discord.Color.green())
level = farm_data["level"]
xp = farm_data["xp"]
xp_needed = self.get_xp_for_next_level(level)
progress = min(int((xp / xp_needed) * 20), 20)
progress_bar = "🟩" * progress + "⬛" * (20 - progress)
embed.add_field(name="📜 Level", value=f"**{level}**", inline=True)
embed.add_field(name="🌱 XP", value=f"{xp} / {xp_needed}", inline=True)
embed.add_field(name="📊 Progress", value=f"`{progress_bar}`", inline=False)
embed.add_field(name="🏞️ Land", value=self.land_types[farm_data["land_type"]]["name"], inline=True)
if not farm_data["crop"]:
embed.add_field(name="🌾 Current Crop", value="Plot is empty.", inline=True)
else:
crop_name = farm_data["crop"]
crop_data = self.crops[crop_name]
plant_time = datetime.fromisoformat(farm_data["plant_time"])
land_mod = self.land_types[farm_data["land_type"]]["mod"]
harvest_time = plant_time + timedelta(seconds=crop_data["growth"] * land_mod)
embed.add_field(name="🌾 Current Crop", value=crop_data["name"], inline=True)
if datetime.utcnow() >= harvest_time:
embed.add_field(name="⏰ Status", value="✅ **Ready to Harvest!**", inline=False)
else:
embed.add_field(
name="⏰ Ready In", value=f"{discord.utils.format_dt(harvest_time, style='R')}", inline=False
)
await interaction.followup.send(embed=embed)
@farm.command(name="shop", description="Open the seed shop to buy new crops.")
async def shop(self, interaction: discord.Interaction):
farm_data = await self.get_farm_data(interaction.guild_id, interaction.user.id)
user_level = farm_data["level"]
embed = discord.Embed(
title="🌱 Seed Shop", description="Select a seed to plant from the shop.", color=discord.Color.dark_gold()
)
for crop in self.crops.values():
unlocked = user_level >= crop["level"]
emoji = "✅" if unlocked else "🔒"
embed.add_field(
name=f"{crop['name']} (Lvl {crop['level']}) {emoji}",
value=f"Cost: 🪙 {crop['cost']}\nReward: 🪙 {crop['reward']}\nXP: {crop['xp']}",
inline=True,
)
await interaction.response.send_message(embed=embed)
@farm.command(name="plant", description="Plant a crop in your farm.")
@app_commands.describe(crop="The name of the crop to plant (e.g., wheat, potato).")
async def plant(self, interaction: discord.Interaction, crop: str):
guild_id = interaction.guild_id
crop_name = crop.lower()
if crop_name not in self.crops:
await interaction.response.send_message(
"That's not a valid crop! Check the `/farm shop` for options.", ephemeral=True
)
return
farm_data = await self.get_farm_data(guild_id, interaction.user.id)
if farm_data["crop"]:
await interaction.response.send_message("You already have a crop growing!", ephemeral=True)
return
crop_data = self.crops[crop_name]
if farm_data["level"] < crop_data["level"]:
await interaction.response.send_message(
f"You're not a high enough level! You need to be level {crop_data['level']} to plant {crop_data['name']}.",
ephemeral=True,
)
return
economy_cog = self.bot.get_cog("Economy")
if not economy_cog:
await interaction.response.send_message("The economy system is currently unavailable.", ephemeral=True)
return
async with economy_cog._guild_lock(guild_id):
user_balance = await economy_cog._get_or_create_user_unlocked(guild_id, interaction.user.id)
if user_balance < crop_data["cost"]:
await interaction.response.send_message(
f"You don't have enough coins! Planting {crop_data['name']} costs 🪙 {crop_data['cost']}.",
ephemeral=True,
)
return
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(user_balance - crop_data["cost"], guild_id, interaction.user.id),
)
await cursor.execute(
"UPDATE farms SET crop = ?, plant_time = ? WHERE guild_id = ? AND user_id = ?",
(crop_name, datetime.utcnow().isoformat(), guild_id, interaction.user.id),
)
await self.bot.db.commit()
land_mod = self.land_types[farm_data["land_type"]]["mod"]
growth_seconds = crop_data["growth"] * land_mod
growth_hours = round(growth_seconds / 3600, 1)
await interaction.response.send_message(
f"You've successfully planted **{crop_data['name']}** for 🪙 {crop_data['cost']}! It will be ready in {growth_hours} hours."
)
@farm.command(name="harvest", description="Harvest your fully grown crop for coins and XP.")
async def harvest(self, interaction: discord.Interaction):
guild_id = interaction.guild_id
farm_data = await self.get_farm_data(guild_id, interaction.user.id)
if not farm_data["crop"]:
await interaction.response.send_message("You don't have anything planted right now.", ephemeral=True)
return
crop_name = farm_data["crop"]
crop_data = self.crops[crop_name]
plant_time = datetime.fromisoformat(farm_data["plant_time"])
land_mod = self.land_types[farm_data["land_type"]]["mod"]
harvest_time = plant_time + timedelta(seconds=crop_data["growth"] * land_mod)
if datetime.utcnow() < harvest_time:
await interaction.response.send_message(
f"Your {crop_data['name']} isn't ready yet! Come back in {discord.utils.format_dt(harvest_time, style='R')}.",
ephemeral=True,
)
return
economy_cog = self.bot.get_cog("Economy")
if not economy_cog:
await interaction.response.send_message("The economy system is currently unavailable.", ephemeral=True)
return
reward = crop_data["reward"]
xp_gain = crop_data["xp"]
event_message = ""
roll = random.random()
if roll < PEST_EVENT_CHANCE:
reward = int(reward * PEST_REWARD_MULTIPLIER)
event_message = "\n\n**Oh no! A swarm of pests ate half your harvest!** 🐛"
elif roll > BOUNTIFUL_EVENT_CHANCE:
reward = int(reward * BOUNTIFUL_REWARD_MULTIPLIER)
xp_gain = int(xp_gain * BOUNTIFUL_XP_MULTIPLIER)
event_message = "\n\n**Amazing! A bountiful harvest! You got extra coins and XP!** ✨"
new_xp = farm_data["xp"] + xp_gain
current_level = farm_data["level"]
xp_needed = self.get_xp_for_next_level(current_level)
level_up_message = ""
while new_xp >= xp_needed:
current_level += 1
new_xp -= xp_needed
xp_needed = self.get_xp_for_next_level(current_level)
level_up_message += f"\n🎉 **LEVEL UP! You are now Farm Level {current_level}!** 🎉"
async with economy_cog._guild_lock(guild_id):
user_balance = await economy_cog._get_or_create_user_unlocked(guild_id, interaction.user.id)
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(user_balance + reward, guild_id, interaction.user.id),
)
await cursor.execute(
"""
UPDATE farms
SET crop = NULL, plant_time = NULL, level = ?, xp = ?
WHERE guild_id = ? AND user_id = ?
""",
(current_level, new_xp, guild_id, interaction.user.id),
)
await self.bot.db.commit()
await interaction.response.send_message(
f"You harvested your **{crop_data['name']}** and earned 🪙 **{reward}** and **{xp_gain} XP**!"
f"{event_message}{level_up_message}"
)
@farm.command(name="upgrade", description="Upgrade your farm land for faster growth times.")
async def upgrade(self, interaction: discord.Interaction):
guild_id = interaction.guild_id
farm_data = await self.get_farm_data(guild_id, interaction.user.id)
current_land_level = farm_data["land_type"]
if current_land_level >= len(self.land_types):
await interaction.response.send_message("You already have the best land available!", ephemeral=True)
return
next_land_level = current_land_level + 1
upgrade_data = self.land_types[next_land_level]
economy_cog = self.bot.get_cog("Economy")
if not economy_cog:
await interaction.response.send_message("The economy system is currently unavailable.", ephemeral=True)
return
async with economy_cog._guild_lock(guild_id):
user_balance = await economy_cog._get_or_create_user_unlocked(guild_id, interaction.user.id)
if user_balance < upgrade_data["cost"]:
await interaction.response.send_message(
f"You don't have enough coins! Upgrading to **{upgrade_data['name']}** costs 🪙 {upgrade_data['cost']}.",
ephemeral=True,
)
return
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"UPDATE users SET balance = ? WHERE guild_id = ? AND user_id = ?",
(user_balance - upgrade_data["cost"], guild_id, interaction.user.id),
)
await cursor.execute(
"UPDATE farms SET land_type = ? WHERE guild_id = ? AND user_id = ?",
(next_land_level, guild_id, interaction.user.id),
)
await self.bot.db.commit()
await interaction.response.send_message(
f"Congratulations! You've spent 🪙 {upgrade_data['cost']} to upgrade your farm to **{upgrade_data['name']}**!"
" Your crops will now grow faster."
)
async def setup(bot: commands.Bot):
await bot.add_cog(Farming(bot))
================================================
FILE: cogs/fun.py
================================================
import discord
from discord import app_commands
from discord.ext import commands
import random
from PIL import Image, ImageDraw, ImageFont
import io
import textwrap
class Fun(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = bot.http_session
self.jokes = [
"Why don't scientists trust atoms? Because they make up everything!",
"I told my wife she should embrace her mistakes. She gave me a hug.",
"Why did the scarecrow win an award? Because he was outstanding in his field!",
"I'm reading a book on anti-gravity. It's impossible to put down!",
"What do you call a fake noodle? An Impasta!",
"Why don't skeletons fight each other? They don't have the guts.",
"What do you call cheese that isn't yours? Nacho cheese.",
"Why did the bicycle fall over? Because it was two-tired.",
"How does a penguin build its house? Igloos it together.",
"I would tell you a joke about an empty pizza box, but it's too cheesy.",
"What do you get when you cross a snowman and a vampire? Frostbite.",
"Why are ghosts such bad liars? Because you can see right through them.",
"What's orange and sounds like a parrot? A carrot.",
"I invented a new word! Plagiarism.",
"Did you hear about the mathematician who’s afraid of negative numbers? He’ll stop at nothing to avoid them.",
"Why do we tell actors to 'break a leg?' Because every play has a cast.",
"Helvetica and Times New Roman walk into a bar. 'Get out of here!' shouts the bartender. 'We don't serve your type.'",
"Yesterday I saw a guy spill all his Scrabble letters on the road. I asked him, 'What’s the word on the street?'",
"What’s the best thing about Switzerland? I don’t know, but the flag is a big plus.",
"Why did the coffee file a police report? It got mugged.",
"I'm so good at sleeping, I can do it with my eyes closed.",
"Why was the big cat disqualified from the race? Because it was a cheetah.",
"What do you call a bear with no teeth? A gummy bear.",
"I asked the librarian if the library had any books on paranoia. She whispered, 'They're right behind you!'",
"What did the zero say to the eight? Nice belt!",
"What did one wall say to the other? I'll meet you at the corner.",
"Why did the invisible man turn down the job offer? He couldn't see himself doing it.",
"I have a joke about construction, but I'm still working on it.",
"I used to play piano by ear, but now I use my hands.",
"What do you call a boomerang that won't come back? A stick.",
"Why did the golfer bring two pairs of pants? In case he got a hole in one.",
"I'm on a seafood diet. I see food, and I eat it.",
"What do you call a fish with no eyes? Fsh.",
"Parallel lines have so much in common. It’s a shame they’ll never meet.",
"My boss told me to have a good day, so I went home.",
"Why can't you hear a pterodactyl go to the bathroom? Because the 'P' is silent.",
"Why did the stadium get hot after the game? Because all the fans left.",
"What's a vampire's favorite fruit? A neck-tarine.",
"I don't trust stairs. They're always up to something.",
"Why did the scarecrow get a promotion? He was outstanding in his field.",
"What's brown and sticky? A stick.",
"Why are pirates called pirates? Because they arrrr!",
"I was wondering why the frisbee was getting bigger. Then it hit me.",
"What do you call a lazy kangaroo? Pouch potato.",
"Why was the math book sad? Because it had too many problems.",
"What did the grape do when it got stepped on? It let out a little wine.",
"Why don’t eggs tell jokes? They’d crack each other up.",
"What’s the best way to watch a fly-fishing tournament? Live stream.",
"What did the janitor say when he jumped out of the closet? 'Supplies!'",
"I'm reading a horror story in Braille. Something bad is about to happen... I can feel it.",
"What do you call an alligator in a vest? An investigator.",
"If you see a robbery at an Apple Store, does that make you an iWitness?",
"What do you call a sad strawberry? A blueberry.",
"Why should you never trust a pig with a secret? Because it's bound to squeal.",
"I got a new job as a human cannonball. They told me I'd be fired.",
"Why did the Oreo go to the dentist? Because it lost its filling.",
"How do you organize a space party? You planet.",
"What has four wheels and flies? A garbage truck.",
"What do you call a thieving alligator? A crook-o-dile.",
"I used to be a baker, but I couldn't make enough dough.",
"I have a fear of speed bumps. I'm slowly getting over it.",
"Where do you learn to make ice cream? At sundae school.",
"Why do bees have sticky hair? Because they use a honeycomb.",
"How do you make a tissue dance? You put a little boogie in it.",
"Why can’t a bicycle stand up by itself? It's two tired.",
"Why did the tomato turn red? Because it saw the salad dressing!",
"What do you call a pony with a cough? A little hoarse.",
"Why was the belt arrested? For holding up a pair of pants.",
"How do you find Will Smith in the snow? You look for the fresh prints.",
"What do you call a man with a rubber toe? Roberto.",
"Why is it annoying to eat next to basketball players? They're always dribbling.",
"What do you call a factory that makes okay products? A satisfactory.",
"I'm terrified of elevators. I'm going to start taking steps to avoid them.",
"What do you call a dog that does magic tricks? A labracadabrador.",
"What did the drummer call his twin daughters? Anna one, Anna two!",
"Why did the cow go to outer space? To see the moooon.",
"What do you call a sleeping bull? A bulldozer.",
"Why did the can crusher quit his job? It was soda pressing.",
"Why did the man get fired from the calendar factory? He took a couple of days off.",
"What's the difference between a hippo and a zippo? One is really heavy, the other is a little lighter.",
"I was going to tell a time-traveling joke, but you guys didn't like it.",
"What do you get from a pampered cow? Spoiled milk.",
"Why did the octopus beat the shark in a fight? Because it was well-armed.",
"Why was the baby strawberry crying? Because its parents were in a jam.",
]
@app_commands.command(name="joke", description="Tells a random joke.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def joke(self, interaction: discord.Interaction):
embed = discord.Embed(
title="Here's a joke for you!", description=random.choice(self.jokes), color=discord.Color.orange()
)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="fact", description="Get a random interesting fact.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def fact(self, interaction: discord.Interaction):
await interaction.response.defer()
try:
async with self.session.get("https://uselessfacts.jsph.pl/random.json?language=en") as response:
if response.status == 200:
data = await response.json()
embed = discord.Embed(
title="Did you know?", description=data.get("text"), color=discord.Color.cyan()
)
await interaction.followup.send(embed=embed)
else:
await interaction.followup.send(
"Could not fetch a fact right now, try again later.", ephemeral=True
)
except Exception:
await interaction.followup.send("An error occurred while trying to get a fact.", ephemeral=True)
@app_commands.command(name="avatar", description="Displays a user's avatar.")
@app_commands.describe(member="The member whose avatar you want to see.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def avatar(self, interaction: discord.Interaction, member: discord.Member = None):
target_member = member or interaction.user
embed = discord.Embed(title=f"{target_member.name}'s Avatar", color=target_member.color)
embed.set_image(url=target_member.display_avatar.url)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="love", description="Calculates the love compatibility between two members.")
@app_commands.describe(member1="The first person.", member2="The second person.")
@app_commands.checks.cooldown(1, 10, key=lambda i: i.user.id)
async def love(self, interaction: discord.Interaction, member1: discord.Member, member2: discord.Member = None):
target_member2 = member2 or interaction.user
random.seed(member1.id + target_member2.id)
love_percentage = random.randint(0, 100)
random.seed()
emoji = "💔"
if 40 <= love_percentage < 75:
emoji = "❤️"
elif love_percentage >= 75:
emoji = "💖"
embed = discord.Embed(title="Love Calculator", color=discord.Color.red())
embed.description = f"**{member1.name}** + **{target_member2.name}** = **{love_percentage}%** {emoji}"
await interaction.response.send_message(embed=embed)
@app_commands.command(name="emojify", description="Converts your text into emojis.")
@app_commands.describe(text="The text to convert.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def emojify(self, interaction: discord.Interaction, text: str):
if len(text) > 50:
await interaction.response.send_message(
"Text is too long! Please keep it under 50 characters.", ephemeral=True
)
return
emojis = ""
for char in text.lower():
if char.isalpha():
emojis += f":regional_indicator_{char}: "
elif char.isdigit():
num_map = {
"0": "zero",
"1": "one",
"2": "two",
"3": "three",
"4": "four",
"5": "five",
"6": "six",
"7": "seven",
"8": "eight",
"9": "nine",
}
emojis += f":{num_map[char]}: "
else:
emojis += " "
if emojis.strip():
await interaction.response.send_message(emojis)
else:
await interaction.response.send_message("Could not emojify the text.", ephemeral=True)
@app_commands.command(name="poll", description="Creates a poll with up to 10 options.")
@app_commands.describe(
question="The question for the poll.",
option1="The first choice.",
option2="The second choice.",
option3="Optional third choice.",
option4="Optional fourth choice.",
option5="Optional fifth choice.",
option6="Optional sixth choice.",
option7="Optional seventh choice.",
option8="Optional eighth choice.",
option9="Optional ninth choice.",
option10="Optional tenth choice.",
)
@app_commands.checks.cooldown(1, 15, key=lambda i: i.channel_id)
async def poll(
self,
interaction: discord.Interaction,
question: str,
option1: str,
option2: str,
option3: str = None,
option4: str = None,
option5: str = None,
option6: str = None,
option7: str = None,
option8: str = None,
option9: str = None,
option10: str = None,
):
# Check Add Reactions permission before creating the poll
bot_member = interaction.guild.me if interaction.guild else None
if bot_member and interaction.channel:
perms = interaction.channel.permissions_for(bot_member)
if not perms.add_reactions:
await interaction.response.send_message(
"I need the **Add Reactions** permission in this channel to create polls.",
ephemeral=True,
)
return
options = [
opt
for opt in [option1, option2, option3, option4, option5, option6, option7, option8, option9, option10]
if opt is not None
]
reactions = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]
description = [f"{reactions[i]} {option}" for i, option in enumerate(options)]
embed = discord.Embed(title=question, description="\n".join(description), color=discord.Color.blurple())
embed.set_footer(text=f"Poll created by {interaction.user.name}")
await interaction.response.send_message(embed=embed)
poll_message = await interaction.original_response()
for i in range(len(options)):
await poll_message.add_reaction(reactions[i])
@app_commands.command(name="clap", description="Adds a clap emoji between each word.")
@app_commands.describe(text="The text to clapify.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def clap(self, interaction: discord.Interaction, text: str):
if len(text.split()) < 2:
await interaction.response.send_message("You need at least two words to clap!", ephemeral=True)
return
clapped_text = " 👏 ".join(text.split())
await interaction.response.send_message(clapped_text)
@app_commands.command(name="tweet", description="Generate an image of a fake tweet.")
@app_commands.describe(text="The content of the tweet (max 280 chars).")
@app_commands.checks.cooldown(1, 15, key=lambda i: i.user.id)
async def tweet(self, interaction: discord.Interaction, text: str):
await interaction.response.defer()
if len(text) > 280:
await interaction.followup.send("Tweet text cannot exceed 280 characters.", ephemeral=True)
return
user = interaction.user
bg = Image.new("RGB", (1000, 400), color=(21, 32, 43))
try:
async with self.session.get(user.display_avatar.url) as response:
if response.status == 200:
avatar_data = await response.read()
avatar = Image.open(io.BytesIO(avatar_data)).convert("RGBA")
avatar = avatar.resize((150, 150))
mask = Image.new("L", avatar.size, 0)
draw_mask = ImageDraw.Draw(mask)
draw_mask.ellipse((0, 0) + avatar.size, fill=255)
bg.paste(avatar, (50, 50), mask)
except Exception as e:
print(f"Tweet avatar error: {e}")
draw = ImageDraw.Draw(bg)
try:
font_bold = ImageFont.truetype("arialbd.ttf", 60)
font_regular = ImageFont.truetype("arial.ttf", 50)
font_handle = ImageFont.truetype("arial.ttf", 45)
except IOError:
print("Arial font not found, using default font for tweet command.")
font_bold = ImageFont.load_default(size=60)
font_regular = ImageFont.load_default(size=50)
font_handle = ImageFont.load_default(size=45)
draw.text((230, 60), user.display_name, font=font_bold, fill=(255, 255, 255))
draw.text((230, 125), f"@{user.name}", font=font_handle, fill=(136, 153, 166))
wrapped_text = textwrap.fill(text, width=35)
draw.text((50, 230), wrapped_text, font=font_regular, fill=(255, 255, 255))
buffer = io.BytesIO()
bg.save(buffer, format="PNG")
buffer.seek(0)
await interaction.followup.send(file=discord.File(buffer, "tweet.png"))
async def setup(bot: commands.Bot):
await bot.add_cog(Fun(bot))
================================================
FILE: cogs/games.py
================================================
import discord
from discord import app_commands
from discord.ext import commands
import random
import asyncio
class TicTacToeButton(discord.ui.Button["TicTacToe"]):
def __init__(self, x: int, y: int):
super().__init__(style=discord.ButtonStyle.secondary, label="\u200b", row=y)
self.x = x
self.y = y
async def callback(self, interaction: discord.Interaction):
assert self.view is not None
view: TicTacToe = self.view
if interaction.user != view.current_player:
await interaction.response.send_message("It's not your turn!", ephemeral=True)
return
state = view.board[self.y][self.x]
if state in (view.X, view.O):
return
if view.current_player == view.player1:
self.style = discord.ButtonStyle.danger
self.label = "X"
view.board[self.y][self.x] = view.X
view.current_player = view.player2
content = f"It's {view.player2.name}'s turn (O)."
else:
self.style = discord.ButtonStyle.success
self.label = "O"
view.board[self.y][self.x] = view.O
view.current_player = view.player1
content = f"It's {view.player1.name}'s turn (X)."
self.disabled = True
winner = view.check_board_winner()
if winner is not None:
if winner == view.X:
content = f"🏆 {view.player1.name} wins! 🏆"
elif winner == view.O:
content = f"🏆 {view.player2.name} wins! 🏆"
else:
content = "It's a tie!"
for child in view.children:
child.disabled = True
view.stop()
await interaction.response.edit_message(content=content, view=view)
class TicTacToe(discord.ui.View):
X = -1
O = 1
Tie = 2
def __init__(self, player1: discord.Member, player2: discord.Member):
super().__init__(timeout=180)
self.player1 = player1
self.player2 = player2
self.current_player = player1
self.board = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
]
for x in range(3):
for y in range(3):
self.add_item(TicTacToeButton(x, y))
async def on_timeout(self):
for item in self.children:
item.disabled = True
message = await self.message.edit(content="Game timed out! No one made a move.", view=self)
def check_board_winner(self):
for i in range(3):
if sum(self.board[i]) == 3:
return self.O
if sum(self.board[i]) == -3:
return self.X
for i in range(3):
if self.board[0][i] + self.board[1][i] + self.board[2][i] == 3:
return self.O
if self.board[0][i] + self.board[1][i] + self.board[2][i] == -3:
return self.X
if self.board[0][0] + self.board[1][1] + self.board[2][2] == 3:
return self.O
if self.board[0][0] + self.board[1][1] + self.board[2][2] == -3:
return self.X
if self.board[0][2] + self.board[1][1] + self.board[2][0] == 3:
return self.O
if self.board[0][2] + self.board[1][1] + self.board[2][0] == -3:
return self.X
if all(i != 0 for row in self.board for i in row):
return self.Tie
return None
class Games(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.active_guess_games = set()
@app_commands.command(name="eightball", description="Ask the magic 8-ball a question.")
@app_commands.describe(question="The question you want to ask.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def eightball(self, interaction: discord.Interaction, question: str):
responses = [
"It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes – definitely.",
"You may rely on it.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again.",
"Don't count on it.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful.",
]
embed = discord.Embed(title="🎱 Magic 8-Ball 🎱", color=discord.Color.dark_blue())
embed.add_field(name="Question", value=question, inline=False)
embed.add_field(name="Answer", value=random.choice(responses), inline=False)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="coinflip", description="Flips a coin.")
@app_commands.checks.cooldown(1, 3, key=lambda i: i.user.id)
async def coinflip(self, interaction: discord.Interaction):
result = random.choice(["Heads", "Tails"])
await interaction.response.send_message(f"The coin landed on: **{result}**")
@app_commands.command(name="roll", description="Rolls a dice in NdN format (e.g., 2d6).")
@app_commands.describe(dice="The dice to roll (e.g., 1d6, 2d8).")
@app_commands.checks.cooldown(1, 3, key=lambda i: i.user.id)
async def roll(self, interaction: discord.Interaction, dice: str):
try:
rolls, limit = map(int, dice.lower().split("d"))
except Exception:
await interaction.response.send_message("Format has to be in NdN (e.g., 1d6)!", ephemeral=True)
return
if not (1 <= rolls <= 100 and 1 <= limit <= 1000):
await interaction.response.send_message(
"Please keep rolls between 1-100 and faces between 1-1000.", ephemeral=True
)
return
results = [random.randint(1, limit) for _ in range(rolls)]
total = sum(results)
embed = discord.Embed(title=f"Dice Roll: {dice}", description=f"Total: **{total}**", color=discord.Color.red())
embed.add_field(name="Individual Rolls", value=", ".join(map(str, results)), inline=False)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="guess", description="Starts a 'guess the number' game.")
@app_commands.checks.cooldown(1, 60, key=lambda i: i.channel_id)
async def guess(self, interaction: discord.Interaction):
if interaction.channel.id in self.active_guess_games:
await interaction.response.send_message("A game is already active in this channel!", ephemeral=True)
return
self.active_guess_games.add(interaction.channel.id)
number_to_guess = random.randint(1, 100)
attempts = 0
await interaction.response.send_message(
f"I've picked a number between 1 and 100. You have 60 seconds to guess it!"
)
def check(m):
return m.channel == interaction.channel and m.author == interaction.user and m.content.isdigit()
try:
while True:
guess_msg = await self.bot.wait_for("message", timeout=60.0, check=check)
guess = int(guess_msg.content)
attempts += 1
if guess < number_to_guess:
await guess_msg.reply("Too low! Try again.", delete_after=5)
elif guess > number_to_guess:
await guess_msg.reply("Too high! Try again.", delete_after=5)
else:
await guess_msg.reply(
f"🎉 You got it! The number was **{number_to_guess}**. It took you {attempts} attempts."
)
self.active_guess_games.remove(interaction.channel.id)
return
except asyncio.TimeoutError:
if interaction.channel.id in self.active_guess_games:
await interaction.followup.send(f"Time's up! The number was {number_to_guess}.")
self.active_guess_games.remove(interaction.channel.id)
@app_commands.command(name="rps", description="Play Rock, Paper, Scissors with the bot.")
@app_commands.describe(choice="Your choice.")
@app_commands.choices(
choice=[
app_commands.Choice(name="Rock", value="rock"),
app_commands.Choice(name="Paper", value="paper"),
app_commands.Choice(name="Scissors", value="scissors"),
]
)
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def rockpaperscissors(self, interaction: discord.Interaction, choice: app_commands.Choice[str]):
user_choice = choice.value
bot_choice = random.choice(["rock", "paper", "scissors"])
emoji_map = {"rock": "🗿", "paper": "📄", "scissors": "✂️"}
result_text = f"You chose {emoji_map[user_choice]}, I chose {emoji_map[bot_choice]}.\n\n"
if user_choice == bot_choice:
result_text += "**It's a tie!**"
elif (
(user_choice == "rock" and bot_choice == "scissors")
or (user_choice == "paper" and bot_choice == "rock")
or (user_choice == "scissors" and bot_choice == "paper")
):
result_text += "**You win!** 🎉"
else:
result_text += "**I win!** 🤖"
await interaction.response.send_message(result_text)
@app_commands.command(name="tictactoe", description="Play a game of Tic-Tac-Toe with another member.")
@app_commands.describe(opponent="The member you want to challenge.")
@app_commands.checks.cooldown(1, 30, key=lambda i: i.channel_id)
async def tictactoe(self, interaction: discord.Interaction, opponent: discord.Member):
if opponent == interaction.user:
await interaction.response.send_message("You can't play against yourself!", ephemeral=True)
return
if opponent.bot:
await interaction.response.send_message(
"You can't challenge a bot to a game of Tic-Tac-Toe!", ephemeral=True
)
return
view = TicTacToe(interaction.user, opponent)
await interaction.response.send_message(
f"Tic-Tac-Toe: {interaction.user.name} vs {opponent.name}\nIt's {interaction.user.name}'s turn (X).",
view=view,
)
view.message = await interaction.original_response()
async def setup(bot: commands.Bot):
await bot.add_cog(Games(bot))
================================================
FILE: cogs/interactions.py
================================================
import discord
from discord import app_commands
from discord.ext import commands
class Interactions(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = bot.http_session
async def get_gif(self, category: str):
try:
async with self.session.get(f"https://api.waifu.pics/sfw/{category}") as response:
if response.status == 200:
data = await response.json()
return data.get("url")
except Exception as e:
print(f"Could not fetch GIF for category {category}: {e}")
return None
async def create_interaction_embed(
self,
interaction: discord.Interaction,
member: discord.Member,
category: str,
self_message: str,
other_message: str,
):
await interaction.response.defer()
gif_url = await self.get_gif(category)
if not gif_url:
await interaction.followup.send(
f"Sorry, couldn't get a GIF right now. But... {other_message.format(user=interaction.user.mention, target=member.mention)}",
ephemeral=True,
)
return
message = (
self_message.format(user=interaction.user.mention, target=member.mention)
if member == interaction.user
else other_message.format(user=interaction.user.mention, target=member.mention)
)
embed = discord.Embed(description=message, color=discord.Color.from_rgb(255, 182, 193)) # Pink
embed.set_image(url=gif_url)
await interaction.followup.send(embed=embed)
@app_commands.command(name="hug", description="Give someone a hug.")
@app_commands.describe(member="The person you want to hug.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def hug(self, interaction: discord.Interaction, member: discord.Member):
await self.create_interaction_embed(
interaction,
member,
category="hug",
self_message="You can't hug yourself, but I can! Here's a hug from me to you.",
other_message="{user} gives {target} a big, warm hug!",
)
@app_commands.command(name="pat", description="Pat someone's head.")
@app_commands.describe(member="The person you want to pat.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def pat(self, interaction: discord.Interaction, member: discord.Member):
await self.create_interaction_embed(
interaction,
member,
category="pat",
self_message="Feeling a bit lonely? I'll pat your head for you. *pats*",
other_message="{user} gently pats {target}'s head. Aww.",
)
@app_commands.command(name="slap", description="Slap someone.")
@app_commands.describe(member="The person you want to slap.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def slap(self, interaction: discord.Interaction, member: discord.Member):
if member == self.bot.user:
await interaction.response.send_message(f"Ouch! What did I do to deserve that, {interaction.user.mention}?")
return
await self.create_interaction_embed(
interaction,
member,
category="slap",
self_message="{user} slaps themself in confusion!",
other_message="Oof! {user} slaps {target} right across the face!",
)
@app_commands.command(name="kiss", description="Give someone a kiss.")
@app_commands.describe(member="The person you want to kiss.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def kiss(self, interaction: discord.Interaction, member: discord.Member):
await self.create_interaction_embed(
interaction,
member,
category="kiss",
self_message="Blowing a kiss to yourself in the mirror, nice!",
other_message="{user} gives {target} a sweet kiss. Mwah!",
)
@app_commands.command(name="cuddle", description="Cuddle with someone.")
@app_commands.describe(member="The person you want to cuddle with.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def cuddle(self, interaction: discord.Interaction, member: discord.Member):
await self.create_interaction_embed(
interaction,
member,
category="cuddle",
self_message="Cuddling with a pillow is nice, but here's a virtual one!",
other_message="{user} snuggles up and cuddles with {target}. So cozy!",
)
@app_commands.command(name="poke", description="Poke someone.")
@app_commands.describe(member="The person you want to poke.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def poke(self, interaction: discord.Interaction, member: discord.Member):
await self.create_interaction_embed(
interaction,
member,
category="poke",
self_message="You poke yourself. Why?",
other_message="Hey! {user} just poked {target}.",
)
async def setup(bot: commands.Bot):
await bot.add_cog(Interactions(bot))
================================================
FILE: cogs/media.py
================================================
import logging
import discord
from discord import app_commands
from discord.ext import commands
logger = logging.getLogger(__name__)
class Media(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.session = bot.http_session
@app_commands.command(name="meme", description="Fetches a random meme.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def meme(self, interaction: discord.Interaction):
await interaction.response.defer()
try:
async with self.session.get("https://meme-api.com/gimme") as response:
if response.status == 200:
data = await response.json()
embed = discord.Embed(title=data["title"], url=data["postLink"], color=discord.Color.blue())
embed.set_image(url=data["url"])
embed.set_footer(text=f"From r/{data['subreddit']} | Upvotes: {data['ups']}")
await interaction.followup.send(embed=embed)
else:
await interaction.followup.send("Could not fetch a meme, please try again.")
except Exception:
logger.exception("Error fetching meme")
await interaction.followup.send("An error occurred while fetching a meme. Please try again later.", ephemeral=True)
@app_commands.command(name="cat", description="Fetches a random cat picture.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def cat(self, interaction: discord.Interaction):
await interaction.response.defer()
try:
async with self.session.get("https://api.thecatapi.com/v1/images/search") as response:
if response.status == 200:
data = await response.json()
embed = discord.Embed(title="Here's a random cat!", color=discord.Color.purple())
embed.set_image(url=data[0]["url"])
await interaction.followup.send(embed=embed)
else:
await interaction.followup.send("Could not fetch a cat picture, the cats are hiding!")
except Exception:
logger.exception("Error fetching cat picture")
await interaction.followup.send("An error occurred while fetching a cat picture. Please try again later.", ephemeral=True)
@app_commands.command(name="dog", description="Fetches a random dog picture.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def dog(self, interaction: discord.Interaction):
await interaction.response.defer()
try:
async with self.session.get("https://dog.ceo/api/breeds/image/random") as response:
if response.status == 200:
data = await response.json()
embed = discord.Embed(title="Here's a random dog!", color=discord.Color.gold())
embed.set_image(url=data["message"])
await interaction.followup.send(embed=embed)
else:
await interaction.followup.send("Could not fetch a dog picture, the dogs are playing fetch!")
except Exception:
logger.exception("Error fetching dog picture")
await interaction.followup.send("An error occurred while fetching a dog picture. Please try again later.", ephemeral=True)
async def setup(bot: commands.Bot):
await bot.add_cog(Media(bot))
================================================
FILE: cogs/moderation.py
================================================
import json
import re
from datetime import timedelta
from typing import Dict, List, Optional
import discord
from discord import app_commands
from discord.ext import commands
INVITE_RE = re.compile(r"(discord\.gg/|discord\.com/invite/)", re.IGNORECASE)
LINK_RE = re.compile(r"https?://", re.IGNORECASE)
class Moderation(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.bot.loop.create_task(self.setup_database())
async def setup_database(self):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS automod_settings (
guild_id INTEGER PRIMARY KEY,
filter_invites INTEGER NOT NULL DEFAULT 0,
filter_links INTEGER NOT NULL DEFAULT 0,
bad_words TEXT,
whitelist_channel_ids TEXT,
action TEXT NOT NULL DEFAULT 'delete'
)
"""
)
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS moderation_warnings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
moderator_id INTEGER NOT NULL,
reason TEXT NOT NULL,
created_at TEXT NOT NULL
)
"""
)
await cursor.execute(
"""
CREATE INDEX IF NOT EXISTS idx_moderation_warnings_guild_user_created_at
ON moderation_warnings(guild_id, user_id, created_at DESC)
"""
)
await self.bot.db.commit()
def _serialize_ids(self, ids: List[int]) -> Optional[str]:
cleaned = sorted({int(item) for item in ids})
return json.dumps(cleaned) if cleaned else None
def _deserialize_ids(self, raw: Optional[str]) -> List[int]:
if not raw:
return []
try:
values = json.loads(raw)
except json.JSONDecodeError:
return []
return [int(item) for item in values if str(item).isdigit()]
async def get_automod_settings(self, guild_id: int) -> Dict[str, object]:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT filter_invites, filter_links, bad_words, whitelist_channel_ids, action
FROM automod_settings
WHERE guild_id = ?
""",
(guild_id,),
)
row = await cursor.fetchone()
if row is None:
return {
"filter_invites": False,
"filter_links": False,
"bad_words": [],
"whitelist_channel_ids": [],
"action": "delete",
}
filter_invites, filter_links, bad_words, whitelist_channel_ids, action = row
return {
"filter_invites": bool(filter_invites),
"filter_links": bool(filter_links),
"bad_words": [word for word in (bad_words or "").split(",") if word],
"whitelist_channel_ids": self._deserialize_ids(whitelist_channel_ids),
"action": action or "delete",
}
async def update_automod(self, guild_id: int, **fields):
async with self.bot.db.cursor() as cursor:
await cursor.execute("INSERT OR IGNORE INTO automod_settings (guild_id) VALUES (?)", (guild_id,))
for field, value in fields.items():
await cursor.execute(f"UPDATE automod_settings SET {field} = ? WHERE guild_id = ?", (value, guild_id))
await self.bot.db.commit()
async def mutate_whitelist(self, guild_id: int, channel_id: int, add: bool) -> List[int]:
settings = await self.get_automod_settings(guild_id)
current = set(settings["whitelist_channel_ids"])
if add:
current.add(channel_id)
else:
current.discard(channel_id)
await self.update_automod(guild_id, whitelist_channel_ids=self._serialize_ids(list(current)))
return sorted(current)
async def add_warning(self, guild_id: int, user_id: int, moderator_id: int, reason: str) -> int:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
INSERT INTO moderation_warnings (guild_id, user_id, moderator_id, reason, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(guild_id, user_id, moderator_id, reason, discord.utils.utcnow().isoformat()),
)
warning_id = cursor.lastrowid
await self.bot.db.commit()
return warning_id
async def log_action(self, guild: discord.Guild, title: str, description: str, color: discord.Color):
community = self.bot.get_cog("Community")
if community:
await community._log_to_modlog(guild, title, description, color)
automod = app_commands.Group(name="automod", description="Configure basic automod rules.", guild_only=True)
@automod.command(name="view", description="View the current automod settings.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_view(self, interaction: discord.Interaction):
settings = await self.get_automod_settings(interaction.guild_id)
whitelist = ", ".join(f"<#{channel_id}>" for channel_id in settings["whitelist_channel_ids"]) or "Not set"
bad_words = ", ".join(f"`{word}`" for word in settings["bad_words"]) or "Not set"
embed = discord.Embed(title="Automod Settings", color=discord.Color.orange())
embed.add_field(name="Invite Filter", value="On" if settings["filter_invites"] else "Off", inline=True)
embed.add_field(name="Link Filter", value="On" if settings["filter_links"] else "Off", inline=True)
embed.add_field(name="Action", value=settings["action"], inline=True)
embed.add_field(name="Bad Words", value=bad_words, inline=False)
embed.add_field(name="Whitelisted Channels", value=whitelist, inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
@automod.command(name="toggle-invites", description="Turn invite filtering on or off.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_toggle_invites(self, interaction: discord.Interaction, enabled: bool):
await self.update_automod(interaction.guild_id, filter_invites=int(enabled))
await interaction.response.send_message(f"Invite filtering is now {'on' if enabled else 'off'}.", ephemeral=True)
@automod.command(name="toggle-links", description="Turn link filtering on or off.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_toggle_links(self, interaction: discord.Interaction, enabled: bool):
await self.update_automod(interaction.guild_id, filter_links=int(enabled))
await interaction.response.send_message(f"Link filtering is now {'on' if enabled else 'off'}.", ephemeral=True)
@automod.command(name="set-action", description="Choose what automod should do when it triggers.")
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.choices(
action=[
app_commands.Choice(name="Delete", value="delete"),
app_commands.Choice(name="Warn", value="warn"),
app_commands.Choice(name="Timeout", value="timeout"),
]
)
async def automod_set_action(self, interaction: discord.Interaction, action: app_commands.Choice[str]):
await self.update_automod(interaction.guild_id, action=action.value)
await interaction.response.send_message(f"Automod action set to `{action.value}`.", ephemeral=True)
@automod.command(name="set-bad-words", description="Set a comma-separated list of blocked words.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_set_bad_words(self, interaction: discord.Interaction, words: str):
normalized = ",".join(sorted({word.strip().lower() for word in words.split(",") if word.strip()}))
await self.update_automod(interaction.guild_id, bad_words=normalized or None)
count = 0 if not normalized else len(normalized.split(","))
await interaction.response.send_message(f"Stored {count} blocked word(s).", ephemeral=True)
@automod.command(name="clear-bad-words", description="Clear all blocked words.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_clear_bad_words(self, interaction: discord.Interaction):
await self.update_automod(interaction.guild_id, bad_words=None)
await interaction.response.send_message("Blocked words cleared.", ephemeral=True)
@automod.command(name="whitelist-channel", description="Exclude a channel from automod checks.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_whitelist_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
channels = await self.mutate_whitelist(interaction.guild_id, channel.id, add=True)
await interaction.response.send_message(
f"Automod whitelist updated. {len(channels)} channel(s) are exempt now.",
ephemeral=True,
)
@automod.command(name="remove-whitelist-channel", description="Remove a channel from the automod whitelist.")
@app_commands.checks.has_permissions(manage_guild=True)
async def automod_remove_whitelist_channel(self, interaction: discord.Interaction, channel: discord.TextChannel):
channels = await self.mutate_whitelist(interaction.guild_id, channel.id, add=False)
await interaction.response.send_message(
f"Automod whitelist updated. {len(channels)} channel(s) remain exempt.",
ephemeral=True,
)
@app_commands.command(name="warn", description="Warn a member and log it.")
@app_commands.guild_only()
@app_commands.checks.has_permissions(manage_messages=True)
async def warn(self, interaction: discord.Interaction, member: discord.Member, reason: str):
warning_id = await self.add_warning(interaction.guild_id, member.id, interaction.user.id, reason)
await self.log_action(
interaction.guild,
"Member Warned",
f"{member.mention} was warned by {interaction.user.mention}.\nReason: {reason}\nWarning ID: `{warning_id}`",
discord.Color.orange(),
)
await interaction.response.send_message(
f"Warning `{warning_id}` recorded for {member.mention}.",
ephemeral=True,
)
@app_commands.command(name="warnings", description="View warning history for a member.")
@app_commands.guild_only()
@app_commands.checks.has_permissions(manage_messages=True)
async def warnings(self, interaction: discord.Interaction, member: discord.Member):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT id, moderator_id, reason, created_at
FROM moderation_warnings
WHERE guild_id = ? AND user_id = ?
ORDER BY created_at DESC
LIMIT 10
""",
(interaction.guild_id, member.id),
)
rows = await cursor.fetchall()
if not rows:
await interaction.response.send_message(f"{member.mention} has no warnings in this server.", ephemeral=True)
return
lines = []
for warning_id, moderator_id, reason, created_at in rows:
moderator = interaction.guild.get_member(moderator_id)
moderator_label = moderator.mention if moderator else f"`{moderator_id}`"
lines.append(
f"`{warning_id}` • {discord.utils.format_dt(discord.utils.parse_time(created_at), style='R')} • {moderator_label}\n{reason}"
)
embed = discord.Embed(title=f"Warnings for {member}", description="\n\n".join(lines), color=discord.Color.orange())
await interaction.response.send_message(embed=embed, ephemeral=True)
@app_commands.command(name="clear-warning", description="Delete a single warning by id.")
@app_commands.guild_only()
@app_commands.checks.has_permissions(manage_messages=True)
async def clear_warning(self, interaction: discord.Interaction, warning_id: int):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"DELETE FROM moderation_warnings WHERE id = ? AND guild_id = ?",
(warning_id, interaction.guild_id),
)
deleted = cursor.rowcount
await self.bot.db.commit()
if not deleted:
await interaction.response.send_message("Warning not found.", ephemeral=True)
return
await self.log_action(
interaction.guild,
"Warning Cleared",
f"{interaction.user.mention} removed warning `{warning_id}`.",
discord.Color.green(),
)
await interaction.response.send_message(f"Warning `{warning_id}` removed.", ephemeral=True)
async def handle_violation(self, message: discord.Message, reason: str, action: str):
deleted = False
try:
await message.delete()
deleted = True
except (discord.Forbidden, discord.HTTPException):
deleted = False
details = [f"User: {message.author.mention}", f"Channel: {message.channel.mention}", f"Reason: {reason}"]
if not deleted:
details.append("Action skipped because the original message could not be deleted")
await self.log_action(message.guild, "Automod Triggered", "\n".join(details), discord.Color.red())
return
if action == "warn":
warning_id = await self.add_warning(message.guild.id, message.author.id, self.bot.user.id, f"Automod: {reason}")
details.append(f"Warning ID: `{warning_id}`")
try:
await message.channel.send(
f"{message.author.mention}, your message was removed by automod: {reason}",
delete_after=10,
)
except (discord.Forbidden, discord.HTTPException):
pass
elif action == "timeout" and isinstance(message.author, discord.Member):
try:
await message.author.timeout(discord.utils.utcnow() + timedelta(minutes=10), reason=f"Automod: {reason}")
details.append("Action: timeout")
except (discord.Forbidden, discord.HTTPException):
details.append("Action: timeout failed, message removed only")
try:
await message.channel.send(
f"{message.author.mention}, automod tried to time you out but lacked permission. Message removed.",
delete_after=10,
)
except (discord.Forbidden, discord.HTTPException):
pass
await self.log_action(message.guild, "Automod Triggered", "\n".join(details), discord.Color.red())
@commands.Cog.listener("on_message")
async def automod_listener(self, message: discord.Message):
if message.author.bot or message.guild is None:
return
if not isinstance(message.author, discord.Member):
return
if not isinstance(message.channel, (discord.TextChannel, discord.Thread)):
return
if message.author.guild_permissions.manage_messages:
return
settings = await self.get_automod_settings(message.guild.id)
whitelisted = settings["whitelist_channel_ids"]
parent_id = getattr(message.channel, "parent_id", None)
if message.channel.id in whitelisted or (parent_id and parent_id in whitelisted):
return
lowered = message.content.lower()
violation = None
if settings["filter_invites"] and INVITE_RE.search(message.content):
violation = "Discord invite links are not allowed."
elif settings["filter_links"] and LINK_RE.search(message.content):
violation = "Links are not allowed."
else:
for word in settings["bad_words"]:
if not word:
continue
pattern = re.compile(rf"(?<!\w){re.escape(word)}(?!\w)", re.IGNORECASE)
if pattern.search(lowered):
violation = f"Blocked word detected: `{word}`"
break
if violation:
await self.handle_violation(message, violation, settings["action"])
async def setup(bot: commands.Bot):
await bot.add_cog(Moderation(bot))
================================================
FILE: cogs/utility.py
================================================
import platform
import re
import sys
import time
from datetime import timedelta
from typing import Optional, Union
import discord
from discord import app_commands
from discord.ext import commands, tasks
REMINDER_LIMIT_SECONDS = 30 * 24 * 60 * 60
REMINDER_POLL_SECONDS = 30
REMINDER_BATCH_SIZE = 20
MAX_REMINDER_DELIVERY_FAILURES = 5
MAX_REMINDER_TEXT_LENGTH = 1500
REMINDER_RETRY_BASE_SECONDS = 300
REMINDER_RETRY_MAX_SECONDS = 21600
def parse_duration_spec(value: str) -> Optional[int]:
match = re.fullmatch(r"(\d+)([smhd])", value.lower())
if not match:
return None
quantity, unit = match.groups()
return int(quantity) * {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit]
class Utility(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.afk_cache: dict[tuple[int, int], tuple[str, str]] = {}
if not hasattr(bot, "start_time"):
self.bot.start_time = discord.utils.utcnow()
self.reminder_loop.start()
def cog_unload(self):
self.reminder_loop.cancel()
async def setup_database(self):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
channel_id INTEGER,
guild_id INTEGER,
remind_at TEXT NOT NULL,
reason TEXT NOT NULL,
recurring_seconds INTEGER,
delivery_failures INTEGER NOT NULL DEFAULT 0
)
"""
)
await cursor.execute(
"""
CREATE TABLE IF NOT EXISTS afk_statuses (
guild_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
reason TEXT NOT NULL,
set_at TEXT NOT NULL,
PRIMARY KEY (guild_id, user_id)
)
"""
)
await cursor.execute("PRAGMA table_info(reminders)")
reminder_columns = [row[1] for row in await cursor.fetchall()]
if "recurring_seconds" not in reminder_columns:
await cursor.execute("ALTER TABLE reminders ADD COLUMN recurring_seconds INTEGER")
if "delivery_failures" not in reminder_columns:
await cursor.execute("ALTER TABLE reminders ADD COLUMN delivery_failures INTEGER NOT NULL DEFAULT 0")
if "disabled" not in reminder_columns:
await cursor.execute("ALTER TABLE reminders ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0")
if "last_error" not in reminder_columns:
await cursor.execute("ALTER TABLE reminders ADD COLUMN last_error TEXT")
await cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_reminders_due ON reminders(disabled, remind_at)"
)
await cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_reminders_user_remind_at ON reminders(user_id, remind_at)"
)
await self.bot.db.commit()
async def get_afk_status(self, guild_id: int, user_id: int):
cache_key = (guild_id, user_id)
if cache_key in self.afk_cache:
return self.afk_cache[cache_key]
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"SELECT reason, set_at FROM afk_statuses WHERE guild_id = ? AND user_id = ?",
(guild_id, user_id),
)
row = await cursor.fetchone()
if row:
self.afk_cache[cache_key] = row
return row
async def get_many_afk_statuses(self, guild_id: int, user_ids: list[int]) -> dict[int, tuple[str, str]]:
statuses: dict[int, tuple[str, str]] = {}
missing_ids = []
for user_id in user_ids:
cache_key = (guild_id, user_id)
cached = self.afk_cache.get(cache_key)
if cached:
statuses[user_id] = cached
else:
missing_ids.append(user_id)
if not missing_ids:
return statuses
placeholders = ",".join("?" for _ in missing_ids)
async with self.bot.db.cursor() as cursor:
await cursor.execute(
f"""
SELECT user_id, reason, set_at
FROM afk_statuses
WHERE guild_id = ? AND user_id IN ({placeholders})
""",
[guild_id, *missing_ids],
)
rows = await cursor.fetchall()
for user_id, reason, set_at in rows:
status = (reason, set_at)
self.afk_cache[(guild_id, user_id)] = status
statuses[user_id] = status
return statuses
async def set_afk_status(self, guild_id: int, user_id: int, reason: str):
set_at = discord.utils.utcnow().isoformat()
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
INSERT INTO afk_statuses (guild_id, user_id, reason, set_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(guild_id, user_id)
DO UPDATE SET reason = excluded.reason, set_at = excluded.set_at
""",
(guild_id, user_id, reason, set_at),
)
await self.bot.db.commit()
self.afk_cache[(guild_id, user_id)] = (reason, set_at)
async def clear_afk_status(self, guild_id: int, user_id: int):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"DELETE FROM afk_statuses WHERE guild_id = ? AND user_id = ?",
(guild_id, user_id),
)
await self.bot.db.commit()
self.afk_cache.pop((guild_id, user_id), None)
async def list_user_reminders(self, user_id: int):
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
SELECT id, guild_id, channel_id, remind_at, reason, recurring_seconds, delivery_failures, disabled, last_error
FROM reminders
WHERE user_id = ?
ORDER BY remind_at ASC
""",
(user_id,),
)
return await cursor.fetchall()
def reminder_retry_delay(self, failures: int) -> timedelta:
seconds = min(REMINDER_RETRY_BASE_SECONDS * (2 ** max(failures - 1, 0)), REMINDER_RETRY_MAX_SECONDS)
return timedelta(seconds=seconds)
async def create_reminder(
self,
user_id: int,
channel_id: Optional[int],
guild_id: Optional[int],
remind_at,
reason: str,
recurring_seconds: Optional[int] = None,
) -> int:
async with self.bot.db.cursor() as cursor:
await cursor.execute(
"""
INSERT INTO reminders (user_id, channel_id, guild_id, remind_at, reason, recurring_seconds)
VALUES (?, ?, ?, ?, ?, ?)
""",
(user_id, channel_id, guild_id, remind_at.isoformat(), reason, recurring_seconds),
)
reminder_id = cursor.lastrowid
await self.bot.db.commit()
return reminder_id
help_group = app_commands.Group(name="help", description="Get help with the bot's commands.")
reminders_group = app_commands.Group(name="reminders", description="Manage your active reminders.")
@app_commands.command(name="ping", description="Checks the bot's latency.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def ping(self, interaction: discord.Interaction):
latency = self.bot.latency * 1000
await interaction.response.send_message(f"Pong! Latency: {latency:.2f}ms")
@app_commands.command(name="ping_raw", description="Checks the bot's detailed latency (Websocket & REST).")
@app_commands.checks.cooldown(1, 10, key=lambda i: i.user.id)
async def ping_raw(self, interaction: discord.Interaction):
ws_latency = self.bot.latency * 1000
rest_latency = "N/A"
session = getattr(self.bot, "http_session", None)
if session and not session.closed:
started = time.perf_counter()
try:
async with session.get("https://discord.com/api/v10/gateway") as response:
if 200 <= response.status < 500:
rest_latency = f"{(time.perf_counter() - started) * 1000:.2f}"
except Exception:
rest_latency = "N/A"
embed = discord.Embed(title="🏓 Pong!", color=discord.Color.blue())
embed.add_field(name="WebSocket Latency", value=f"{ws_latency:.2f}ms", inline=False)
embed.add_field(name="REST API Latency", value=f"{rest_latency}ms", inline=False)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="memberinfo", description="Displays information about a member.")
@app_commands.guild_only()
@app_commands.describe(member="The member to get info about.")
@app_commands.checks.cooldown(1, 10, key=lambda i: i.user.id)
async def memberinfo(self, interaction: discord.Interaction, member: discord.Member = None):
target_member = member or interaction.user
embed = discord.Embed(color=target_member.color, timestamp=discord.utils.utcnow())
embed.set_author(name=f"User Info - {target_member}")
if target_member.display_avatar:
embed.set_thumbnail(url=target_member.display_avatar.url)
embed.set_footer(
text=f"Requested by {interaction.user.name}",
icon_url=interaction.user.avatar.url if interaction.user.avatar else None,
)
embed.add_field(name="ID", value=target_member.id, inline=False)
embed.add_field(name="Display Name", value=target_member.display_name, inline=True)
embed.add_field(name="Bot?", value=target_member.bot, inline=True)
embed.add_field(name="Created At", value=discord.utils.format_dt(target_member.created_at, style="F"), inline=False)
if target_member.joined_at:
embed.add_field(name="Joined At", value=discord.utils.format_dt(target_member.joined_at, style="F"), inline=False)
roles = [role.mention for role in reversed(target_member.roles[1:])]
roles_str = ", ".join(roles) if roles else "No roles"
embed.add_field(
name=f"Roles [{len(roles)}]",
value=roles_str if len(roles_str) < 1024 else f"{len(roles)} roles (too many to display)",
inline=False,
)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="clear", description="Deletes a specified number of messages.")
@app_commands.guild_only()
@app_commands.describe(amount="The number of messages to delete (1-100).")
@app_commands.checks.has_permissions(manage_messages=True)
async def clear(self, interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
me = interaction.guild.me or interaction.guild.get_member(self.bot.user.id)
if me is None or not interaction.channel.permissions_for(me).manage_messages:
await interaction.response.send_message(
"I don't have permission to manage messages in this channel.", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True)
deleted = await interaction.channel.purge(limit=amount)
await interaction.followup.send(f"Successfully deleted {len(deleted)} messages.", ephemeral=True)
@app_commands.command(name="serverinfo", description="Displays detailed information about the server.")
@app_commands.guild_only()
@app_commands.checks.cooldown(1, 10, key=lambda i: i.guild_id)
async def serverinfo(self, interaction: discord.Interaction):
guild = interaction.guild
embed = discord.Embed(
title=f"Server Info: {guild.name}", color=discord.Color.blue(), timestamp=discord.utils.utcnow()
)
if guild.icon:
embed.set_thumbnail(url=guild.icon.url)
owner_text = guild.owner.mention if guild.owner else f"ID: {guild.owner_id}"
embed.add_field(name="Owner", value=owner_text, inline=True)
embed.add_field(name="ID", value=guild.id, inline=True)
embed.add_field(name="Created At", value=discord.utils.format_dt(guild.created_at, "F"), inline=False)
total_members = guild.member_count if guild.member_count is not None else len(guild.members)
bots = sum(1 for member in guild.members if member.bot) if guild.members else 0
humans = total_members - bots
embed.add_field(name="Members", value=f"Total: {total_members}\nHumans: {humans}\nBots: {bots}", inline=True)
channels_total = len(guild.text_channels) + len(guild.voice_channels)
channels_total += len(guild.stage_channels) if hasattr(guild, "stage_channels") else 0
channels_total += len(guild.forum_channels) if hasattr(guild, "forum_channels") else 0
embed.add_field(
name="Channels",
value=(
f"Total: {channels_total}\nText: {len(guild.text_channels)}\nVoice: {len(guild.voice_channels)}\n"
f"Stage: {len(guild.stage_channels) if hasattr(guild, 'stage_channels') else 0}\n"
f"Forum: {len(guild.forum_channels) if hasattr(guild, 'forum_channels') else 0}"
),
inline=True,
)
embed.add_field(name="Roles", value=len(guild.roles), inline=True)
if guild.features:
embed.add_field(
name="Features",
value=", ".join(feature.replace("_", " ").title() for feature in guild.features),
inline=False,
)
embed.set_footer(
text=f"Requested by {interaction.user.name}",
icon_url=interaction.user.avatar.url if interaction.user.avatar else None,
)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="botinfo", description="Displays information and stats about the bot.")
@app_commands.checks.cooldown(1, 10, key=lambda i: i.user.id)
async def botinfo(self, interaction: discord.Interaction):
delta_uptime = discord.utils.utcnow() - self.bot.start_time
days = delta_uptime.days
hours, remainder = divmod(int(delta_uptime.total_seconds() % 86400), 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}d {hours}h {minutes}m {seconds}s"
embed = discord.Embed(
title=f"{self.bot.user.name} Stats", color=discord.Color.purple(), timestamp=discord.utils.utcnow()
)
if self.bot.user.avatar:
embed.set_thumbnail(url=self.bot.user.avatar.url)
embed.add_field(name="Developer", value="Sentinel Team", inline=True)
embed.add_field(name="Servers", value=str(len(self.bot.guilds)), inline=True)
embed.add_field(name="Total Users", value=str(len(self.bot.users)), inline=True)
embed.add_field(name="Python Version", value=sys.version.split(" ")[0], inline=True)
embed.add_field(name="discord.py Version", value=discord.__version__, inline=True)
embed.add_field(name="Uptime", value=uptime_str, inline=True)
await interaction.response.send_message(embed=embed)
@app_commands.command(name="uptime", description="Shows how long the bot has been online, and server info.")
@app_commands.checks.cooldown(1, 5, key=lambda i: i.user.id)
async def uptime(self, interaction: discord.Interaction):
delta_uptime = discord.utils.utcnow() - self.bot.start_time
days = delta_uptime.days
hours, remainder = divmod(int(delta_uptime.total_seconds() % 86400), 3600)
minutes, seconds = divmod(remainder, 60)
uptime_str = f"{days}d {hours}h {minutes}m {seconds}s"
embed = discord.Embed(title="Bot Uptime & System Info", color=discord.Color.teal())
embed.add_field(name="Bot Uptime", value=f"**{uptime_str}**", inline=False)
embed.add_field(
name="Operating System",
value=f"{platform.system()} {platform.release()} ({platform.version()})",
inline=False,
)
embed.add_field(name="Architecture", value=platform.machine(), inline=False)
await interaction.response.send_message(embed=embed)
def _iter_commands(self):
stack = list(self.bot.tree.get_commands())
while stack:
command = stack.pop(0)
yield command
if isinstance(command, app_commands.Group):
stack[0:0] = command.commands
def _get_full_command_name(self, command: Union[app_commands.Command, app_commands.Group]) -> str:
parts = [command.name]
parent = command.parent
while parent is not None:
parts.append(parent.name)
parent = parent.parent
return " ".join(reversed(parts))
def _command_support_label(self, command: Union[app_commands.Command, app_commands.Group]) -> str:
checks = getattr(command, "checks", [])
if any(getattr(check, "__qualname__", "").endswith("guild_only.<locals>.predicate") for check in checks):
return "Servers only"
return "Servers and DMs"
def _command_permission_label(self, command: Union[app_commands.Command, app_commands.Group]) -> Optional[str]:
checks = getattr(command, "checks", [])
for check in checks:
qualname = get
gitextract_ia4j90bs/
├── .env.example
├── .flake8
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── SUPPORT.md
├── cogs/
│ ├── chat.py
│ ├── community.py
│ ├── economy.py
│ ├── farming.py
│ ├── fun.py
│ ├── games.py
│ ├── interactions.py
│ ├── media.py
│ ├── moderation.py
│ └── utility.py
├── config.example.json
├── config_loader.py
├── docs/
│ ├── commands.md
│ ├── configuration.md
│ ├── deployment.md
│ ├── faq.md
│ └── operations.md
├── install.bat
├── install.sh
├── main.py
├── requirements-dev.txt
├── requirements.txt
├── site/
│ ├── 404.html
│ ├── _headers
│ ├── index.html
│ ├── script.js
│ └── styles.css
└── tests/
├── conftest.py
├── test_config_loader.py
├── test_duration_parsing.py
├── test_issue_fixes.py
└── test_retry_delays.py
SYMBOL INDEX (273 symbols across 16 files)
FILE: cogs/chat.py
class Chat (line 29) | class Chat(commands.Cog):
method __init__ (line 30) | def __init__(self, bot: commands.Bot):
method cog_unload (line 39) | def cog_unload(self):
method load_config (line 42) | def load_config(self):
method _prune_cooldowns (line 54) | async def _prune_cooldowns(self):
method setup_database (line 60) | async def setup_database(self):
method _table_columns (line 100) | async def _table_columns(self, cursor, table_name: str) -> List[str]:
method session (line 105) | def session(self):
method _context_key (line 108) | def _context_key(self, interaction: discord.Interaction) -> Tuple[Any,...
method _serialize_ids (line 113) | def _serialize_ids(self, ids: List[int]) -> Optional[str]:
method _deserialize_ids (line 117) | def _deserialize_ids(self, raw: Optional[str]) -> List[int]:
method get_guild_config (line 126) | async def get_guild_config(self, guild_id: Optional[int]):
method set_guild_key (line 133) | async def set_guild_key(self, guild_id: int, key: Optional[str] = None):
method set_guild_persona (line 139) | async def set_guild_persona(self, guild_id: int, persona: Optional[str...
method get_policy (line 145) | async def get_policy(self, guild_id: Optional[int]) -> Dict[str, Any]:
method update_policy (line 184) | async def update_policy(self, guild_id: int, **fields):
method mutate_id_list (line 191) | async def mutate_id_list(self, guild_id: int, field: str, value: int, ...
method get_usage_count (line 201) | async def get_usage_count(self, guild_id: int) -> int:
method increment_usage (line 211) | async def increment_usage(self, guild_id: int):
method validate_api_key (line 225) | async def validate_api_key(self, api_key: str) -> Tuple[bool, str]:
method enforce_policy (line 241) | async def enforce_policy(self, interaction: discord.Interaction, polic...
method define_tools (line 275) | def define_tools(self):
method execute_google_search (line 291) | async def execute_google_search(self, query: str):
method model_autocomplete (line 308) | async def model_autocomplete(self, interaction: discord.Interaction, c...
method _channel_labels (line 316) | def _channel_labels(self, guild: discord.Guild, ids: List[int]) -> str:
method _role_labels (line 325) | def _role_labels(self, guild: discord.Guild, ids: List[int]) -> str:
method _truncate_field_value (line 334) | def _truncate_field_value(self, value: str, limit: int = MAX_EMBED_FIE...
method _trim_history (line 339) | def _trim_history(self, messages: List[Dict[str, Any]]) -> List[Dict[s...
method _prune_runtime_state (line 344) | def _prune_runtime_state(self) -> None:
method _set_conversation_state (line 359) | def _set_conversation_state(self, context_key: Tuple[Any, ...], messag...
method set_key (line 376) | async def set_key(self, interaction: discord.Interaction, key: str):
method set_persona (line 395) | async def set_persona(self, interaction: discord.Interaction, persona:...
method set_enabled (line 408) | async def set_enabled(self, interaction: discord.Interaction, enabled:...
method set_cooldown (line 416) | async def set_cooldown(self, interaction: discord.Interaction, seconds...
method set_usage_cap (line 423) | async def set_usage_cap(self, interaction: discord.Interaction, limit:...
method allow_channel (line 431) | async def allow_channel(self, interaction: discord.Interaction, channe...
method block_channel (line 440) | async def block_channel(self, interaction: discord.Interaction, channe...
method clear_channel_rules (line 449) | async def clear_channel_rules(self, interaction: discord.Interaction):
method allow_role (line 455) | async def allow_role(self, interaction: discord.Interaction, role: dis...
method remove_role (line 464) | async def remove_role(self, interaction: discord.Interaction, role: di...
method clear_role_rules (line 473) | async def clear_role_rules(self, interaction: discord.Interaction):
method view_config (line 479) | async def view_config(self, interaction: discord.Interaction):
method test_config (line 532) | async def test_config(self, interaction: discord.Interaction):
method list_models (line 549) | async def list_models(self, interaction: discord.Interaction):
method chat (line 566) | async def chat(self, interaction: discord.Interaction, prompt: str, mo...
method chat_reset (line 680) | async def chat_reset(self, interaction: discord.Interaction):
function setup (line 687) | async def setup(bot: commands.Bot):
FILE: cogs/community.py
function parse_duration (line 21) | def parse_duration(value: str) -> Optional[int]:
class Community (line 31) | class Community(commands.Cog):
method __init__ (line 32) | def __init__(self, bot: commands.Bot):
method cog_unload (line 36) | def cog_unload(self):
method setup_database (line 39) | async def setup_database(self):
method get_settings (line 87) | async def get_settings(self, guild_id: int) -> Dict[str, Optional[int]]:
method update_setting (line 122) | async def update_setting(self, guild_id: int, field: str, value):
method validate_template (line 128) | def validate_template(self, template: str) -> Optional[str]:
method render_template (line 141) | def render_template(self, template: Optional[str], member: discord.abc...
method try_render_template (line 145) | def try_render_template(
method schedule_retry_delay (line 153) | def schedule_retry_delay(self, failures: int) -> timedelta:
method _resolve_channel (line 157) | async def _resolve_channel(self, channel_id: Optional[int]):
method _send_to_channel (line 168) | async def _send_to_channel(
method _log_to_modlog (line 187) | async def _log_to_modlog(self, guild: discord.Guild, title: str, descr...
method _channel_value (line 196) | def _channel_value(self, channel_id: Optional[int]) -> str:
method _schedule_announcement (line 199) | async def _schedule_announcement(
method view (line 233) | async def view(self, interaction: discord.Interaction):
method set_welcome_channel (line 258) | async def set_welcome_channel(self, interaction: discord.Interaction, ...
method set_goodbye_channel (line 264) | async def set_goodbye_channel(self, interaction: discord.Interaction, ...
method set_announcement_channel (line 270) | async def set_announcement_channel(self, interaction: discord.Interact...
method set_modlog_channel (line 276) | async def set_modlog_channel(self, interaction: discord.Interaction, c...
method set_welcome_message (line 283) | async def set_welcome_message(self, interaction: discord.Interaction, ...
method set_goodbye_message (line 296) | async def set_goodbye_message(self, interaction: discord.Interaction, ...
method preview_welcome (line 308) | async def preview_welcome(self, interaction: discord.Interaction):
method preview_goodbye (line 323) | async def preview_goodbye(self, interaction: discord.Interaction):
method reset_message (line 344) | async def reset_message(self, interaction: discord.Interaction, target...
method reset_channel (line 358) | async def reset_channel(self, interaction: discord.Interaction, target...
method announce (line 366) | async def announce(self, interaction: discord.Interaction, message: str):
method schedule (line 415) | async def schedule(
method list_scheduled (line 463) | async def list_scheduled(self, interaction: discord.Interaction):
method diagnose_scheduled (line 496) | async def diagnose_scheduled(self, interaction: discord.Interaction, a...
method cancel_scheduled (line 532) | async def cancel_scheduled(self, interaction: discord.Interaction, ann...
method schedule_loop (line 546) | async def schedule_loop(self):
method before_schedule_loop (line 629) | async def before_schedule_loop(self):
method on_member_join (line 634) | async def on_member_join(self, member: discord.Member):
method on_member_remove (line 656) | async def on_member_remove(self, member: discord.Member):
method on_message_delete (line 678) | async def on_message_delete(self, message: discord.Message):
function setup (line 696) | async def setup(bot: commands.Bot):
FILE: cogs/economy.py
class Economy (line 34) | class Economy(commands.Cog):
method __init__ (line 35) | def __init__(self, bot: commands.Bot):
method _guild_lock (line 40) | def _guild_lock(self, guild_id: int) -> asyncio.Lock:
method setup_database (line 47) | async def setup_database(self):
method _get_guild_id (line 91) | def _get_guild_id(self, interaction: discord.Interaction) -> int:
method _get_or_create_user_unlocked (line 96) | async def _get_or_create_user_unlocked(self, guild_id: int, user_id: i...
method get_or_create_user (line 112) | async def get_or_create_user(self, guild_id: int, user_id: int) -> int:
method change_balance (line 116) | async def change_balance(self, guild_id: int, user_id: int, delta: int...
method transfer_balance (line 131) | async def transfer_balance(self, guild_id: int, sender_id: int, receiv...
method set_balance (line 150) | async def set_balance(self, guild_id: int, user_id: int, amount: int) ...
method balance (line 168) | async def balance(self, interaction: discord.Interaction, member: disc...
method daily (line 179) | async def daily(self, interaction: discord.Interaction):
method daily_error (line 192) | async def daily_error(self, interaction: discord.Interaction, error: a...
method jobs_freelance (line 204) | async def jobs_freelance(self, interaction: discord.Interaction):
method freelance_error (line 217) | async def freelance_error(self, interaction: discord.Interaction, erro...
method jobs_regular (line 226) | async def jobs_regular(self, interaction: discord.Interaction):
method regular_work_error (line 239) | async def regular_work_error(self, interaction: discord.Interaction, e...
method jobs_crime (line 248) | async def jobs_crime(self, interaction: discord.Interaction):
method crime_error (line 266) | async def crime_error(self, interaction: discord.Interaction, error: a...
method gamble (line 277) | async def gamble(self, interaction: discord.Interaction, amount: app_c...
method leaderboard (line 301) | async def leaderboard(self, interaction: discord.Interaction):
method transfer (line 337) | async def transfer(
method rob (line 366) | async def rob(self, interaction: discord.Interaction, member: discord....
method rob_error (line 418) | async def rob_error(self, interaction: discord.Interaction, error: app...
method slots (line 429) | async def slots(self, interaction: discord.Interaction, bet: app_comma...
method admin_add (line 465) | async def admin_add(self, interaction: discord.Interaction, member: di...
method admin_remove (line 476) | async def admin_remove(
method admin_set (line 491) | async def admin_set(self, interaction: discord.Interaction, member: di...
method admin_reset_guild (line 501) | async def admin_reset_guild(self, interaction: discord.Interaction):
function setup (line 510) | async def setup(bot: commands.Bot):
FILE: cogs/farming.py
class Farming (line 20) | class Farming(commands.Cog):
method __init__ (line 21) | def __init__(self, bot: commands.Bot):
method setup_database (line 38) | async def setup_database(self):
method get_farm_data (line 96) | async def get_farm_data(self, guild_id: int, user_id: int):
method get_xp_for_next_level (line 109) | def get_xp_for_next_level(self, level: int):
method profile (line 115) | async def profile(self, interaction: discord.Interaction):
method shop (line 152) | async def shop(self, interaction: discord.Interaction):
method plant (line 172) | async def plant(self, interaction: discord.Interaction, crop: str):
method harvest (line 228) | async def harvest(self, interaction: discord.Interaction):
method upgrade (line 300) | async def upgrade(self, interaction: discord.Interaction):
function setup (line 343) | async def setup(bot: commands.Bot):
FILE: cogs/fun.py
class Fun (line 10) | class Fun(commands.Cog):
method __init__ (line 11) | def __init__(self, bot: commands.Bot):
method joke (line 103) | async def joke(self, interaction: discord.Interaction):
method fact (line 111) | async def fact(self, interaction: discord.Interaction):
method avatar (line 131) | async def avatar(self, interaction: discord.Interaction, member: disco...
method love (line 140) | async def love(self, interaction: discord.Interaction, member1: discor...
method emojify (line 159) | async def emojify(self, interaction: discord.Interaction, text: str):
method poll (line 206) | async def poll(
method clap (line 249) | async def clap(self, interaction: discord.Interaction, text: str):
method tweet (line 259) | async def tweet(self, interaction: discord.Interaction, text: str):
function setup (line 307) | async def setup(bot: commands.Bot):
FILE: cogs/games.py
class TicTacToeButton (line 8) | class TicTacToeButton(discord.ui.Button["TicTacToe"]):
method __init__ (line 9) | def __init__(self, x: int, y: int):
method callback (line 14) | async def callback(self, interaction: discord.Interaction):
class TicTacToe (line 57) | class TicTacToe(discord.ui.View):
method __init__ (line 62) | def __init__(self, player1: discord.Member, player2: discord.Member):
method on_timeout (line 77) | async def on_timeout(self):
method check_board_winner (line 83) | def check_board_winner(self):
class Games (line 111) | class Games(commands.Cog):
method __init__ (line 112) | def __init__(self, bot: commands.Bot):
method eightball (line 119) | async def eightball(self, interaction: discord.Interaction, question: ...
method coinflip (line 149) | async def coinflip(self, interaction: discord.Interaction):
method roll (line 156) | async def roll(self, interaction: discord.Interaction, dice: str):
method guess (line 177) | async def guess(self, interaction: discord.Interaction):
method rockpaperscissors (line 223) | async def rockpaperscissors(self, interaction: discord.Interaction, ch...
method tictactoe (line 245) | async def tictactoe(self, interaction: discord.Interaction, opponent: ...
function setup (line 263) | async def setup(bot: commands.Bot):
FILE: cogs/interactions.py
class Interactions (line 6) | class Interactions(commands.Cog):
method __init__ (line 7) | def __init__(self, bot: commands.Bot):
method get_gif (line 11) | async def get_gif(self, category: str):
method create_interaction_embed (line 21) | async def create_interaction_embed(
method hug (line 53) | async def hug(self, interaction: discord.Interaction, member: discord....
method pat (line 65) | async def pat(self, interaction: discord.Interaction, member: discord....
method slap (line 77) | async def slap(self, interaction: discord.Interaction, member: discord...
method kiss (line 93) | async def kiss(self, interaction: discord.Interaction, member: discord...
method cuddle (line 105) | async def cuddle(self, interaction: discord.Interaction, member: disco...
method poke (line 117) | async def poke(self, interaction: discord.Interaction, member: discord...
function setup (line 127) | async def setup(bot: commands.Bot):
FILE: cogs/media.py
class Media (line 10) | class Media(commands.Cog):
method __init__ (line 11) | def __init__(self, bot: commands.Bot):
method meme (line 17) | async def meme(self, interaction: discord.Interaction):
method cat (line 35) | async def cat(self, interaction: discord.Interaction):
method dog (line 52) | async def dog(self, interaction: discord.Interaction):
function setup (line 68) | async def setup(bot: commands.Bot):
FILE: cogs/moderation.py
class Moderation (line 15) | class Moderation(commands.Cog):
method __init__ (line 16) | def __init__(self, bot: commands.Bot):
method setup_database (line 20) | async def setup_database(self):
method _serialize_ids (line 54) | def _serialize_ids(self, ids: List[int]) -> Optional[str]:
method _deserialize_ids (line 58) | def _deserialize_ids(self, raw: Optional[str]) -> List[int]:
method get_automod_settings (line 67) | async def get_automod_settings(self, guild_id: int) -> Dict[str, object]:
method update_automod (line 97) | async def update_automod(self, guild_id: int, **fields):
method mutate_whitelist (line 104) | async def mutate_whitelist(self, guild_id: int, channel_id: int, add: ...
method add_warning (line 114) | async def add_warning(self, guild_id: int, user_id: int, moderator_id:...
method log_action (line 127) | async def log_action(self, guild: discord.Guild, title: str, descripti...
method automod_view (line 136) | async def automod_view(self, interaction: discord.Interaction):
method automod_toggle_invites (line 150) | async def automod_toggle_invites(self, interaction: discord.Interactio...
method automod_toggle_links (line 156) | async def automod_toggle_links(self, interaction: discord.Interaction,...
method automod_set_action (line 169) | async def automod_set_action(self, interaction: discord.Interaction, a...
method automod_set_bad_words (line 175) | async def automod_set_bad_words(self, interaction: discord.Interaction...
method automod_clear_bad_words (line 183) | async def automod_clear_bad_words(self, interaction: discord.Interacti...
method automod_whitelist_channel (line 189) | async def automod_whitelist_channel(self, interaction: discord.Interac...
method automod_remove_whitelist_channel (line 198) | async def automod_remove_whitelist_channel(self, interaction: discord....
method warn (line 208) | async def warn(self, interaction: discord.Interaction, member: discord...
method warnings (line 224) | async def warnings(self, interaction: discord.Interaction, member: dis...
method clear_warning (line 256) | async def clear_warning(self, interaction: discord.Interaction, warnin...
method handle_violation (line 276) | async def handle_violation(self, message: discord.Message, reason: str...
method automod_listener (line 317) | async def automod_listener(self, message: discord.Message):
function setup (line 352) | async def setup(bot: commands.Bot):
FILE: cogs/utility.py
function parse_duration_spec (line 22) | def parse_duration_spec(value: str) -> Optional[int]:
class Utility (line 30) | class Utility(commands.Cog):
method __init__ (line 31) | def __init__(self, bot: commands.Bot):
method cog_unload (line 38) | def cog_unload(self):
method setup_database (line 41) | async def setup_database(self):
method get_afk_status (line 86) | async def get_afk_status(self, guild_id: int, user_id: int):
method get_many_afk_statuses (line 100) | async def get_many_afk_statuses(self, guild_id: int, user_ids: list[in...
method set_afk_status (line 133) | async def set_afk_status(self, guild_id: int, user_id: int, reason: str):
method clear_afk_status (line 148) | async def clear_afk_status(self, guild_id: int, user_id: int):
method list_user_reminders (line 157) | async def list_user_reminders(self, user_id: int):
method reminder_retry_delay (line 170) | def reminder_retry_delay(self, failures: int) -> timedelta:
method create_reminder (line 174) | async def create_reminder(
method ping (line 200) | async def ping(self, interaction: discord.Interaction):
method ping_raw (line 206) | async def ping_raw(self, interaction: discord.Interaction):
method memberinfo (line 230) | async def memberinfo(self, interaction: discord.Interaction, member: d...
method clear (line 264) | async def clear(self, interaction: discord.Interaction, amount: app_co...
method serverinfo (line 279) | async def serverinfo(self, interaction: discord.Interaction):
method botinfo (line 327) | async def botinfo(self, interaction: discord.Interaction):
method uptime (line 351) | async def uptime(self, interaction: discord.Interaction):
method _iter_commands (line 369) | def _iter_commands(self):
method _get_full_command_name (line 377) | def _get_full_command_name(self, command: Union[app_commands.Command, ...
method _command_support_label (line 385) | def _command_support_label(self, command: Union[app_commands.Command, ...
method _command_permission_label (line 391) | def _command_permission_label(self, command: Union[app_commands.Comman...
method command_autocomplete (line 402) | async def command_autocomplete(
method help_all (line 413) | async def help_all(self, interaction: discord.Interaction):
method _iter_group_commands (line 459) | def _iter_group_commands(self, group: app_commands.Group):
method help_command (line 470) | async def help_command(self, interaction: discord.Interaction, command...
method remindme (line 504) | async def remindme(self, interaction: discord.Interaction, time: str, ...
method reminders_recurring (line 540) | async def reminders_recurring(self, interaction: discord.Interaction, ...
method reminders_list (line 574) | async def reminders_list(self, interaction: discord.Interaction):
method reminders_cancel (line 601) | async def reminders_cancel(self, interaction: discord.Interaction, rem...
method reminders_snooze (line 617) | async def reminders_snooze(self, interaction: discord.Interaction, rem...
method reminders_clear (line 646) | async def reminders_clear(self, interaction: discord.Interaction):
method afk (line 656) | async def afk(self, interaction: discord.Interaction, reason: str = "A...
method afk_clear (line 662) | async def afk_clear(self, interaction: discord.Interaction):
method afk_message_listener (line 667) | async def afk_message_listener(self, message: discord.Message):
method reminder_loop (line 708) | async def reminder_loop(self):
method before_reminder_loop (line 805) | async def before_reminder_loop(self):
function setup (line 810) | async def setup(bot: commands.Bot):
FILE: config_loader.py
function _load_file_config (line 15) | def _load_file_config() -> dict[str, Any]:
function _get_bool (line 23) | def _get_bool(name: str, default: bool, file_config: dict[str, Any]) -> ...
function _get_list (line 37) | def _get_list(name: str, default: list[str], file_config: dict[str, Any]...
function load_runtime_config (line 51) | def load_runtime_config() -> dict[str, Any]:
FILE: main.py
function _find_local_venv_python (line 11) | def _find_local_venv_python() -> Optional[Path]:
function _maybe_reexec_into_local_venv (line 20) | def _maybe_reexec_into_local_venv() -> None:
class FunBot (line 79) | class FunBot(commands.Bot):
method __init__ (line 80) | def __init__(self, runtime_config: dict):
method setup_hook (line 90) | async def setup_hook(self):
method on_ready (line 134) | async def on_ready(self):
method _flush_message_logs (line 139) | async def _flush_message_logs(self, batch: list[tuple[int, int, int, s...
method _message_log_worker (line 149) | async def _message_log_worker(self) -> None:
method on_message (line 181) | async def on_message(self, message: discord.Message):
method close (line 231) | async def close(self):
method _prune_message_logs_task (line 247) | async def _prune_message_logs_task(self):
method _before_prune_message_logs (line 256) | async def _before_prune_message_logs(self):
function on_app_command_error (line 264) | async def on_app_command_error(interaction: discord.Interaction, error: ...
FILE: tests/test_config_loader.py
function test_load_runtime_config_prefers_env_over_file (line 6) | def test_load_runtime_config_prefers_env_over_file(monkeypatch, tmp_path):
function test_load_runtime_config_falls_back_to_config_json (line 30) | def test_load_runtime_config_falls_back_to_config_json(monkeypatch, tmp_...
FILE: tests/test_duration_parsing.py
function test_parse_duration_spec_supports_seconds_minutes_hours_days (line 5) | def test_parse_duration_spec_supports_seconds_minutes_hours_days():
function test_parse_duration_spec_rejects_invalid_values (line 12) | def test_parse_duration_spec_rejects_invalid_values():
function test_parse_duration_supports_minutes_hours_days (line 18) | def test_parse_duration_supports_minutes_hours_days():
function test_parse_duration_rejects_invalid_values (line 24) | def test_parse_duration_rejects_invalid_values():
FILE: tests/test_issue_fixes.py
class FakeCursor (line 14) | class FakeCursor:
method __init__ (line 15) | def __init__(self, rows=None):
method __aenter__ (line 19) | async def __aenter__(self):
method __aexit__ (line 22) | async def __aexit__(self, exc_type, exc, tb):
method execute (line 25) | async def execute(self, query, params=()):
method fetchall (line 28) | async def fetchall(self):
class FakeDB (line 32) | class FakeDB:
method __init__ (line 33) | def __init__(self, rows=None):
method cursor (line 38) | def cursor(self):
method commit (line 43) | async def commit(self):
function test_afk_listener_deduplicates_repeated_mentions (line 48) | async def test_afk_listener_deduplicates_repeated_mentions():
function test_reminder_loop_queries_due_work_in_bounded_batches (line 75) | async def test_reminder_loop_queries_due_work_in_bounded_batches():
function test_schedule_loop_queries_due_work_in_bounded_batches (line 86) | async def test_schedule_loop_queries_due_work_in_bounded_batches():
function test_chat_prune_cooldowns_removes_only_expired_entries (line 97) | async def test_chat_prune_cooldowns_removes_only_expired_entries():
function test_economy_guild_lock_is_scoped_per_guild (line 113) | def test_economy_guild_lock_is_scoped_per_guild():
FILE: tests/test_retry_delays.py
function test_reminder_retry_delay_grows_and_caps (line 7) | def test_reminder_retry_delay_grows_and_caps():
function test_schedule_retry_delay_grows_and_caps (line 16) | def test_schedule_retry_delay_grows_and_caps():
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (308K chars).
[
{
"path": ".env.example",
"chars": 558,
"preview": "# Milo configuration\n# Copy this file to `.env` and replace placeholder values.\n\n# Required\nDISCORD_TOKEN=your_discord_b"
},
{
"path": ".flake8",
"chars": 127,
"preview": "[flake8]\nmax-line-length = 120\nexclude = \n .git,\n __pycache__,\n .venv,\n venv,\n database\nignore = E203,E50"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 1776,
"preview": "name: Bug Report\ndescription: Report a reproducible problem in Milo\ntitle: \"[Bug]: \"\nlabels:\n - bug\nbody:\n - type: mar"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1219,
"preview": "name: Feature Request\ndescription: Suggest an improvement or new feature for Milo\ntitle: \"[Feature]: \"\nlabels:\n - enhan"
},
{
"path": ".github/pull_request_template.md",
"chars": 335,
"preview": "## Summary\n\n- What problem does this change solve?\n\n## Changes\n\n- What changed?\n\n## Verification\n\n- How was this tested?"
},
{
"path": ".github/workflows/ci.yml",
"chars": 672,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n pull_request:\n\njobs:\n compile:\n runs-on: ubuntu-latest\n\n steps"
},
{
"path": ".github/workflows/release.yml",
"chars": 448,
"preview": "name: Release\n\non:\n push:\n tags:\n - \"v*\"\n\npermissions:\n contents: write\n\njobs:\n create-release:\n runs-on: "
},
{
"path": ".gitignore",
"chars": 116,
"preview": "config.json\n.env\n.env.*\n!.env.example\n__pycache__/\n*.py[cod]\n.wrangler/\n.venv/\ndatabase/*.db\n/Milo-discord-fun-bot/\n"
},
{
"path": "CHANGELOG.md",
"chars": 1115,
"preview": "## Changelog\n\n**v1.0.4** – 2026-03-20\n- Added bounded reminder and scheduled announcement polling, plus admin diagnostic"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 1039,
"preview": "# Code of Conduct\n\n## Our Standard\n\nParticipation in this project should be respectful, direct, and constructive.\n\nExamp"
},
{
"path": "CONTRIBUTING.md",
"chars": 2206,
"preview": "# Contributing to Milo\n\nThanks for contributing. Keep changes small, reviewable, and easy to test.\n\n## Before You Start\n"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "Copyright 2026 Sentinel Team and contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 7116,
"preview": "# Milo\n\nMilo is an open-source Discord bot for community operations, support, and lightweight moderation, built with `di"
},
{
"path": "SECURITY.md",
"chars": 1022,
"preview": "# Security Policy\n\n## Supported Versions\n\nOnly the latest code on `main` should be assumed to receive security fixes.\n\n#"
},
{
"path": "SUPPORT.md",
"chars": 816,
"preview": "# Support\n\n## Getting Help\n\nUse the repository issue tracker for:\n\n- Reproducible bugs\n- Feature requests\n- Documentatio"
},
{
"path": "cogs/chat.py",
"chars": 33647,
"preview": "import json\nimport logging\nfrom collections import defaultdict\nfrom datetime import timedelta\nfrom typing import Any, Di"
},
{
"path": "cogs/community.py",
"chars": 33179,
"preview": "import string\nfrom datetime import timedelta\nfrom typing import Dict, Optional\n\nimport discord\nfrom discord import app_c"
},
{
"path": "cogs/economy.py",
"chars": 24213,
"preview": "import asyncio\nimport logging\nimport random\n\nimport discord\nfrom discord import app_commands\nfrom discord.ext import com"
},
{
"path": "cogs/farming.py",
"chars": 15570,
"preview": "import logging\nimport math\nimport random\nfrom datetime import datetime, timedelta\n\nimport discord\nfrom discord import ap"
},
{
"path": "cogs/fun.py",
"chars": 16565,
"preview": "import discord\nfrom discord import app_commands\nfrom discord.ext import commands\nimport random\nfrom PIL import Image, Im"
},
{
"path": "cogs/games.py",
"chars": 10613,
"preview": "import discord\nfrom discord import app_commands\nfrom discord.ext import commands\nimport random\nimport asyncio\n\n\nclass Ti"
},
{
"path": "cogs/interactions.py",
"chars": 5318,
"preview": "import discord\nfrom discord import app_commands\nfrom discord.ext import commands\n\n\nclass Interactions(commands.Cog):\n "
},
{
"path": "cogs/media.py",
"chars": 3487,
"preview": "import logging\n\nimport discord\nfrom discord import app_commands\nfrom discord.ext import commands\n\nlogger = logging.getLo"
},
{
"path": "cogs/moderation.py",
"chars": 16953,
"preview": "import json\nimport re\nfrom datetime import timedelta\nfrom typing import Dict, List, Optional\n\nimport discord\nfrom discor"
},
{
"path": "cogs/utility.py",
"chars": 37151,
"preview": "import platform\nimport re\nimport sys\nimport time\nfrom datetime import timedelta\nfrom typing import Optional, Union\n\nimpo"
},
{
"path": "config.example.json",
"chars": 381,
"preview": "{\n \"DISCORD_TOKEN\": \"your_discord_token_here\",\n \"OPENAI_API_KEY\": \"your_openai_api_key_here\",\n \"OPENAI_API_BASE\": \"ht"
},
{
"path": "config_loader.py",
"chars": 2173,
"preview": "import json\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\n\n\nCONFIG_PATH = Pa"
},
{
"path": "docs/commands.md",
"chars": 6608,
"preview": "# Command Reference\n\nThis document summarizes the current slash commands exposed by Milo.\n\n## AI Chat\n\n- `/chat`\n Purpo"
},
{
"path": "docs/configuration.md",
"chars": 2285,
"preview": "# Configuration Guide\n\nMilo supports two config sources:\n\n1. Environment variables, including values loaded from `.env`\n"
},
{
"path": "docs/deployment.md",
"chars": 1939,
"preview": "# Deployment Guide\n\nThis guide covers practical ways to run Milo outside a local development shell.\n\n## Requirements\n\n- "
},
{
"path": "docs/faq.md",
"chars": 2176,
"preview": "# FAQ\n\n## Does Milo support AI chat in DMs?\n\nYes. `/chat` and `/chat-reset` work in DMs. Server configuration commands u"
},
{
"path": "docs/operations.md",
"chars": 1706,
"preview": "# Operations Notes\n\nThis document covers behavior that matters when hosting or maintaining Milo.\n\n## Runtime Requirement"
},
{
"path": "install.bat",
"chars": 6749,
"preview": "@echo off\ntitle Milo Discord Fun Bot Installer v1.3.0\n\nsetlocal enabledelayedexpansion\n\n:: ---------------- ASCII LOGO -"
},
{
"path": "install.sh",
"chars": 4717,
"preview": "#!/usr/bin/env bash\n\ncat << \"EOF\"\n __ __ _ _ _ ____ _ \n | \\/ (_) |_ ___| |__ | __ ) ___ "
},
{
"path": "main.py",
"chars": 10715,
"preview": "import asyncio\nimport os\nimport sys\nfrom collections import defaultdict\nimport datetime\nfrom pathlib import Path\nfrom ty"
},
{
"path": "requirements-dev.txt",
"chars": 123,
"preview": "# Development dependencies\n-r requirements.txt\n\n# Code quality tools\nflake8>=7.0.0\nblack>=24.0.0\nmypy>=1.8.0\npylint>=3.0"
},
{
"path": "requirements.txt",
"chars": 49,
"preview": "discord.py\naiohttp\npython-dotenv\naiosqlite\nPillow"
},
{
"path": "site/404.html",
"chars": 1512,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "site/_headers",
"chars": 175,
"preview": "/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin\n Permissions-Policy: camera=(),"
},
{
"path": "site/index.html",
"chars": 11944,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "site/script.js",
"chars": 2464,
"preview": "document.addEventListener('DOMContentLoaded', () => {\n const revealTargets = document.querySelectorAll('.animate-on-scr"
},
{
"path": "site/styles.css",
"chars": 13828,
"preview": ":root {\n --bg: #09090b;\n --card: #131316;\n --border: rgba(255, 255, 255, 0.08);\n --border-hover: rgba(255, 255, 255,"
},
{
"path": "tests/conftest.py",
"chars": 169,
"preview": "import sys\nfrom pathlib import Path\n\n\nPROJECT_ROOT = Path(__file__).resolve().parents[1]\nif str(PROJECT_ROOT) not in sys"
},
{
"path": "tests/test_config_loader.py",
"chars": 2250,
"preview": "import json\n\nfrom config_loader import load_runtime_config\n\n\ndef test_load_runtime_config_prefers_env_over_file(monkeypa"
},
{
"path": "tests/test_duration_parsing.py",
"chars": 921,
"preview": "from cogs.community import parse_duration\nfrom cogs.utility import parse_duration_spec\n\n\ndef test_parse_duration_spec_su"
},
{
"path": "tests/test_issue_fixes.py",
"chars": 3647,
"preview": "from datetime import timedelta\nfrom types import SimpleNamespace\nfrom unittest.mock import AsyncMock\n\nimport discord\nimp"
},
{
"path": "tests/test_retry_delays.py",
"chars": 944,
"preview": "from datetime import timedelta\n\nfrom cogs.community import Community, SCHEDULE_RETRY_MAX_SECONDS\nfrom cogs.utility impor"
}
]
About this extraction
This page contains the full source code of the msgaxzzz/Milo-discord-fun-bot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (286.9 KB), approximately 66.7k tokens, and a symbol index with 273 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.