mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
* 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>
178 lines
6 KiB
Go
178 lines
6 KiB
Go
package http
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/getAlby/hub/alby"
|
|
"github.com/getAlby/hub/config"
|
|
"github.com/getAlby/hub/logger"
|
|
"github.com/getAlby/hub/service"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
type AlbyHttpService struct {
|
|
albySvc alby.AlbyService
|
|
albyOAuthSvc alby.AlbyOAuthService
|
|
appConfig *config.AppConfig
|
|
svc service.Service
|
|
}
|
|
|
|
func NewAlbyHttpService(svc service.Service, albySvc alby.AlbyService, albyOAuthSvc alby.AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService {
|
|
return &AlbyHttpService{
|
|
albySvc: albySvc,
|
|
albyOAuthSvc: albyOAuthSvc,
|
|
appConfig: appConfig,
|
|
svc: svc,
|
|
}
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(readOnlyApiGroup *echo.Group, fullAccessApiGroup *echo.Group, e *echo.Echo) {
|
|
e.GET("/api/alby/callback", albyHttpSvc.albyCallbackHandler)
|
|
e.GET("/api/alby/info", albyHttpSvc.albyInfoHandler)
|
|
e.GET("/api/alby/rates/:currency", albyHttpSvc.albyBitcoinRateHandler)
|
|
e.GET("/api/alby/currencies", albyHttpSvc.albyCurrenciesHandler)
|
|
e.GET("/api/alby/stories", albyHttpSvc.albyStoriesHandler)
|
|
readOnlyApiGroup.GET("/alby/me", albyHttpSvc.albyMeHandler)
|
|
fullAccessApiGroup.POST("/alby/link-account", albyHttpSvc.albyLinkAccountHandler)
|
|
fullAccessApiGroup.POST("/alby/auto-channel", albyHttpSvc.autoChannelHandler)
|
|
fullAccessApiGroup.POST("/alby/unlink-account", albyHttpSvc.unlinkHandler)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) autoChannelHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var autoChannelRequest alby.AutoChannelRequest
|
|
if err := c.Bind(&autoChannelRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
autoChannelResponseResponse, err := albyHttpSvc.albyOAuthSvc.RequestAutoChannel(ctx, albyHttpSvc.svc.GetLNClient(), autoChannelRequest.IsPublic)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request auto channel: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, autoChannelResponseResponse)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) unlinkHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
err := albyHttpSvc.albyOAuthSvc.UnlinkAccount(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to unlink: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyInfoHandler(c echo.Context) error {
|
|
info, err := albyHttpSvc.albySvc.GetInfo(c.Request().Context())
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to request alby info endpoint")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request alby info endpoint: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, info)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyBitcoinRateHandler(c echo.Context) error {
|
|
rate, err := albyHttpSvc.albySvc.GetBitcoinRate(c.Request().Context(), c.Param("currency"))
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to get Bitcoin rate")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get Bitcoin rate: %s", err.Error()),
|
|
})
|
|
}
|
|
return c.JSON(http.StatusOK, rate)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyCurrenciesHandler(c echo.Context) error {
|
|
currencies, err := albyHttpSvc.albySvc.GetCurrencies(c.Request().Context())
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to get currencies")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get currencies: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, currencies)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyStoriesHandler(c echo.Context) error {
|
|
stories, err := albyHttpSvc.albyOAuthSvc.GetStories(c.Request().Context())
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to get stories")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get stories: %s", err.Error()),
|
|
})
|
|
}
|
|
return c.JSON(http.StatusOK, stories)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error {
|
|
code := c.QueryParam("code")
|
|
|
|
err := albyHttpSvc.albyOAuthSvc.CallbackHandler(c.Request().Context(), code)
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to handle Alby OAuth callback")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to handle Alby OAuth callback: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
if albyHttpSvc.appConfig.IsDefaultClientId() {
|
|
// do not redirect if using default OAuth client
|
|
// redirect will be handled by the frontend instead
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
redirectUrl := albyHttpSvc.appConfig.GetBaseFrontendUrl()
|
|
|
|
if redirectUrl == "" {
|
|
// OAuth using a custom client requires a base URL set for the callback
|
|
return errors.New("no BASE_URL set")
|
|
}
|
|
|
|
return c.Redirect(http.StatusFound, redirectUrl)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyMeHandler(c echo.Context) error {
|
|
me, err := albyHttpSvc.albyOAuthSvc.GetMe(c.Request().Context())
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to request alby me endpoint")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request alby me endpoint: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, me)
|
|
}
|
|
|
|
func (albyHttpSvc *AlbyHttpService) albyLinkAccountHandler(c echo.Context) error {
|
|
var linkAccountRequest alby.AlbyLinkAccountRequest
|
|
if err := c.Bind(&linkAccountRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := albyHttpSvc.albyOAuthSvc.LinkAccount(c.Request().Context(), albyHttpSvc.svc.GetLNClient(), linkAccountRequest.Budget, linkAccountRequest.Renewal)
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to connect alby account")
|
|
return err
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|