diff --git a/config.json b/config.json index 57a7f75..9a73073 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.1-aio.1", + "version": "1.3.0-aio.6", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets", @@ -36,7 +36,7 @@ { "name": "padreug", "uri": "https://git.atitlan.io/padreug", - "role": "Developer (aio fork: approval workflow + NIP-52 Nostr sync + edit gating)" + "role": "Developer (aio fork: approval workflow + NIP-52 Nostr sync)" } ], "images": [ diff --git a/crud.py b/crud.py index 004fa7f..0a51727 100644 --- a/crud.py +++ b/crud.py @@ -94,7 +94,7 @@ async def update_ticket(ticket: Ticket) -> Ticket: async def get_ticket(payment_hash: str) -> Ticket | None: - row = await db.fetchone( + row: dict | None = await db.fetchone( "SELECT * FROM events.ticket WHERE id = :id", {"id": payment_hash}, ) @@ -107,13 +107,15 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: if isinstance(wallet_ids, str): wallet_ids = [wallet_ids] q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) - rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})") + rows: list[dict] = await db.fetchall( + f"SELECT * FROM events.ticket WHERE wallet IN ({q})" + ) return [Ticket(**_parse_ticket_row(row)) for row in rows] async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: """All tickets owned by the given LNbits user_id.""" - rows = await db.fetchall( + rows: list[dict] = await db.fetchall( "SELECT * FROM events.ticket WHERE user_id = :user_id ORDER BY time DESC", {"user_id": user_id}, ) @@ -206,7 +208,7 @@ async def get_pending_events() -> list[Event]: async def get_settings() -> EventsSettings: """Singleton settings row, seeded by m010.""" - row = await db.fetchone("SELECT * FROM events.settings WHERE id = 1") + row: dict | None = await db.fetchone("SELECT * FROM events.settings WHERE id = 1") if row: return EventsSettings(**dict(row)) return EventsSettings() @@ -225,7 +227,7 @@ async def delete_event(event_id: str) -> None: async def get_event_tickets(event_id: str) -> list[Ticket]: - rows = await db.fetchall( + rows: list[dict] = await db.fetchall( "SELECT * FROM events.ticket WHERE event = :event", {"event": event_id}, ) diff --git a/migrations.py b/migrations.py index 512540d..6f8e838 100644 --- a/migrations.py +++ b/migrations.py @@ -175,23 +175,3 @@ async def m006_add_extra_fields(db): # Add 'extra' column to ticket table await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;") - - -async def m007_add_allow_fiat(db): - """ - Add an allow_fiat column so event owners can explicitly enable fiat checkout. - """ - await db.execute(""" - ALTER TABLE events.events - ADD COLUMN allow_fiat BOOLEAN NOT NULL DEFAULT FALSE; - """) - - -async def m008_add_fiat_currency(db): - """ - Add a fiat_currency column for sat-denominated events using fiat checkout. - """ - await db.execute(""" - ALTER TABLE events.events - ADD COLUMN fiat_currency TEXT NOT NULL DEFAULT 'GBP'; - """) diff --git a/models.py b/models.py index d3f43d3..a617a13 100644 --- a/models.py +++ b/models.py @@ -24,10 +24,6 @@ class EventExtra(BaseModel): promo_codes: list[PromoCode] = Field(default_factory=list) conditional: bool = False min_tickets: int = 1 - email_notifications: bool = False - nostr_notifications: bool = False - notification_subject: str = "" - notification_body: str = "" class CreateEvent(BaseModel): @@ -40,8 +36,6 @@ class CreateEvent(BaseModel): event_start_date: str event_end_date: str | None = None # same format as event_start_date currency: str = "sat" - allow_fiat: bool = False - fiat_currency: str = "GBP" amount_tickets: int = 0 # 0 = unlimited / not ticketed price_per_ticket: float = 0 # 0 = free banner: str | None = None @@ -61,8 +55,6 @@ class Event(BaseModel): event_start_date: str event_end_date: str | None = None currency: str = "sat" - allow_fiat: bool = False - fiat_currency: str = "GBP" amount_tickets: int = 0 price_per_ticket: float = 0 time: datetime @@ -90,14 +82,9 @@ class PublicEvent(BaseModel): canceled: bool event_start_date: str event_end_date: str | None = None - currency: str - allow_fiat: bool = False - fiat_currency: str = "GBP" - price_per_ticket: float banner: str | None location: str | None = None categories: list[str] = Field(default_factory=list) - extra: EventExtra = Field(default_factory=EventExtra) status: str = "approved" # surfaces "proposed"/"rejected" so SFC can render banner @validator("categories", pre=True) @@ -117,10 +104,6 @@ class TicketExtra(BaseModel): applied_promo_code: str | None = None sats_paid: int | None = None refund_address: str | None = None - nostr_identifier: str | None = None - ticket_base_url: str | None = None - email_notification_sent: bool = False - nostr_notification_sent: bool = False refunded: bool = False @@ -130,9 +113,6 @@ class CreateTicket(BaseModel): user_id: str | None = None # LNbits user id (alternative to name+email) promo_code: str | None = None refund_address: str | None = None - nostr_identifier: str | None = None - payment_method: str | None = None - fiat_provider: str | None = None @root_validator def validate_identifiers(cls, values): @@ -171,7 +151,4 @@ class PublicTicket(BaseModel): class TicketPaymentRequest(BaseModel): payment_hash: str - payment_request: str | None = None - fiat_payment_request: str | None = None - fiat_provider: str | None = None - is_fiat: bool = False + payment_request: str diff --git a/nostr_hooks.py b/nostr_hooks.py index 3211b24..daf1fc0 100644 --- a/nostr_hooks.py +++ b/nostr_hooks.py @@ -29,11 +29,15 @@ async def publish_or_delete_nostr_event(event: Event, *, delete: bool = False) - if not wallet_obj: return account = await get_account(wallet_obj.user) - if not account or not account.pubkey or not account.prvkey: + if not account or not account.pubkey or not account.prvkey: # type: ignore[attr-defined] return nostr_event = await publish_event_to_nostr( - nostr_client, event, account.pubkey, account.prvkey, delete=delete + nostr_client, + event, + account.pubkey, + account.prvkey, # type: ignore[attr-defined] + delete=delete, ) if nostr_event and not delete: event.nostr_event_id = nostr_event.id diff --git a/nostr_sync.py b/nostr_sync.py index 1dc52bc..11869cd 100644 --- a/nostr_sync.py +++ b/nostr_sync.py @@ -50,7 +50,7 @@ async def _handle_calendar_event(nostr_client: NostrClient, event_data: dict): return tags = {t[0]: t[1] for t in event_data.get("tags", []) if len(t) >= 2} - tag_lists = {} + tag_lists: dict[str, list[str]] = {} for t in event_data.get("tags", []): if len(t) >= 2: tag_lists.setdefault(t[0], []).append(t[1]) diff --git a/services.py b/services.py index 159bbdc..9099ef0 100644 --- a/services.py +++ b/services.py @@ -1,15 +1,3 @@ -from __future__ import annotations - -from asyncio.tasks import create_task - -from lnbits.core.models.users import UserNotifications -from lnbits.core.services.nostr import send_nostr_dm -from lnbits.core.services.notifications import ( - send_email_notification, - send_user_notification, -) -from lnbits.settings import settings -from lnbits.utils.nostr import normalize_private_key, normalize_public_key from lnurl import execute from loguru import logger @@ -20,13 +8,7 @@ from .crud import ( update_event, update_ticket, ) -from .models import Event, Ticket - -DEFAULT_NOSTR_RELAYS = [ - "wss://relay.damus.io", - "wss://relay.primal.net", - "wss://relay.nostr.band", -] +from .models import Ticket async def set_ticket_paid(ticket: Ticket) -> Ticket: @@ -45,97 +27,6 @@ async def set_ticket_paid(ticket: Ticket) -> Ticket: return ticket -def send_ticket_notification_in_background(ticket: Ticket) -> None: - create_task(_send_ticket_notification(ticket)) - - -async def _send_ticket_notification(ticket: Ticket) -> None: - event = await get_event(ticket.event) - if not event: - logger.warning(f"Event {ticket.event} not found for ticket notification.") - return - - subject, message = _ticket_notification_message(ticket, event) - updated = False - - if ( - event.extra.email_notifications - and settings.lnbits_email_notifications_enabled - and ticket.email - ): - try: - await send_email_notification([ticket.email], message, subject) - ticket.extra.email_notification_sent = True - updated = True - except Exception as exc: - logger.warning(f"Failed to email ticket {ticket.id}: {exc}") - - if ( - event.extra.nostr_notifications - and settings.is_nostr_notifications_configured() - and ticket.extra.nostr_identifier - ): - try: - await _send_nostr_ticket_notification( - ticket.extra.nostr_identifier, message - ) - ticket.extra.nostr_notification_sent = True - updated = True - except Exception as exc: - logger.warning(f"Failed to send nostr DM for ticket {ticket.id}: {exc}") - - if updated: - await update_ticket(ticket) - - -async def resend_ticket_email_notification(ticket: Ticket) -> Ticket: - event = await get_event(ticket.event) - if not event: - raise ValueError("Event does not exist.") - if not settings.lnbits_email_notifications_enabled: - raise ValueError("Email notifications are not enabled.") - if not ticket.email: - raise ValueError("Ticket does not have an email address.") - - subject, message = _ticket_notification_message(ticket, event) - await send_email_notification([ticket.email], message, subject) - ticket.extra.email_notification_sent = True - return await update_ticket(ticket) - - -def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str]: - ticket_url = _ticket_url(ticket) - subject = ( - event.extra.notification_subject.strip() - or f"Your ticket for '{event.name}' is ready" - ) - body = ( - event.extra.notification_body.strip() - or f"Your ticket for '{event.name}' is ready." - ) - - return subject, f"{body}\n\nOpen it here: {ticket_url}" - - -async def _send_nostr_ticket_notification(identifier: str, message: str) -> None: - if "@" in identifier: - await send_user_notification( - UserNotifications(nostr_identifier=identifier), - message, - "text_message", - ) - return - - private_key = normalize_private_key(settings.lnbits_nostr_notifications_private_key) - public_key = normalize_public_key(identifier) - await send_nostr_dm(private_key, public_key, message, DEFAULT_NOSTR_RELAYS) - - -def _ticket_url(ticket: Ticket) -> str: - base_url = (ticket.extra.ticket_base_url or settings.lnbits_baseurl).rstrip("/") - return f"{base_url}/events/ticket/{ticket.id}" - - async def refund_tickets(event_id: str): """ Refund tickets for an event that has not met the minimum ticket requirement. diff --git a/static/js/display.js b/static/js/display.js index d8be8e9..a652ba5 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -11,9 +11,7 @@ window.PageEventsDisplay = { data: { name: '', email: '', - refund: '', - nostr_identifier: '', - payment_method: 'lightning' + refund: '' } }, ticketLink: { @@ -25,8 +23,7 @@ window.PageEventsDisplay = { receive: { show: false, status: 'pending', - paymentReq: null, - isFiat: false + paymentReq: null }, paymentDismissMsg: null, paymentWebsocket: null @@ -38,25 +35,7 @@ window.PageEventsDisplay = { }, computed: { formatDescription() { - return LNbits.utils.convertMarkdown(this.event?.info || '') - }, - allowFiatCheckout() { - return Boolean(this.event?.allow_fiat) - }, - fiatCheckoutLabel() { - if (!this.allowFiatCheckout) return 'Fiat' - const unit = ['sat', 'sats'].includes( - (this.event?.currency || '').toLowerCase() - ) - ? this.event?.fiat_currency - : this.event?.currency - return `Fiat (${(unit || 'GBP').toUpperCase()})` - }, - allowEmailNotifications() { - return Boolean(this.event?.extra?.email_notifications) - }, - allowNostrNotifications() { - return Boolean(this.event?.extra?.nostr_notifications) + return LNbits.utils.convertMarkdown(this.info) } }, methods: { @@ -77,8 +56,6 @@ window.PageEventsDisplay = { this.formDialog.data.name = '' this.formDialog.data.email = '' this.formDialog.data.refund = '' - this.formDialog.data.nostr_identifier = '' - this.formDialog.data.payment_method = 'lightning' }, closeReceiveDialog() { @@ -110,9 +87,6 @@ window.PageEventsDisplay = { this.paymentReq = null this.formDialog.data.name = '' this.formDialog.data.email = '' - this.formDialog.data.refund = '' - this.formDialog.data.nostr_identifier = '' - this.formDialog.data.payment_method = 'lightning' Quasar.Notify.create({ type: 'positive', message: 'Sent, thank you!', @@ -121,8 +95,7 @@ window.PageEventsDisplay = { this.receive = { show: false, status: 'complete', - paymentReq: null, - isFiat: false + paymentReq: null } this.ticketLink = { show: true, @@ -130,7 +103,9 @@ window.PageEventsDisplay = { link: `/events/ticket/${paymentHash}` } } - window.open(`/events/ticket/${paymentHash}`, '_blank', 'noopener') + setTimeout(() => { + window.location.href = `/events/ticket/${paymentHash}` + }, 5000) }, async createInvoice() { try { @@ -142,15 +117,10 @@ window.PageEventsDisplay = { name: this.formDialog.data.name, email: this.formDialog.data.email, promo_code: this.formDialog.data.promo_code || null, - refund_address: this.formDialog.data.refund || null, - nostr_identifier: this.formDialog.data.nostr_identifier || null, - payment_method: this.formDialog.data.payment_method + refund_address: this.formDialog.data.refund || null } ) - const isFiat = Boolean(data.is_fiat) - this.paymentReq = isFiat - ? data.fiat_payment_request || null - : data.payment_request + this.paymentReq = data.payment_request this.paymentHash = data.payment_hash this.paymentDismissMsg = Quasar.Notify.create({ @@ -160,34 +130,30 @@ window.PageEventsDisplay = { this.receive = { show: true, status: 'pending', - paymentReq: this.paymentReq, - isFiat + paymentReq: this.paymentReq } - if (isFiat && this.paymentReq) { - window.open(this.paymentReq, '_blank', 'noopener') - } - this.paymentWatcher(this.paymentHash) + this.websocketListener(this.paymentHash) } catch (error) { LNbits.utils.notifyApiError(error) } }, - paymentWatcher(paymentHash) { + websocketListener(paymentHash) { if (this.paymentWebsocket) { this.paymentWebsocket.close() } const url = new URL(window.location) - url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' - url.pathname = `/api/v1/ws/${paymentHash}` + url.protocol = url.protocol === 'https:' ? 'wss' : 'ws' + url.pathname = `/events/api/v1/tickets/ws/${paymentHash}` url.search = '' url.hash = '' - const ws = new WebSocket(url.toString()) + const ws = new WebSocket(url) this.paymentWebsocket = ws ws.onmessage = event => { const data = JSON.parse(event.data) - if (data.pending === false) { + if (data.paid) { this.paymentSuccess(paymentHash) ws.close() } diff --git a/static/js/display.vue b/static/js/display.vue index 9b27783..58fec04 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -41,77 +41,41 @@
Buy Ticket
-
-
- -
-
- -
-
- -
-
+ + + -
-
- -
-
- -
-
Link to your ticket! +

+

You'll be redirected in a few moments...

@@ -153,37 +119,6 @@ class="q-pa-lg q-pt-xl lnbits__dialog-card" > - -
-
Continue to checkout
-
- Your fiat checkout opened in a new tab. If it did not, use the - button below. -
- - Go to checkout - -
-
- Copy payment link - Close -
-
{ - if (this.isFiatCurrency(row.currency)) { + if (row.currency != 'sats') { return LNbits.utils.formatCurrency( row.price_per_ticket.toFixed(2), row.currency @@ -106,21 +105,14 @@ window.PageEvents = { show: false, data: { currency: 'sats', - allow_fiat: false, - fiat_currency: 'GBP', extra: { - promo_codes: [], - notification_subject: '', - notification_body: '' + promo_codes: [] } } } } }, methods: { - isFiatCurrency(currency) { - return !['sat', 'sats'].includes((currency || '').toLowerCase()) - }, getTickets() { LNbits.api .request( @@ -153,35 +145,6 @@ window.PageEvents = { .catch(LNbits.utils.notifyApiError) }) }, - resendTicketEmail(ticket) { - if (!ticket.paid || !ticket.email) return - const wallet = _.findWhere(this.g.user.wallets, {id: ticket.wallet}) - if (!wallet) return - - this.resendingTicketEmails.push(ticket.id) - LNbits.api - .request( - 'POST', - '/events/api/v1/tickets/' + ticket.id + '/resend-email', - wallet.adminkey - ) - .then(response => { - this.tickets = this.tickets.map(obj => - obj.id === ticket.id ? response.data : obj - ) - Quasar.Notify.create({ - type: 'positive', - message: 'Ticket email resent.', - icon: null - }) - }) - .catch(LNbits.utils.notifyApiError) - .finally(() => { - this.resendingTicketEmails = this.resendingTicketEmails.filter( - ticketId => ticketId !== ticket.id - ) - }) - }, exportticketsCSV() { LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets) }, @@ -224,12 +187,7 @@ window.PageEvents = { }, saveSettings() { LNbits.api - .request( - 'PUT', - '/events/api/v1/events/settings', - null, - this.settings - ) + .request('PUT', '/events/api/v1/events/settings', null, this.settings) .then(() => { Quasar.Notify.create({type: 'positive', message: 'Settings saved'}) }) @@ -308,18 +266,10 @@ window.PageEvents = { delete data.event_end_day delete data.event_end_time - if (data.extra?.promo_codes) { + if (data.extra && !data.extra.promo_codes) { data.extra.promo_codes = data.extra.promo_codes - .filter(code => code.code?.trim() !== '') - .map(code => ({ - ...code, - code: code.code.trim().toUpperCase() - })) - } - if (!this.isFiatCurrency(data.currency)) { - if (!data.allow_fiat) { - data.fiat_currency = 'GBP' - } + .filter(code => code.trim() !== '') + .map(code => code.trim().toUpperCase()) } if (data.id) { @@ -343,8 +293,6 @@ window.PageEvents = { } else { this.formDialog.data = { currency: 'sats', - allow_fiat: false, - fiat_currency: 'GBP', event_start_day: '', event_start_time: '', event_end_day: '', @@ -352,11 +300,7 @@ window.PageEvents = { extra: { conditional: false, min_tickets: 1, - email_notifications: false, - nostr_notifications: false, - promo_codes: [], - notification_subject: '', - notification_body: '' + promo_codes: [] } } } @@ -365,15 +309,8 @@ window.PageEvents = { resetEventDialog() { this.formDialog.show = false this.formDialog.data = { - currency: 'sats', - allow_fiat: false, - fiat_currency: 'GBP', extra: { - email_notifications: false, - nostr_notifications: false, - promo_codes: [], - notification_subject: '', - notification_body: '' + promo_codes: [] } } }, diff --git a/static/js/index.vue b/static/js/index.vue index 4117f47..df3a990 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -192,7 +192,13 @@ @@ -284,7 +290,13 @@ @@ -317,12 +329,10 @@ >