fix #16: cleanup - remove old/dead code and update docs #35

Merged
shoko merged 1 commits from fix/issue-16-cleanup into main 2026-04-03 21:09:59 +02:00
6 changed files with 87 additions and 252 deletions

View File

@@ -5,27 +5,42 @@
```bash ```bash
git clone https://git.fbrns.co/shoko/jigaido.git git clone https://git.fbrns.co/shoko/jigaido.git
cd jigaido cd jigaido
# Create virtual environment
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
pip install -r requirements.txt
# Install dependencies
pip install -r apps/telegram-bot/requirements.txt
# Run tests
pytest
# Run bot # Run bot
export JIGAIDO_BOT_TOKEN="test:token" export JIGAIDO_BOT_TOKEN="your_bot_token"
python bot.py python -m apps.telegram-bot.bot
``` ```
## Architecture
JIGAIDO follows hexagonal architecture:
- **Core** (`core/`): Pure domain logic - models, ports (interfaces), and services
- **Adapters** (`adapters/`): Infrastructure implementations - storage adapters
- **Apps** (`apps/`): CLI applications - Telegram bot
## Code Style ## Code Style
- Python (no strict formatter enforced yet) - Python 3.10+ with type hints
- Async/await for Telegram handlers - Async/await for Telegram handlers
- Type hints where obvious
- Docstrings for public functions - Docstrings for public functions
- Follow existing code patterns
## Pull Request Workflow ## Pull Request Workflow
1. Branch from `main` 1. Branch from `main`
2. Make changes 2. Make changes
3. Test locally 3. Test locally with `pytest`
4. Open PR with description of what changed and why 4. Open PR with description of what changed and why
5. Someone reviews and merges 5. Someone reviews and merges

View File

@@ -22,14 +22,33 @@ A bounty tracking platform. Currently ships with a Telegram bot for managing and
``` ```
jigaido/ jigaido/
├── core/ # Domain layer (pure Python, no external deps)
│ ├── models.py # Domain dataclasses (Bounty, Tracking)
│ ├── ports.py # Port interfaces
│ └── services.py # Domain services
├── adapters/ # Infrastructure adapters
│ └── storage/
│ └── json_file.py # JSON file storage implementation
├── apps/ ├── apps/
│ └── telegram-bot/ ← first app (Python) │ └── telegram-bot/ # Telegram bot CLI application
│ ├── bot.py │ ├── bot.py # Bot entry point
── commands.py ── commands.py # Command handlers
│ ├── cron.py ├── tests/ # Unit tests
│ ├── db.py ├── config.py # Configuration management
│ └── requirements.txt └── SPEC.md # Full design specification
└── SPEC.md ← full design specification ```
## Quick Start
```bash
# Install dependencies
pip install -r apps/telegram-bot/requirements.txt
# Set bot token
export JIGAIDO_BOT_TOKEN="your_bot_token"
# Run bot
python -m apps.telegram-bot.bot
``` ```
## License ## License

59
SPEC.md
View File

@@ -20,7 +20,7 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
### Stack ### Stack
- **Bot**: `python-telegram-bot` (pure Python, no C extensions) - **Bot**: `python-telegram-bot` (pure Python, no C extensions)
- **Storage**: Per-group JSON files (zero-setup, no DB server) - **Storage**: Per-group JSON files via `adapters/storage/json_file.py`
- **Date parsing**: `dateparser` - **Date parsing**: `dateparser`
- **Runtime**: Python 3.10+ - **Runtime**: Python 3.10+
- **Deployment**: Any $5 VPS with Python 3.10+ - **Deployment**: Any $5 VPS with Python 3.10+
@@ -28,19 +28,40 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
### Directory Structure ### Directory Structure
``` ```
~/.jigaido/ # Data root (~/.jigaido/) jigaido/
├── {group_id}/ ├── core/ # Domain layer
│ ├── group.json # Group bounties │ ├── models.py # Domain dataclasses (Bounty, Tracking)
── {user_id}.json # User tracking within this group ── ports.py # Port interfaces (abstract base classes)
└── {user_id}/ │ └── services.py # Domain services (BountyService, TrackingService)
└── user.json # User's personal bounties (DM mode) ├── adapters/ # Infrastructure adapters
│ └── storage/
│ └── json_file.py # JSON file storage implementation
├── apps/
│ └── telegram-bot/ # Telegram bot CLI application
│ ├── bot.py # Bot entry point
│ └── commands.py # Command handlers
├── config.py # Configuration management
└── tests/ # Unit tests
``` ```
**Note:** Data directory is at `~/.jigaido/` (home directory), NOT inside the repository or app directory. ### Hexagonal Architecture
### Storage Design - **Core** (`core/`): Pure domain logic, no external dependencies
- `models.py`: Domain dataclasses
- `ports.py`: Abstract interfaces for storage
- `services.py`: Business logic
**File: `data/{group_id}/group.json`** - **Adapters** (`adapters/`): Implementations of ports
- `storage/json_file.py`: JSON file-based storage
- **Apps** (`apps/`): CLI applications
- `telegram-bot/`: Telegram bot interface
### Data Storage
Data is stored at `~/.jigaido/` (home directory), NOT inside the repository.
**File: `~/.jigaido/{group_id}/group.json`**
```json ```json
{ {
@@ -58,7 +79,7 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
} }
``` ```
**File: `data/{group_id}/{user_id}.json`** **File: `~/.jigaido/{group_id}/{user_id}.json`**
```json ```json
{ {
@@ -68,7 +89,7 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
} }
``` ```
**File: `data/{user_id}/user.json`** **File: `~/.jigaido/{user_id}/user.json`**
```json ```json
{ {
@@ -87,10 +108,10 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
**Key design decisions:** **Key design decisions:**
1. **Group-isolated storage** — Each group has its own directory. No cross-group access. 1. **Hexagonal architecture** — Core domain is isolated from infrastructure
2. **Bounty IDs are sequential per group.json** — Not global. Each group's file has its own ID counter. 2. **Group-isolated storage** Each group has its own directory. No cross-group access.
3. **Atomic writes** — Uses `tempfile` + `rename` for safe writes. 3. **Bounty IDs are sequential per group.json** — Not global. Each group's file has its own ID counter.
4. **No reminders in v1** — Dropped for simplicity. 4. **Atomic writes** — Uses `tempfile` + `rename` for safe writes.
--- ---
## Commands ## Commands
@@ -102,7 +123,7 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
| `/bounty` | anyone | List all bounties in this group | | `/bounty` | anyone | List all bounties in this group |
| `/my` | anyone | List bounties tracked by you in this group | | `/my` | anyone | List bounties tracked by you in this group |
| `/add <text> [link] [due date]` | anyone | Add a new bounty to the group | | `/add <text> [link] [due date]` | anyone | Add a new bounty to the group |
| `/update <bounty_id> [text] [link] [due_date]` | creator only | Update an existing bounty | | `/edit <bounty_id> [text] [link] [due_date]` | creator only | Edit an existing bounty |
| `/delete <bounty_id>` | creator only | Delete a bounty | | `/delete <bounty_id>` | creator only | Delete a bounty |
| `/track <bounty_id>` | anyone | Track a group bounty | | `/track <bounty_id>` | anyone | Track a group bounty |
| `/untrack <bounty_id>` | anyone | Stop tracking a bounty | | `/untrack <bounty_id>` | anyone | Stop tracking a bounty |
@@ -114,7 +135,7 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
| `/bounty` | List all your personal bounties | | `/bounty` | List all your personal bounties |
| `/my` | List all your personal bounties | | `/my` | List all your personal bounties |
| `/add <text> [link] [due date]` | Add a personal bounty | | `/add <text> [link] [due date]` | Add a personal bounty |
| `/update <bounty_id> [text] [link] [due_date]` | Update a personal bounty | | `/edit <bounty_id> [text] [link] [due_date]` | Edit a personal bounty |
| `/delete <bounty_id>` | Delete a personal bounty | | `/delete <bounty_id>` | Delete a personal bounty |
| `/track <bounty_id>` | Track a personal bounty | | `/track <bounty_id>` | Track a personal bounty |
@@ -149,7 +170,7 @@ Stored as Unix timestamp. User-facing display can be localized/converted to any
## Error Handling ## Error Handling
- Unknown command → help text with available commands - Unknown command → help text with available commands
- `/update`/`/delete` by non-creator → "⛔ Only the creator can edit/delete this bounty." - `/edit`/`/delete` by non-creator → "⛔ Only the creator can edit/delete this bounty."
- `/track` already tracked → "Already tracking" (idempotent) - `/track` already tracked → "Already tracking" (idempotent)
- `/untrack` not tracked → "Not tracking" (idempotent) - `/untrack` not tracked → "Not tracking" (idempotent)
- Bounty not found → "Bounty not found" - Bounty not found → "Bounty not found"

View File

@@ -1,4 +0,0 @@
python-telegram-bot==21.6
dateparser==1.2.0
pytest==8.3.5
pytest-asyncio==0.25.2

View File

@@ -1,216 +0,0 @@
"""Per-group JSON file storage for JIGAIDO."""
import json
import os
import tempfile
from pathlib import Path
from typing import Optional
DATA_DIR = Path.home() / ".jigaido"
def _ensure_dirs() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
def _group_dir(group_id: int) -> Path:
return DATA_DIR / str(group_id)
def _user_personal_dir(user_id: int) -> Path:
return DATA_DIR / str(user_id)
def _group_file(group_id: int) -> Path:
return _group_dir(group_id) / "group.json"
def _user_tracking_file(group_id: int, user_id: int) -> Path:
return _group_dir(group_id) / f"{user_id}.json"
def _user_personal_file(user_id: int) -> Path:
return _user_personal_dir(user_id) / "user.json"
def _atomic_write(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as tmp:
json.dump(data, tmp, indent=2)
tmp_path = tmp.name
os.rename(tmp_path, path)
def load_group_bounties(group_id: int) -> dict:
_ensure_dirs()
path = _group_file(group_id)
if not path.exists():
return {"bounties": [], "next_id": 1}
with open(path) as f:
return json.load(f)
def save_group_bounties(group_id: int, data: dict) -> None:
_atomic_write(_group_file(group_id), data)
def add_group_bounty(
group_id: int,
created_by_user_id: int,
text: Optional[str],
link: Optional[str],
due_date_ts: Optional[int],
) -> dict:
data = load_group_bounties(group_id)
bounty = {
"id": data["next_id"],
"created_by_user_id": created_by_user_id,
"text": text,
"link": link,
"due_date_ts": due_date_ts,
"created_at": int(os.path.getctime(_group_file(group_id)))
if _group_file(group_id).exists()
else 0,
}
data["bounties"].append(bounty)
data["next_id"] += 1
save_group_bounties(group_id, data)
return bounty
def update_group_bounty(
group_id: int,
bounty_id: int,
text: Optional[str],
link: Optional[str],
due_date_ts: Optional[int],
) -> bool:
data = load_group_bounties(group_id)
for bounty in data["bounties"]:
if bounty["id"] == bounty_id:
if text is not None:
bounty["text"] = text
if link is not None:
bounty["link"] = link
if due_date_ts is not None:
bounty["due_date_ts"] = due_date_ts
save_group_bounties(group_id, data)
return True
return False
def delete_group_bounty(group_id: int, bounty_id: int) -> bool:
data = load_group_bounties(group_id)
for i, bounty in enumerate(data["bounties"]):
if bounty["id"] == bounty_id:
data["bounties"].pop(i)
save_group_bounties(group_id, data)
return True
return False
def get_group_bounty(group_id: int, bounty_id: int) -> Optional[dict]:
data = load_group_bounties(group_id)
for bounty in data["bounties"]:
if bounty["id"] == bounty_id:
return bounty
return None
def load_user_tracking(group_id: int, user_id: int) -> dict:
path = _user_tracking_file(group_id, user_id)
if not path.exists():
return {"tracked": []}
with open(path) as f:
return json.load(f)
def save_user_tracking(group_id: int, user_id: int, data: dict) -> None:
_atomic_write(_user_tracking_file(group_id, user_id), data)
def track_bounty(group_id: int, user_id: int, bounty_id: int) -> bool:
data = load_user_tracking(group_id, user_id)
if any(t["bounty_id"] == bounty_id for t in data["tracked"]):
return False
data["tracked"].append({"bounty_id": bounty_id})
save_user_tracking(group_id, user_id, data)
return True
def untrack_bounty(group_id: int, user_id: int, bounty_id: int) -> bool:
data = load_user_tracking(group_id, user_id)
for i, t in enumerate(data["tracked"]):
if t["bounty_id"] == bounty_id:
data["tracked"].pop(i)
save_user_tracking(group_id, user_id, data)
return True
return False
def load_user_personal(user_id: int) -> dict:
path = _user_personal_file(user_id)
if not path.exists():
return {"bounties": [], "next_id": 1}
with open(path) as f:
return json.load(f)
def save_user_personal(user_id: int, data: dict) -> None:
_atomic_write(_user_personal_file(user_id), data)
def add_personal_bounty(
user_id: int, text: Optional[str], link: Optional[str], due_date_ts: Optional[int]
) -> dict:
data = load_user_personal(user_id)
bounty = {
"id": data["next_id"],
"text": text,
"link": link,
"due_date_ts": due_date_ts,
"created_at": 0,
}
data["bounties"].append(bounty)
data["next_id"] += 1
save_user_personal(user_id, data)
return bounty
def update_personal_bounty(
user_id: int,
bounty_id: int,
text: Optional[str],
link: Optional[str],
due_date_ts: Optional[int],
) -> bool:
data = load_user_personal(user_id)
for bounty in data["bounties"]:
if bounty["id"] == bounty_id:
if text is not None:
bounty["text"] = text
if link is not None:
bounty["link"] = link
if due_date_ts is not None:
bounty["due_date_ts"] = due_date_ts
save_user_personal(user_id, data)
return True
return False
def delete_personal_bounty(user_id: int, bounty_id: int) -> bool:
data = load_user_personal(user_id)
for i, bounty in enumerate(data["bounties"]):
if bounty["id"] == bounty_id:
data["bounties"].pop(i)
save_user_personal(user_id, data)
return True
return False
def get_personal_bounty(user_id: int, bounty_id: int) -> Optional[dict]:
data = load_user_personal(user_id)
for bounty in data["bounties"]:
if bounty["id"] == bounty_id:
return bounty
return None