Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Required: your Discord bot token (https://discord.com/developers/applications)
MATRIMONY_TOKEN=

# Optional configuration (defaults shown)
MATRIMONY_DEFAULT_PREFIX=!
MATRIMONY_COMMAND_NAME=matr
MATRIMONY_DB_PATH=matrimony.db
# 0 = unlimited (matches the original MarriageBot, which has no partner cap)
MATRIMONY_MAX_PARTNERS=0
MATRIMONY_MAX_CHILDREN=0
MATRIMONY_MAX_TREE_DEPTH=10
# Seconds a proposal stays open (buttons + accept/decline commands)
MATRIMONY_PROPOSAL_TTL=300
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -r requirements-dev.txt
- name: Lint
run: ruff check .
- name: Test
run: python -m pytest tests -q
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,9 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

# Local bot database
matrimony.db
*.db-journal
*.db-wal
*.db-shm
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY matrimony ./matrimony

VOLUME /data
ENV MATRIMONY_DB_PATH=/data/matrimony.db

CMD ["python", "-m", "matrimony"]
176 changes: 174 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,174 @@
# matrimony-bot
Matrimony — a MarriageBot-style Discord bot: marriages, divorces, adoptions, family trees, and custom prefixes (!matr <action> <params>).
# Matrimony

A MarriageBot-style family-tree bot for Discord, written in Python with
[discord.py](https://github.com/Rapptz/discord.py). Users can marry, divorce,
adopt, disown, and render family trees — **for humans and bot accounts alike**.

All commands live under one group, `matr`, invoked with a per-guild prefix
(default `!`):

```
!matr <action> <params>
```

## Features

- **Marriages** — propose, accept/decline (buttons *or* text commands),
divorce, multiple simultaneous partners.
- **Parentage** — `adopt` (they become your child) and `makeparent` (they
become your parent), `disown`, `disownall`, `emancipate`/`runaway`,
`abandon` (wipe your whole family).
- **Family trees** — `tree` (blood relatives) and `fulltree` (partners
included), rendered as an inline text tree, plus `parent`, `children`,
`siblings`, `familysize`, `relationship`.
- **Global trees** — like the original MarriageBot, family relationships are
stored globally and follow users across every server the bot is in.
- **Custom prefixes** — per-guild prefix (`!matr prefix ?` → `?matr marry`).
- **Incest toggle** — `!matr incest` lets server admins permit proposals
between family members (off by default, as upstream).
- **Blocks** — users can `block`/`unblock` others from proposing to them.
- **Admin force commands** — `forcemarry`, `forcedivorce`, `forceadopt`,
`forceemancipate` (Manage Server).
- **Fun commands** — `hug`, `kiss`, `slap`, `punch`, `bite`, `stab`, `ship`.
- **Meta** — `info`, `invite`, `stats`, `help`.

## Bot accounts are first-class

Unlike upstream MarriageBot (which refuses to let bots take part), Matrimony
explicitly supports bot-authored commands and bot targets:

- Humans can marry/adopt bots, bots can marry/adopt humans, and bots can
marry/adopt each other.
- The message listener does **not** ignore `message.author.bot`; other bots'
command messages are dispatched exactly like human ones.
- The only exclusion is Matrimony's *own* messages — enough to prevent
self-command loops while leaving every other bot free to participate.
- Covered by tests: proposal/accept flows driven by bot-authored commands
(`tests/test_dispatch.py`).

## Command reference

| Command | Description |
| --- | --- |
| `!matr help` | Show the command list |
| `!matr marry @user` | Propose to a user or bot |
| `!matr divorce [@user]` | Divorce a partner (required if you have several) |
| `!matr accept [@proposer]` / `decline` | Answer a pending proposal |
| `!matr partners [@user]` | List partners |
| `!matr adopt @user` | Ask to adopt a user as your child |
| `!matr makeparent @user` | Ask a user to become your parent |
| `!matr disown @child` / `disownall` | Remove one/all children |
| `!matr emancipate` (alias `runaway`) | Remove your parent |
| `!matr abandon` | Remove all your family ties |
| `!matr tree [@user]` / `fulltree [@user]` | Blood / full family tree |
| `!matr parent` / `children` / `siblings` [@user] | Relatives at a glance |
| `!matr familysize [@user]` | Size of a user's tree |
| `!matr relationship @a @b` | How B relates to A |
| `!matr block` / `unblock @user` / `blocked` | Manage proposal blocks |
| `!matr prefix [new]` | View/set guild prefix (Manage Server) |
| `!matr incest` | Toggle incest proposals (Manage Server) |
| `!matr forcemarry @a @b` | Force-marry a pair (Manage Server) |
| `!matr forcedivorce @a [@b]` | Force-divorce (Manage Server) |
| `!matr forceadopt @parent @child` | Force adoption (Manage Server) |
| `!matr forceemancipate @user` | Force-remove a parent (Manage Server) |
| `!matr hug`/`kiss`/`slap`/`punch`/`bite`/`stab @user` | Flavour actions |
| `!matr ship @a [@b]` | Deterministic ship percentage |
| `!matr info` / `invite` / `stats` | Bot information |

`@Matrimony matr ...` (mention prefix) also always works.

## Running

### Requirements

- Python 3.10+
- A Discord application + bot token (<https://discord.com/developers/applications>)
- Privileged **Message Content** and **Server Members** intents enabled in the
developer portal (required for prefix commands and member lookups)

### Local

```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # fill in MATRIMONY_TOKEN
export $(grep -v '^#' .env | xargs)
python -m matrimony
```

### Docker

```bash
docker build -t matrimony .
docker run -e MATRIMONY_TOKEN=... -v matrimony-data:/data matrimony
```

### Configuration

| Variable | Default | Description |
| --- | --- | --- |
| `MATRIMONY_TOKEN` | *(required)* | Discord bot token |
| `MATRIMONY_DEFAULT_PREFIX` | `!` | Fallback prefix for guilds/DMs |
| `MATRIMONY_COMMAND_NAME` | `matr` | The command group name (`!matr ...`) |
| `MATRIMONY_DB_PATH` | `matrimony.db` | SQLite database file |
| `MATRIMONY_MAX_PARTNERS` | `0` | Partner cap per user (0 = unlimited) |
| `MATRIMONY_MAX_CHILDREN` | `0` | Children cap per user (0 = unlimited) |
| `MATRIMONY_MAX_TREE_DEPTH` | `10` | Depth limit when rendering trees |
| `MATRIMONY_PROPOSAL_TTL` | `300` | Seconds before a proposal expires |

## Fidelity to the original MarriageBot

Matrimony replicates the original's mechanics as closely as is practical
(sources below):

- Same command surface: marry/divorce/partners, adopt/makeparent/disown/
disownall/emancipate/runaway/abandon, tree/fulltree/parent/children/
siblings/familysize/relationship, block/unblock, incest toggle, force
commands, fun commands, info/invite/stats.
- **Cross-server global family trees.**
- **One parent per child**, matching upstream's single-parent tree model.
- Proposals need the target's consent (upstream uses buttons; Matrimony
supports buttons *and* `accept`/`decline` text commands, which also lets
non-UI bots respond).
- `incest` is a per-guild admin toggle, off by default.
- No partner limit by default (upstream allows polygamy; caps are
configurable here).

**Known divergences:**

- Upstream renders trees as Graphviz images; Matrimony renders inline text
trees (no Graphviz dependency, works in DMs and on lightweight hosts).
- Upstream has no documented "bots can be married" support — Matrimony adds
it deliberately.
- Upstream now primarily uses slash commands; Matrimony uses the requested
`!matr <action>` prefix-command surface (plus mention-prefix support).
- Gold-tier/perks features (custom colours, bigger limits) are not
replicated; limits are plain env vars instead.
- Slash-command-only upstream additions (`customize-tree`, `transfer-gold`,
`runaway` alias kept as `emancipate` alias) are out of scope.

**Sources:**

- [Voxel-Fox-Ltd/MarriageBot](https://github.com/Voxel-Fox-Ltd/MarriageBot) —
original bot source (Python, "cross-server family trees", command list).
- [top.gg command listing](https://top.gg/bot/468281173072805889/commands)
and [alternative.me command listing](https://alternative.me/discord/bots/marriagebot/commands) —
upstream command descriptions including `incest`, `disownall`, `abandon`,
`makeparent`, and force commands.
- [docs.marriagebot.xyz](https://docs.marriagebot.xyz/) — upstream docs.

## Development

```bash
pip install -r requirements-dev.txt
python -m pytest tests # unit tests (no Discord connection needed)
ruff check . # lint
```

Storage is SQLite (`matrimony/store.py`); family-graph logic
(`matrimony/family.py`) is Discord-free and fully unit-tested. CI runs lint +
tests on Python 3.11/3.12.

## License

MIT — see [LICENSE](LICENSE).
3 changes: 3 additions & 0 deletions matrimony/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Matrimony - a MarriageBot-style family-tree Discord bot."""

__version__ = "0.1.0"
6 changes: 6 additions & 0 deletions matrimony/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Entrypoint: ``python -m matrimony``."""

from .bot import run

if __name__ == "__main__":
run()
88 changes: 88 additions & 0 deletions matrimony/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""The Matrimony bot client."""

from __future__ import annotations

import logging

import discord
from discord.ext import commands

from .config import Config
from .family import FamilyView
from .store import Store

log = logging.getLogger("matrimony")


class MatrimonyBot(commands.Bot):
"""commands.Bot subclass wiring together config, store and cogs.

Deliberately does **not** ignore messages from other bot accounts:
marrying/adopting bots is a supported feature, so bot-authored command
messages are dispatched like any other. Only messages authored by this
bot itself are skipped, which is enough to prevent self-command loops.
"""

def __init__(self, config: Config, store: Store | None = None) -> None:
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
super().__init__(
command_prefix=self._get_prefix,
intents=intents,
case_insensitive=True,
help_command=None,
)
self.config = config
self.store = store or Store(config.database_path)
self.family = FamilyView(self.store)

# ------------------------------------------------------------- prefixes

async def _get_prefix(self, bot: MatrimonyBot, message: discord.Message):
"""Per-guild prefix; falls back to the configured default.

``commands.when_mentioned`` support is kept so ``@Matrimony matr ...``
always works regardless of prefix."""
prefix = self.config.default_prefix
if message.guild is not None:
prefix = self.store.get_prefix(message.guild.id) or prefix
return commands.when_mentioned_or(prefix)(bot, message)

# ----------------------------------------------------------- dispatching

def should_process(self, message: discord.Message) -> bool:
"""Whether ``message`` should be run through the command parser.

Returns False only for this bot's own messages (self-loop guard).
Other bots' messages ARE processed so that bot accounts can propose,
accept, and adopt like human users."""
return self.user is None or message.author.id != self.user.id

async def on_message(self, message: discord.Message) -> None:
if not self.should_process(message):
return
await self.process_commands(message)

# --------------------------------------------------------------- setup

async def setup_hook(self) -> None:
from .cogs import ALL_COGS

for cog in ALL_COGS:
await self.add_cog(cog(self))
log.info("Loaded %d cogs", len(ALL_COGS))


def run() -> None:
"""Console entrypoint: ``python -m matrimony``."""
logging.basicConfig(level=logging.INFO)
config = Config.from_env()
if not config.token:
raise SystemExit(
"MATRIMONY_TOKEN is not set. Copy .env.example to .env, fill in "
"your Discord bot token, and export it (or use a process manager "
"that loads .env)."
)
bot = MatrimonyBot(config)
bot.run(config.token)
5 changes: 5 additions & 0 deletions matrimony/cogs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Cog registration for Matrimony."""

from .matr import MatrCog

ALL_COGS = [MatrCog]
Loading
Loading