scaffold tasks extension

Empty skeleton mirroring the events extension layout: config/manifest,
pyproject + Makefile + ruff/mypy config, FastAPI routers wired into
tasks_ext, NostrClient bootstrap stubs, and an empty static/routes.json.

Models, migrations, CRUD, Nostr publisher/sync, and the API/template
layers land in follow-up commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-13 11:34:04 +02:00
commit bc88b421b6
16 changed files with 354 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
__pycache__/
*.pyc
*.pyo
.mypy_cache/
.pytest_cache/
.ruff_cache/
.venv/
node_modules/
data/
*.egg-info/
.DS_Store

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 aiolabs
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.

32
Makefile Normal file
View file

@ -0,0 +1,32 @@
all: format check
format: black ruff
check: mypy checkblack checkruff
mypy:
uv run mypy .
black:
uv run black .
ruff:
uv run ruff check . --fix
checkruff:
uv run ruff check .
checkblack:
uv run black --check .
test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
uv run pytest
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
uv run pre-commit install
pre-commit:
uv run pre-commit run --all-files

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# Tasks — LNbits extension
LNbits-side counterpart to the webapp `tasks` module. Stores and syncs
recurring tasks / chores as Nostr NIP-52 calendar events.
## Data model
- **Kind 31922** — task (parameterized replaceable). Tagged with
`event-type: task` so activities consumers can filter it out
(see aiolabs/webapp#25 for context).
- **Kind 31925** — task completion / RSVP-ish status update. Carries
`task-status` (`claimed`, `in-progress`, `completed`, `blocked`,
`cancelled`), an optional `occurrence` date for recurring tasks, and
`completed_at` when finalized.
- **Kind 5** — NIP-09 deletions for the above.
Recurrence is encoded as task-only tags: `recurrence` (`daily` |
`weekly`), `recurrence-day` (weekday name for weekly), `recurrence-end`
(YYYY-MM-DD).
## What this extension does
- Caches tasks and completions in a local sqlite/postgres schema.
- Publishes mutations to Nostr via the `nostrclient` extension's
internal WebSocket bridge.
- Subscribes to inbound 31922/31925 events filtered to `event-type=task`
so the local DB stays in sync with the relay set.
## Status
Early scaffold. Mirrors `events`' extension layout. See
`~/dev/webapp/src/modules/tasks/` for the canonical client-side model.

77
__init__.py Normal file
View file

@ -0,0 +1,77 @@
import asyncio
from fastapi import APIRouter
from loguru import logger
from .crud import db
from .views import tasks_generic_router
from .views_api import tasks_api_router
tasks_ext: APIRouter = APIRouter(prefix="/tasks", tags=["Tasks"])
tasks_ext.include_router(tasks_generic_router)
tasks_ext.include_router(tasks_api_router)
tasks_static_files = [
{
"path": "/tasks/static",
"name": "tasks_static",
}
]
scheduled_tasks: list[asyncio.Task] = []
# Module-level NostrClient — None when nostrclient is unavailable. Set by the
# bootstrap task in tasks_start() and read via dynamic attribute lookup from
# nostr_hooks.publish_or_delete_task_event.
nostr_client = None
def tasks_stop():
for task in scheduled_tasks:
try:
task.cancel()
except Exception as ex:
logger.warning(ex)
global nostr_client
if nostr_client:
asyncio.get_event_loop().create_task(nostr_client.stop())
def tasks_start():
from lnbits.tasks import create_permanent_unique_task
async def _start_nostr_client():
global nostr_client
await asyncio.sleep(10) # Wait for nostrclient to be ready
try:
from .nostr.nostr_client import NostrClient
nostr_client = NostrClient()
logger.info("[TASKS] Starting NostrClient for NIP-52 sync")
await nostr_client.run_forever()
except Exception as exc:
logger.warning(f"[TASKS] NostrClient failed to start: {exc}")
logger.info("[TASKS] Tasks will work without Nostr sync")
task1 = create_permanent_unique_task("ext_tasks_nostr", _start_nostr_client)
scheduled_tasks.append(task1)
async def _sync_nostr_events():
global nostr_client
await asyncio.sleep(15)
if not nostr_client:
logger.info("[TASKS] No NostrClient, skipping Nostr sync")
return
try:
from .nostr_sync import wait_for_nostr_events
await wait_for_nostr_events(nostr_client)
except Exception as exc:
logger.error(f"[TASKS] Nostr sync task failed: {exc}")
task2 = create_permanent_unique_task("ext_tasks_nostr_sync", _sync_nostr_events)
scheduled_tasks.append(task2)
__all__ = ["db", "tasks_ext", "tasks_start", "tasks_static_files", "tasks_stop"]

25
config.json Normal file
View file

@ -0,0 +1,25 @@
{
"id": "tasks",
"version": "0.1.0",
"name": "Tasks",
"repo": "https://git.atitlan.io/aiolabs/tasks",
"short_description": "Recurring tasks and chore-tracking, published over Nostr",
"description": "",
"tile": "/tasks/static/image/tasks.png",
"min_lnbits_version": "1.4.1",
"contributors": [
{
"name": "padreug",
"uri": "https://git.atitlan.io/padreug",
"role": "Developer"
}
],
"images": [],
"description_md": "https://git.atitlan.io/aiolabs/tasks/raw/branch/main/description.md",
"terms_and_conditions_md": "https://git.atitlan.io/aiolabs/tasks/raw/branch/main/toc.md",
"license": "MIT",
"paid_features": "",
"tags": ["Productivity"],
"donate": "",
"hidden": false
}

5
crud.py Normal file
View file

@ -0,0 +1,5 @@
from lnbits.db import Database
db = Database("ext_tasks")
# CRUD functions are filled in by the next commit.

9
description.md Normal file
View file

@ -0,0 +1,9 @@
Recurring tasks and chore tracking for LNbits, synced over Nostr.
Features:
- Author tasks with optional recurrence (daily / weekly).
- Multiple participants per task; anyone can claim, start, or complete.
- Federated state via NIP-52 calendar events (kind 31922) and RSVPs
(kind 31925) with a `task-status` extension.
- Per-occurrence completion tracking for recurring tasks.

9
manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"repos": [
{
"id": "tasks",
"organisation": "aiolabs",
"repository": "tasks"
}
]
}

1
migrations.py Normal file
View file

@ -0,0 +1 @@
# Migrations are filled in by the next commit.

1
models.py Normal file
View file

@ -0,0 +1 @@
# Pydantic models are filled in by the next commit.

67
pyproject.toml Normal file
View file

@ -0,0 +1,67 @@
[project]
name = "lnbits-tasks"
version = "0.0.0"
requires-python = ">=3.10,<3.13"
description = "Recurring tasks and chore tracking for LNbits, federated over Nostr (NIP-52)."
authors = [{ name = "aiolabs", email = "dev@aiolabs.dev" }]
urls = { Homepage = "https://git.atitlan.io/aiolabs/tasks", Repository = "https://git.atitlan.io/aiolabs/tasks" }
dependencies = [ "lnbits>1" ]
[dependency-groups]
dev = [
"black",
"pytest-asyncio",
"pytest",
"mypy",
"pre-commit",
"ruff",
]
[tool.poetry]
package-mode = false
[tool.mypy]
exclude = "(nostr/*)"
plugins = ["pydantic.mypy"]
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true
[tool.pytest.ini_options]
log_cli = false
testpaths = [
"tests"
]
[tool.black]
line-length = 88
[tool.ruff]
line-length = 88
exclude = [
"nostr",
]
[tool.ruff.lint]
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
ignore = ["C901"]
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.pep8-naming]
classmethod-decorators = [
"validator",
"root_validator",
]
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = [
"fastapi.Depends",
"fastapi.Query",
]

14
static/routes.json Normal file
View file

@ -0,0 +1,14 @@
[
{
"path": "/tasks/",
"name": "PageTasks",
"template": "/tasks/static/js/index.vue",
"component": "/tasks/static/js/index.js"
},
{
"path": "/tasks/:id",
"name": "PageTaskDisplay",
"template": "/tasks/static/js/display.vue",
"component": "/tasks/static/js/display.js"
}
]

29
toc.md Normal file
View file

@ -0,0 +1,29 @@
# Terms and Conditions for LNbits Extension
## 1. Acceptance of Terms
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
## 2. License
The Extension is free and open-source software, released under the MIT license. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
## 3. No Warranty
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
## 4. Limitation of Liability
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
## 5. Modification of Terms
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
## 6. General Provisions
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
## 7. Contact Information
If you have any questions about these Terms, please contact the developer at dev@aiolabs.dev.

16
views.py Normal file
View file

@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends
from lnbits.core.views.generic import index, index_public
from lnbits.decorators import check_account_id_exists
tasks_generic_router = APIRouter()
tasks_generic_router.add_api_route(
"/",
methods=["GET"],
endpoint=index,
dependencies=[Depends(check_account_id_exists)],
)
tasks_generic_router.add_api_route(
"/{task_id}", methods=["GET"], endpoint=index_public
)

5
views_api.py Normal file
View file

@ -0,0 +1,5 @@
from fastapi import APIRouter
tasks_api_router = APIRouter(prefix="/api/v1/tasks")
# API routes are filled in by the next commit.