Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: compat with nostr calendar #17

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
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
22 changes: 21 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import asyncio

from fastapi import APIRouter
from loguru import logger

from .crud import db
from .tasks import wait_for_paid_invoices
from .views import lncalendar_generic_router
from .views_api import lncalendar_api_router

Expand All @@ -15,4 +19,20 @@
}
]

__all__ = ["db", "lncalendar_ext", "lncalendar_static_files"]
scheduled_tasks: list[asyncio.Task] = []

def lncalendar_stop():
for task in scheduled_tasks:
try:
task.cancel()
except Exception as ex:
logger.warning(ex)


def lncalendar_start():
from lnbits.tasks import create_permanent_unique_task

task = create_permanent_unique_task("ext_lncalendar", wait_for_paid_invoices)
scheduled_tasks.append(task)

__all__ = ["db", "lncalendar_ext", "lncalendar_static_files", "lncalendar_start", "lncalendar_stop"]
43 changes: 41 additions & 2 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,41 @@

from .models import (
Appointment,
CalendarSettings,
CreateAppointment,
CreateSchedule,
CreateUnavailableTime,
Schedule,
UnavailableTime,
)
from .nostr.key import PrivateKey

db = Database("ext_lncalendar")

async def get_or_create_calendar_settings() -> CalendarSettings:
settings = await db.fetchone(
"SELECT * FROM lncalendar.settings LIMIT 1",
model=CalendarSettings, # type: ignore
)
if settings:
return settings
else:
settings = CalendarSettings(
nostr_private_key=PrivateKey().hex(),
)
await db.insert("lncalendar.settings", settings) # type: ignore
return settings

async def update_calendar_settings(settings: CalendarSettings) -> CalendarSettings:
await db.update("lncalendar.settings", settings, "")
return settings

async def delete_calendar_settings() -> None:
await db.execute("DELETE FROM lncalendar.settings")

async def create_schedule(wallet_id: str, data: CreateSchedule) -> Schedule:
schedule_id = urlsafe_short_hash()

schedule = Schedule(
id=schedule_id,
wallet=wallet_id,
Expand All @@ -27,6 +50,9 @@ async def create_schedule(wallet_id: str, data: CreateSchedule) -> Schedule:
start_time=data.start_time,
end_time=data.end_time,
amount=data.amount,
timeslot=data.timeslot,
currency=data.currency,
public_key=data.public_key,
)
await db.insert("lncalendar.schedule", schedule)
return schedule
Expand Down Expand Up @@ -68,15 +94,20 @@ async def create_appointment(
id=appointment_id,
name=data.name,
email=data.email,
nostr_pubkey=data.nostr_pubkey,
info=data.info,
start_time=data.start_time,
end_time=data.end_time,
schedule=schedule_id,
paid=False,
created_at=datetime.now(),
)
await db.insert("lncalendar.appointment", appointment)
return appointment

async def update_appointment(appointment: Appointment) -> Appointment:
await db.update("lncalendar.appointment", appointment, "")
return appointment

async def get_appointment(appointment_id: str) -> Optional[Appointment]:
return await db.fetchone(
Expand All @@ -95,7 +126,7 @@ async def get_appointments(schedule_id: str) -> list[Appointment]:


async def get_appointments_wallets(
wallet_ids: Union[str, list[str]]
wallet_ids: Union[str, list[str]],
) -> list[Appointment]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
Expand Down Expand Up @@ -126,20 +157,28 @@ async def purge_appointments(schedule_id: str) -> None:
await db.execute(
f"""
DELETE FROM lncalendar.appointment
WHERE schedule = :schedule AND paid = false AND time < {tsph}
WHERE schedule = :schedule AND paid = false AND created_at < {tsph}
""",
{"schedule": schedule_id, "diff": time_diff.timestamp()},
)


async def delete_appointment(appointment_id: str) -> None:
await db.execute(
"DELETE FROM lncalendar.appointment WHERE id = :id", {"id": appointment_id}
)


## UnavailableTime CRUD
async def create_unavailable_time(data: CreateUnavailableTime) -> UnavailableTime:
unavailable_time_id = urlsafe_short_hash()
unavailable_time = UnavailableTime(
id=unavailable_time_id,
name=data.name or "",
start_time=data.start_time,
end_time=data.end_time or data.start_time,
schedule=data.schedule,
created_at=datetime.now(),
)
await db.insert("lncalendar.unavailable", unavailable_time)
return unavailable_time
Expand Down
44 changes: 44 additions & 0 deletions helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import requests
from bech32 import bech32_decode, convertbits

from .nostr.key import PrivateKey


def parse_nostr_private_key(key: str) -> PrivateKey:
if key.startswith("nsec"):
return PrivateKey.from_nsec(key)
else:
return PrivateKey(bytes.fromhex(key))

def normalize_public_key(pubkey: str) -> str:
if pubkey.startswith("npub1"):
_, decoded_data = bech32_decode(pubkey)
if not decoded_data:
raise ValueError("Public Key is not valid npub")

decoded_data_bits = convertbits(decoded_data, 5, 8, False)
if not decoded_data_bits:
raise ValueError("Public Key is not valid npub")
return bytes(decoded_data_bits).hex()

# allow for nip05 identifier as well, ex: [email protected]
if "@" in pubkey:
local_part, domain = pubkey.split("@")

request_url = f"https://{domain}/.well-known/nostr.json?name={local_part}"
response = requests.get(request_url)
if response.status_code != 200:
raise ValueError("Public Key is not valid npub")

response_json = response.json()
if not response_json.get("names"):
raise ValueError("Public Key not found")
pubkey = response_json["names"].get(local_part)
if not pubkey:
raise ValueError("Public Key not found")

# check if valid hex
if len(pubkey) != 64:
raise ValueError("Public Key is not valid hex")
int(pubkey, 16)
return pubkey
112 changes: 112 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from sqlalchemy.exc import OperationalError


async def m001_initial(db):
"""
Initial schedules table.
Expand Down Expand Up @@ -51,3 +54,112 @@ async def m001_initial(db):
);
"""
)


async def m002_rename_time_to_created_at(db):
"""
Rename time to created_at in the unavailability table.
"""
await db.execute(
"""
ALTER TABLE lncalendar.unavailable
RENAME COLUMN time TO created_at;
"""
)
await db.execute(
"""
ALTER TABLE lncalendar.appointment
RENAME COLUMN time TO created_at;
"""
)

"""
Add name to the unavailable table.
"""
await db.execute(
"""
ALTER TABLE lncalendar.unavailable
ADD COLUMN name TEXT NOT NULL DEFAULT 'Unavailable';
"""
)

"""
Add timeslot to the schedule table.
"""
await db.execute(
"""
ALTER TABLE lncalendar.schedule
ADD COLUMN timeslot INTEGER NOT NULL DEFAULT 30;
"""
)

"""
Add nostr_pubkey to the appointment table.
"""
await db.execute(
"""
ALTER TABLE lncalendar.appointment
ADD COLUMN nostr_pubkey TEXT;
"""
)


async def m003_add_fiat_currency(db):
"""
Add currency to schedule to allow fiat denomination
of appointments. Make price a float.
"""
try:
await db.execute(
"ALTER TABLE lncalendar.schedule RENAME TO schedule_backup;")
await db.execute(
"""
CREATE TABLE lncalendar.schedule (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
start_day INTEGER NOT NULL,
end_day INTEGER NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
amount FLOAT NOT NULL,
timeslot INTEGER NOT NULL DEFAULT 30,
currency TEXT NOT NULL DEFAULT 'sat'
);
"""
)

await db.execute(
"""
INSERT INTO lncalendar.schedule (id, wallet, name, start_day, end_day, start_time, end_time, amount, timeslot)
SELECT id, wallet, name, start_day, end_day, start_time, end_time, amount, timeslot FROM lncalendar.schedule_backup;
"""
)

await db.execute("DROP TABLE lncalendar.schedule_backup;")
except OperationalError:
pass

async def m004_add_lncalendar_settings(db):
"""
Add settings table to store global settings.
"""
await db.execute(
"""
CREATE TABLE lncalendar.settings (
nostr_private_key TEXT NOT NULL,
relays TEXT
);
"""
)

async def m005_add_public_key(db):
"""
Add public_key to schedule table.
"""
await db.execute(
"""
ALTER TABLE lncalendar.schedule
ADD COLUMN public_key TEXT;
"""
)
Loading
Loading