Merge pull request 'fix #16: cleanup - remove old/dead code and update docs' (#35) from fix/issue-16-cleanup into main

This commit was merged in pull request #35.
This commit is contained in:
2026-04-03 21:09:59 +02:00
6 changed files with 87 additions and 252 deletions

View File

@@ -5,27 +5,42 @@
```bash
git clone https://git.fbrns.co/shoko/jigaido.git
cd jigaido
# Create virtual environment
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Install dependencies
pip install -r apps/telegram-bot/requirements.txt
# Run tests
pytest
# Run bot
export JIGAIDO_BOT_TOKEN="test:token"
python bot.py
export JIGAIDO_BOT_TOKEN="your_bot_token"
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
- Python (no strict formatter enforced yet)
- Python 3.10+ with type hints
- Async/await for Telegram handlers
- Type hints where obvious
- Docstrings for public functions
- Follow existing code patterns
## Pull Request Workflow
1. Branch from `main`
2. Make changes
3. Test locally
3. Test locally with `pytest`
4. Open PR with description of what changed and why
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/
├── 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/
│ └── telegram-bot/ ← first app (Python)
│ ├── bot.py
── commands.py
│ ├── cron.py
│ ├── db.py
│ └── requirements.txt
└── SPEC.md ← full design specification
│ └── telegram-bot/ # Telegram bot CLI application
│ ├── bot.py # Bot entry point
── commands.py # Command handlers
├── tests/ # Unit tests
├── config.py # Configuration management
└── 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

59
SPEC.md
View File

@@ -20,7 +20,7 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
### Stack
- **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`
- **Runtime**: 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
```
~/.jigaido/ # Data root (~/.jigaido/)
├── {group_id}/
│ ├── group.json # Group bounties
── {user_id}.json # User tracking within this group
└── {user_id}/
└── user.json # User's personal bounties (DM mode)
jigaido/
├── core/ # Domain layer
│ ├── models.py # Domain dataclasses (Bounty, Tracking)
── ports.py # Port interfaces (abstract base classes)
│ └── services.py # Domain services (BountyService, TrackingService)
├── 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
{
@@ -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
{
@@ -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
{
@@ -87,10 +108,10 @@ JIGAIDO is a Telegram bot that lets groups and individuals track bounties — ta
**Key design decisions:**
1. **Group-isolated storage** — Each group has its own directory. No cross-group access.
2. **Bounty IDs are sequential per group.json** — Not global. Each group's file has its own ID counter.
3. **Atomic writes** — Uses `tempfile` + `rename` for safe writes.
4. **No reminders in v1** — Dropped for simplicity.
1. **Hexagonal architecture** — Core domain is isolated from infrastructure
2. **Group-isolated storage** Each group has its own directory. No cross-group access.
3. **Bounty IDs are sequential per group.json** — Not global. Each group's file has its own ID counter.
4. **Atomic writes** — Uses `tempfile` + `rename` for safe writes.
---
## 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 |
| `/my` | anyone | List bounties tracked by you in this 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 |
| `/track <bounty_id>` | anyone | Track a group 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 |
| `/my` | List all your personal bounties |
| `/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 |
| `/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
- 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)
- `/untrack` not tracked → "Not tracking" (idempotent)
- 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