Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__pycache__
data
node_modules
.venv
.mypy_cache
20 changes: 10 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,43 @@ format: prettier black ruff
check: mypy pyright checkblack checkruff checkprettier

prettier:
uv run ./node_modules/.bin/prettier --write .
uv --project . --no-config run ./node_modules/.bin/prettier --write .
pyright:
uv run ./node_modules/.bin/pyright
uv --project . --no-config run ./node_modules/.bin/pyright

mypy:
uv run mypy .
uv --project . --no-config run mypy .

black:
uv run black .
uv --project . --no-config run black .

ruff:
uv run ruff check . --fix
uv --project . --no-config run ruff check . --fix

checkruff:
uv run ruff check .
uv --project . --no-config run ruff check .

checkprettier:
uv run ./node_modules/.bin/prettier --check .
uv --project . --no-config run ./node_modules/.bin/prettier --check .

checkblack:
uv run black --check .
uv --project . --no-config run black --check .

checkeditorconfig:
editorconfig-checker

test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
uv run pytest
uv --project . --no-config run pytest

install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with uv run pre-commit uninstall"
uv run pre-commit install

pre-commit:
uv run pre-commit run --all-files
uv --project . --no-config run pre-commit run --all-files


checkbundle:
Expand Down
7 changes: 6 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from loguru import logger

from .crud import db
from .tasks import cleanup_empty_chats, wait_for_paid_invoices
from .tasks import cleanup_empty_chats, dispatch_notification_jobs, wait_for_paid_invoices
from .views import chat_generic_router, chat_public_router
from .views_api import chat_api_router
from .views_lnurl import chat_lnurl_router
Expand Down Expand Up @@ -44,6 +44,11 @@ def chat_start():
scheduled_tasks.append(task)
cleanup_task = create_permanent_unique_task("ext_chat_cleanup", cleanup_empty_chats)
scheduled_tasks.append(cleanup_task)
notification_task = create_permanent_unique_task(
"ext_chat_notifications",
dispatch_notification_jobs,
)
scheduled_tasks.append(notification_task)


__all__ = [
Expand Down
38 changes: 38 additions & 0 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .models import (
Categories,
CategoriesFilters,
ChatNotificationJob,
ChatPayment,
ChatSession,
ChatsFilters,
Expand Down Expand Up @@ -211,3 +212,40 @@ async def delete_empty_chats_before(cutoff: int) -> None:
AND created_at < cast(:cutoff AS timestamp)
"""
await db.execute(query, {"cutoff": cutoff})


################################# Notification Jobs ###########################


async def create_notification_job(job: ChatNotificationJob) -> ChatNotificationJob:
await db.insert("chat.notification_jobs", job)
return job


async def get_notification_job(job_id: str) -> ChatNotificationJob | None:
return await db.fetchone(
"SELECT * FROM chat.notification_jobs WHERE id = :id",
{"id": job_id},
ChatNotificationJob,
)


async def update_notification_job(job: ChatNotificationJob) -> ChatNotificationJob:
await db.update("chat.notification_jobs", job)
return job


async def get_due_notification_jobs(
due_at,
limit: int = 25,
) -> list[ChatNotificationJob]:
return await db.fetchall(
"""
SELECT * FROM chat.notification_jobs
WHERE status = 'pending' AND due_at <= :due_at
ORDER BY due_at ASC
LIMIT :limit
""",
{"due_at": due_at, "limit": limit},
model=ChatNotificationJob,
)
77 changes: 77 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,80 @@ async def m011_chat_guest_notifications(db):
ALTER TABLE chat.chats ADD COLUMN notify_nostr TEXT;
"""
)


async def m012_chat_schedules_seen_and_notification_jobs(db):
"""
Add schedule/template fields, public seen fields, and notification jobs table.
"""

await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN schedule_enabled BOOLEAN DEFAULT FALSE;
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN schedule_timezone TEXT DEFAULT 'Europe/London';
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN schedule_days TEXT DEFAULT '0,1,2,3,4';
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN schedule_start TEXT DEFAULT '09:00';
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN schedule_end TEXT DEFAULT '17:00';
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN admin_after_hours_subject TEXT;
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN admin_after_hours_body TEXT;
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN user_new_message_subject TEXT;
"""
)
await db.execute(
"""
ALTER TABLE chat.categories ADD COLUMN user_new_message_body TEXT;
"""
)
await db.execute(
"""
ALTER TABLE chat.chats ADD COLUMN public_last_seen_message_id TEXT;
"""
)
await db.execute(
"""
ALTER TABLE chat.chats ADD COLUMN public_last_seen_at TIMESTAMP;
"""
)
await db.execute(
f"""
CREATE TABLE chat.notification_jobs (
id TEXT PRIMARY KEY,
chat_id TEXT NOT NULL,
categories_id TEXT NOT NULL,
job_type TEXT NOT NULL,
message_id TEXT NOT NULL,
due_at TIMESTAMP NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
41 changes: 40 additions & 1 deletion models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from lnbits.db import FilterModel
from pydantic import BaseModel, Field

DEFAULT_PUBLIC_NOTE = "we aim to reply instantly but it may take up to 24hrs for a reply"
DEFAULT_PUBLIC_NOTE = "we aim to reply as soon as possible but it may take up to 24hrs for a reply"


class CreateCategories(BaseModel):
Expand All @@ -21,6 +21,15 @@ class CreateCategories(BaseModel):
notify_telegram: str | None = None
notify_nostr: str | None = None
notify_email: str | None = None
schedule_enabled: bool | None = False
schedule_timezone: str | None = "Europe/London"
schedule_days: str | list[int] | None = "0,1,2,3,4"
schedule_start: str | None = "09:00"
schedule_end: str | None = "17:00"
admin_after_hours_subject: str | None = None
admin_after_hours_body: str | None = None
user_new_message_subject: str | None = None
user_new_message_body: str | None = None


class Categories(BaseModel):
Expand All @@ -40,6 +49,15 @@ class Categories(BaseModel):
notify_telegram: str | None = None
notify_nostr: str | None = None
notify_email: str | None = None
schedule_enabled: bool | None = False
schedule_timezone: str | None = "Europe/London"
schedule_days: str | None = "0,1,2,3,4"
schedule_start: str | None = "09:00"
schedule_end: str | None = "17:00"
admin_after_hours_subject: str | None = None
admin_after_hours_body: str | None = None
user_new_message_subject: str | None = None
user_new_message_body: str | None = None

created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
Expand All @@ -59,6 +77,12 @@ class PublicCategories(BaseModel):
public_note: str | None = DEFAULT_PUBLIC_NOTE
notify_email_available: bool | None = False
notify_nostr_available: bool | None = False
schedule_enabled: bool | None = False
schedule_available: bool | None = True
schedule_timezone: str | None = None
schedule_start: str | None = None
schedule_end: str | None = None
schedule_days: list[int] = Field(default_factory=list)


class CategoriesFilters(FilterModel):
Expand Down Expand Up @@ -123,6 +147,8 @@ class ChatSession(BaseModel):
claimed_by_name: str | None = None
notify_email: str | None = None
notify_nostr: str | None = None
public_last_seen_message_id: str | None = None
public_last_seen_at: datetime | None = None
participants: list[dict] = Field(default_factory=list)
messages: list[dict] = Field(default_factory=list)
last_message_at: datetime | None = None
Expand All @@ -141,6 +167,7 @@ class CreateChatMessage(BaseModel):
sender_name: str
sender_role: str
message: str
notify_email: str | None = None


class ChatNotifications(BaseModel):
Expand Down Expand Up @@ -171,6 +198,18 @@ class ChatPayment(BaseModel):
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class ChatNotificationJob(BaseModel):
id: str
chat_id: str
categories_id: str
job_type: str
message_id: str
due_at: datetime
status: str = "pending"
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class TipRequest(BaseModel):
amount: int
sender_id: str
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ dependencies = [ "lnbits>=1.4.0" ]
[tool.poetry]
package-mode = false

[tool.uv]
dev-dependencies = [
[dependency-groups]
dev = [
"black>=24.3.0",
"pytest-asyncio>=0.21.0",
"pytest>=7.3.2",
Expand All @@ -23,6 +23,8 @@ dev-dependencies = [

[tool.mypy]
plugins = ["pydantic.mypy"]
ignore_missing_imports = true
follow_imports = "silent"

[tool.pydantic-mypy]
init_forbid_extra = true
Expand Down
Loading
Loading