alby-hub/alby/models.go
saunter 23dccc6c6f
feat: stories (#2172)
* feat: integrate Stories widget with backend endpoint

Add stories endpoint plumbing for HTTP and Wails, wire the Home Stories card
to fetch from /api/alby/stories, and keep it first in the right column.

Made-with: Cursor

* feat(home): story modal CTAs and preview fallback

- Add contextual actions in the story dialog (update hub with version,
  open Alby Go in-app, install extension) keyed by kind or title
- Use preview stories when the stories API request fails
- Pass hub version from useInfo into the update link

Made-with: Cursor

* feat(stories): polish modal, drop preview fallback

- Widen modal and put video edge-to-edge with overlay close button
- Drop verbose header and 'Watch on YouTube' button
- Remove previewStories fallback so widget hides until upstream API ships
- Tighten title line-height

* feat(stories): render cta from API instead of mapping by kind

Move CTA copy and URLs into the API response. Hub renders story.cta
directly, so adding new story types no longer requires a hub release.

* chore(csp): allow cdn.getalby-assets.com in img-src

* feat(stories): bump avatar size and add ring gap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): post-review cleanups

- Use react-router Link for in-tab CTA instead of plain <a>.
- Drop redundant www.youtube.com from frame-src (embeds always go through nocookie).
- Tighten stories endpoint status check from >= 300 to >= 400.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): address CodeRabbit feedback

- Switch StoriesWidget to useSWR + swrFetcher (project convention).
- Guard story iframe with isYouTubeUrl so non-YouTube urls never embed.
- Wrap GetStories errors with fmt.Errorf("...: %w", err).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): drop isYouTubeUrl guard

Stories are curated and always YouTube; the runtime check was
redundant. CSP frame-src still constrains the iframe source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): drop getYouTubeEmbedUrl, embed videoUrl as-is

The Alby API now sends canonical youtube-nocookie embed URLs with
autoplay/rel query strings (getAlby/getalby.com#2568), so the
runtime normalization is no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): use w-16 instead of arbitrary w-[73px]

Match the avatar's size token; no magic numbers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): take videoId from API and assemble embed url locally

Pairs with getAlby/getalby.com#2568. The API now sends just the
YouTube videoId; the hub composes the canonical embed URL so the
domain/query-string format stays in one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): treat 3xx as non-success, matching file convention

The other status checks in alby_oauth_service.go all use >= 300;
align GetStories so redirects don't slip through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): move viewed-storage key to constants, widen story button

Address review feedback:
- Centralize the localStorage key for viewed stories in localStorageKeys
  alongside the other keys.
- Widen the story button from w-16 to w-20 so "Alby Extension" fits on
  one line and matches the other titles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(stories): bump story button to w-24 so titles fit one line

w-20 still wrapped "Alby Extension"; w-24 fits all current titles
without truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "chore(stories): bump story button to w-24 so titles fit one line"

This reverts commit 0b47438f50.

* chore(stories): split title words onto separate lines

Reserve two lines for every story title so avatars align regardless of
title length.

* chore(stories): align homeStoriesViewed key with sibling pattern

* chore(stories): fit titles on one line

* chore(stories): widen story button to w-21 for one-line titles

---------

Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 10:50:15 +02:00

204 lines
7.2 KiB
Go

package alby
import (
"context"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
)
type AlbyService interface {
GetInfo(ctx context.Context) (*AlbyInfo, error)
GetBitcoinRate(ctx context.Context, currency string) (*BitcoinRate, error)
GetCurrencies(ctx context.Context) ([]Currency, error)
GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error)
}
type AlbyOAuthService interface {
events.EventSubscriber
GetLSPChannelOffer(ctx context.Context) (*LSPChannelOffer, error)
GetLSPInfo(ctx context.Context, lsp, network string) (*LSPInfo, error)
CreateLSPOrder(ctx context.Context, lsp, network string, lspChannelRequest *LSPChannelRequest) (*LSPChannelResponse, error)
GetAuthUrl() string
GetUserIdentifier() (string, error)
GetLightningAddress() (string, error)
IsConnected(ctx context.Context) bool
LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budgetSat uint64, renewal string) error
CallbackHandler(ctx context.Context, code string) error
GetMe(ctx context.Context) (*AlbyMe, error)
UnlinkAccount(ctx context.Context) error
RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error)
GetVssAuthToken(ctx context.Context, nodeIdentifier string) (string, error)
RemoveOAuthAccessToken() error
CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error)
DeleteLightningAddress(ctx context.Context, address string) error
GetStories(ctx context.Context) ([]Story, error)
}
type CreateLightningAddressResponse struct {
Address string `json:"address"`
FullAddress string `json:"full_address"`
}
type AlbyLinkAccountRequest struct {
Budget uint64 `json:"budget"`
Renewal string `json:"renewal"`
}
type AutoChannelRequest struct {
IsPublic bool `json:"isPublic"`
}
type AutoChannelResponse struct {
Invoice string `json:"invoice"`
ChannelSize uint64 `json:"channelSize"` // deprecated
ChannelSizeSat uint64 `json:"channelSizeSat"`
Fee uint64 `json:"fee"` // deprecated
FeeSat uint64 `json:"feeSat"`
}
type AlbyInfoHub struct {
LatestVersion string `json:"latestVersion"`
LatestReleaseNotes string `json:"latestReleaseNotes"`
}
type AlbyInfoIncident struct {
Name string `json:"name"`
Started string `json:"started"`
Status string `json:"status"`
Impact string `json:"impact"`
Url string `json:"url"`
}
type AlbyInfo struct {
Hub AlbyInfoHub `json:"hub"`
Status string `json:"status"`
Healthy bool `json:"healthy"`
AccountAvailable bool `json:"accountAvailable"` // false if country is blocked (can still use Alby Hub without an Alby Account)
Incidents []AlbyInfoIncident `json:"incidents"`
}
type AlbyMeHub struct {
Name string `json:"name"`
Config map[string]interface{} `json:"config"`
}
type AlbyMeSubscription struct {
PlanCode string `json:"plan_code"`
}
type AlbyMe struct {
Identifier string `json:"identifier"`
NPub string `json:"nostr_pubkey"`
LightningAddress string `json:"lightning_address"`
Email string `json:"email"`
Name string `json:"name"`
Avatar string `json:"avatar"`
KeysendPubkey string `json:"keysend_pubkey"`
SharedNode bool `json:"shared_node"`
Hub AlbyMeHub `json:"hub"`
Subscription AlbyMeSubscription `json:"subscription"`
}
type ChannelPeerSuggestion struct {
Network string `json:"network"`
PaymentMethod string `json:"paymentMethod"`
Pubkey string `json:"pubkey"`
Host string `json:"host"`
MinimumChannelSize uint64 `json:"minimumChannelSize"` // deprecated
MinimumChannelSizeSat uint64 `json:"minimumChannelSizeSat"`
MaximumChannelSize uint64 `json:"maximumChannelSize"` // deprecated
MaximumChannelSizeSat uint64 `json:"maximumChannelSizeSat"`
MaximumChannelExpiryBlocks *uint32 `json:"maximumChannelExpiryBlocks"`
Name string `json:"name"`
Image string `json:"image"`
Identifier string `json:"identifier"`
ContactUrl string `json:"contactUrl"`
Type string `json:"type"`
Terms string `json:"terms"`
Description string `json:"description"`
Note string `json:"note"`
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
FeeTotalSat1m *uint32 `json:"feeTotalSat1m"`
FeeTotalSat2m *uint32 `json:"feeTotalSat2m"`
FeeTotalSat3m *uint32 `json:"feeTotalSat3m"`
}
type LSPChannelOffer struct {
LspName string `json:"lspName"`
LspContactUrl string `json:"lspContactUrl"`
LspBalanceSat uint64 `json:"lspBalanceSat"`
FeeTotalSat uint64 `json:"feeTotalSat"`
FeeTotalUsd uint64 `json:"feeTotalUsd"` // in cents
CurrentPaymentMethod string `json:"currentPaymentMethod"`
Terms string `json:"terms"`
LspDescription string `json:"lspDescription"`
}
type Currency struct {
IsoCode string `json:"iso_code"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Priority int `json:"priority"`
}
type BitcoinRate struct {
Code string `json:"code"`
Symbol string `json:"symbol"`
Rate string `json:"rate"`
RateFloat float64 `json:"rate_float"`
RateCents int64 `json:"rate_cents"`
}
type ErrorResponse struct {
Message string `json:"message"`
}
type StoryCta struct {
Label string `json:"label"`
URL string `json:"url"`
OpenInNewTab bool `json:"openInNewTab"`
}
type Story struct {
ID int `json:"id"`
Title string `json:"title"`
Avatar string `json:"avatar"`
VideoID string `json:"videoId,omitempty"`
Cta *StoryCta `json:"cta,omitempty"`
}
type LSPChannelPaymentBolt11 struct {
Invoice string `json:"invoice"`
FeeTotalSat string `json:"fee_total_sat"`
}
type LSPChannelPayment struct {
Bolt11 LSPChannelPaymentBolt11 `json:"bolt11"`
// TODO: add onchain
}
type LSPChannelResponse struct {
Payment *LSPChannelPayment `json:"payment"`
}
type LSPChannelRequest struct {
PublicKey string `json:"public_key"`
LSPBalanceSat string `json:"lsp_balance_sat"`
ClientBalanceSat string `json:"client_balance_sat"`
RequiredChannelConfirmations uint64 `json:"required_channel_confirmations"`
FundingConfirmsWithinBlocks uint64 `json:"funding_confirms_within_blocks"`
ChannelExpiryBlocks uint64 `json:"channel_expiry_blocks"`
Token string `json:"token"`
RefundOnchainAddress string `json:"refund_onchain_address"`
AnnounceChannel bool `json:"announce_channel"`
}
type LSPInfo struct {
Pubkey string
Address string
Port uint16
MaxChannelExpiryBlocks uint64
MinRequiredChannelConfirmations uint64
MinFundingConfirmsWithinBlocks uint64
}