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
61 changes: 61 additions & 0 deletions alby/alby_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/getAlby/hub/config"
Expand Down Expand Up @@ -208,3 +209,63 @@ func (svc *albyService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
Incidents: incidents,
}, nil
}

func (svc *albyService) GetLatestBlogPost(ctx context.Context) (*BlogPost, error) {
client := &http.Client{Timeout: 10 * time.Second}

url := fmt.Sprintf("%s/hub/blog/latest", albyInternalAPIURL)

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to blog endpoint")
return nil, err
}
setDefaultRequestHeaders(req)

res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch blog endpoint")
return nil, err
}
defer res.Body.Close()

body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).Error("Failed to read response body")
return nil, errors.New("failed to read response body")
}
Comment on lines +219 to +236

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unwrapped errors in the new method.
rg -n 'GetLatestBlogPost|return nil, err|errors.New\("failed to read response body"\)' alby/alby_service.go

Repository: getAlby/hub

Length of output: 696


🏁 Script executed:

# Read the GetLatestBlogPost method to see context
sed -n '213,270p' alby/alby_service.go

Repository: getAlby/hub

Length of output: 1763


🏁 Script executed:

# Check imports to verify fmt and errors are available
head -30 alby/alby_service.go

Repository: getAlby/hub

Length of output: 475


🏁 Script executed:

# Confirm the exact line numbers and error returns in the method
sed -n '213,265p' alby/alby_service.go | cat -n

Repository: getAlby/hub

Length of output: 1984


Wrap errors with context in GetLatestBlogPost to aid debugging.

Lines 221, 228, 235, and 257 return raw or opaque errors, losing caller context. Use fmt.Errorf("context: %w", err) to preserve error chains for diagnosis across stack layers.

Suggested error-wrapping patch
 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 	if err != nil {
 		logger.Logger.WithError(err).Error("Error creating request to blog endpoint")
-		return nil, err
+		return nil, fmt.Errorf("create blog request: %w", err)
 	}
 
 	res, err := client.Do(req)
 	if err != nil {
 		logger.Logger.WithError(err).Error("Failed to fetch blog endpoint")
-		return nil, err
+		return nil, fmt.Errorf("fetch blog endpoint: %w", err)
 	}
 	defer res.Body.Close()
 
 	body, err := io.ReadAll(res.Body)
 	if err != nil {
 		logger.Logger.WithError(err).Error("Failed to read response body")
-		return nil, errors.New("failed to read response body")
+		return nil, fmt.Errorf("read blog response body: %w", err)
 	}
 
 	err = json.Unmarshal(body, &post)
 	if err != nil {
 		logger.Logger.WithError(err).Error("Failed to decode blog API response")
-		return nil, err
+		return nil, fmt.Errorf("decode blog API response: %w", err)
 	}

Per coding guidelines for Go: "Use error wrapping with fmt.Errorf("context: %w", err) for debugging".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@alby/alby_service.go` around lines 219 - 236, In GetLatestBlogPost wrap
returned errors with fmt.Errorf to preserve error chains and add context:
wherever the code currently logs and returns err (the request creation error
after logger.Logger.WithError(err).Error("Error creating request to blog
endpoint"), the client.Do error logged as "Failed to fetch blog endpoint", and
the io.ReadAll error logged as "Failed to read response body"), return
fmt.Errorf("creating blog request: %w", err), fmt.Errorf("fetching blog
endpoint: %w", err), and fmt.Errorf("reading blog response body: %w", err)
respectively (keep logging calls but change the returned errors), ensuring
imports include fmt and references to GetLatestBlogPost,
setDefaultRequestHeaders, client.Do, and io.ReadAll to locate the spots.


if res.StatusCode >= 300 {
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": res.StatusCode,
}).Error("Blog endpoint returned non-success code")
return nil, fmt.Errorf("blog endpoint returned non-success code: %s", string(body))
}

var post struct {
ID string `json:"id"`
Title string `json:"title"`
Lead string `json:"lead"`
URL string `json:"url"`
ImageURL string `json:"imageUrl"`
PublishedAt string `json:"publishedAt"`
}
err = json.Unmarshal(body, &post)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode blog API response")
return nil, err
}

if post.Title == "" || post.URL == "" {
return nil, errors.New("no blog post found")
}
Comment on lines +246 to +262

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle fallback keys and enforce required fields before returning.

Line 246 only decodes lead/url/imageUrl, and Line 260 validates only title/url. If upstream sends slug, description/excerpt, or link, this can return an incomplete BlogPost (empty id/description) instead of treating it as invalid.

Suggested normalization + validation patch
 func (svc *albyService) GetLatestBlogPost(ctx context.Context) (*BlogPost, error) {
@@
 	var post struct {
-		ID          string `json:"id"`
-		Title       string `json:"title"`
-		Lead        string `json:"lead"`
-		URL         string `json:"url"`
-		ImageURL    string `json:"imageUrl"`
-		PublishedAt string `json:"publishedAt"`
+		ID          string `json:"id"`
+		Slug        string `json:"slug"`
+		Title       string `json:"title"`
+		Lead        string `json:"lead"`
+		Description string `json:"description"`
+		Excerpt     string `json:"excerpt"`
+		URL         string `json:"url"`
+		Link        string `json:"link"`
+		ImageURL    string `json:"imageUrl"`
+		ImageURLAlt string `json:"image_url"`
+		CoverImage  string `json:"coverImage"`
+		CoverImage2 string `json:"cover_image"`
 	}
@@
-	if post.Title == "" || post.URL == "" {
+	firstNonEmpty := func(values ...string) string {
+		for _, v := range values {
+			if v != "" {
+				return v
+			}
+		}
+		return ""
+	}
+
+	id := firstNonEmpty(post.ID, post.Slug)
+	description := firstNonEmpty(post.Lead, post.Description, post.Excerpt)
+	url := firstNonEmpty(post.URL, post.Link)
+	imageURL := firstNonEmpty(post.ImageURL, post.ImageURLAlt, post.CoverImage, post.CoverImage2)
+
+	if id == "" || post.Title == "" || description == "" || url == "" {
 		return nil, errors.New("no blog post found")
 	}
 
 	return &BlogPost{
-		ID:          post.ID,
+		ID:          id,
 		Title:       post.Title,
-		Description: post.Lead,
-		URL:         post.URL,
-		ImageURL:    strings.ReplaceAll(post.ImageURL, "&", "&"),
+		Description: description,
+		URL:         url,
+		ImageURL:    strings.ReplaceAll(imageURL, "&", "&"),
 	}, nil
 }

Also applies to: 264-270

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@alby/alby_service.go` around lines 246 - 262, The decoded temporary struct
"post" may miss upstream variants (slug, link, description, excerpt, image) and
the code only validates Title and URL; update the normalization and validation
after json.Unmarshal in alby_service.go so you map fallback keys into the
canonical fields (e.g., use slug -> ID, link -> URL, description/excerpt ->
Lead/description, alternate image keys -> ImageURL) and then enforce required
fields (ID, Title, URL) before returning; if any required canonical field is
still empty return an error ("no blog post found" or similar) and log the
detailed decode/normalization failure via logger.Logger.WithError.


return &BlogPost{
ID: post.ID,
Title: post.Title,
Description: post.Lead,
URL: post.URL,
ImageURL: strings.ReplaceAll(post.ImageURL, "&", "&"),
}, nil
}
9 changes: 9 additions & 0 deletions alby/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ type AlbyService interface {
GetInfo(ctx context.Context) (*AlbyInfo, error)
GetBitcoinRate(ctx context.Context) (*BitcoinRate, error)
GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error)
GetLatestBlogPost(ctx context.Context) (*BlogPost, error)
}

type BlogPost struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
ImageURL string `json:"imageUrl,omitempty"`
}

type AlbyOAuthService interface {
Expand Down
5 changes: 4 additions & 1 deletion frontend/.env.local.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# set a custom messageboard wallet (should be a sub-wallet with only make invoice and list transactions permissions)
#VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894"
#VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need of env variables once we shift api fetching to backend. we have alby internal api endpoints already defined in backend


# optional blog endpoint used by Home Alby Blog widget
#VITE_ALBY_BLOG_ENDPOINT=https://getalby.com/api/hub/blog/latest
72 changes: 72 additions & 0 deletions frontend/src/components/home/widgets/AlbyBlogWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { SquareArrowOutUpRightIcon } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "src/components/ui/card";
import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
import { useAlbyBlog } from "src/hooks/useAlbyBlog";
import { cn } from "src/lib/utils";

const fallbackThemes = [
"from-emerald-200 via-cyan-200 to-yellow-200",
"from-orange-200 via-amber-100 to-pink-100",
"from-slate-200 via-zinc-100 to-lime-100",
"from-sky-200 via-indigo-100 to-violet-100",
];

function hashString(str: string): number {
return Math.abs([...str].reduce((sum, ch) => sum + ch.charCodeAt(0), 0));
}

export function AlbyBlogWidget() {
const { data: post } = useAlbyBlog();

if (!post) {
return null;
}

const theme = fallbackThemes[hashString(post.id) % fallbackThemes.length];

return (
<Card>
<CardHeader>
<CardTitle>Alby Blog</CardTitle>
</CardHeader>
<CardContent>
<div className="relative h-[247px] overflow-hidden rounded-xl border">
{post.imageUrl ? (
<img
src={post.imageUrl}
alt={post.title}
className="absolute inset-0 size-full object-cover"
/>
) : (
<div className={cn("absolute inset-0 bg-gradient-to-br", theme)}>
<div className="absolute -left-10 top-6 size-36 rounded-full bg-white/35 blur-3xl" />
<div className="absolute -right-8 bottom-2 size-40 rounded-full bg-white/20 blur-3xl" />
<div className="absolute inset-0 bg-white/10" />
</div>
)}
</div>
</CardContent>
<CardFooter className="flex flex-col items-start gap-4">
<div className="space-y-1">
<CardTitle className="text-xl leading-7">{post.title}</CardTitle>
<CardDescription className="text-base leading-6">
{post.description}
</CardDescription>
</div>
<div className="flex w-full justify-end">
<ExternalLinkButton to={post.url} variant="outline">
Read on Alby Blog
<SquareArrowOutUpRightIcon />
</ExternalLinkButton>
</div>
</CardFooter>
</Card>
);
}
17 changes: 17 additions & 0 deletions frontend/src/hooks/useAlbyBlog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import useSWR from "swr";

import { swrFetcher } from "src/utils/swr";

type BlogPost = {
id: string;
title: string;
description: string;
url: string;
imageUrl?: string;
};

export function useAlbyBlog() {
return useSWR<BlogPost>("/api/alby/blog/latest", swrFetcher, {
dedupingInterval: 5 * 60 * 1000, // 5 minutes
});
}
3 changes: 3 additions & 0 deletions frontend/src/screens/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import React from "react";
import albyGo from "src/assets/suggested-apps/alby-go.png";
import zapplanner from "src/assets/suggested-apps/zapplanner.png";
import { AppOfTheDayWidget } from "src/components/home/widgets/AppOfTheDayWidget";
import { AlbyBlogWidget } from "src/components/home/widgets/AlbyBlogWidget";
import { BlockHeightWidget } from "src/components/home/widgets/BlockHeightWidget";
import { ForwardsWidget } from "src/components/home/widgets/ForwardsWidget";
import { LatestUsedAppsWidget } from "src/components/home/widgets/LatestUsedAppsWidget";
Expand Down Expand Up @@ -193,6 +194,8 @@ function Home() {
</Card>
</Link>

<AlbyBlogWidget />

<Card>
<CardHeader>
<div className="flex justify-between items-center">
Expand Down
2 changes: 1 addition & 1 deletion frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const insertDevCSPPlugin: Plugin = {
"<head>",
`<head>
<!-- DEV-ONLY CSP - when making changes here, also update the CSP header in http_service.go (without the nonce!) -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ${DEVELOPMENT_NONCE}; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com" />`
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ${DEVELOPMENT_NONCE}; img-src 'self' https://uploads.getalby-assets.com https://getalby.com https://framerusercontent.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com" />`
);
},
},
Expand Down
12 changes: 12 additions & 0 deletions http/alby_http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(readOnlyApiGroup *echo.
e.GET("/api/alby/callback", albyHttpSvc.albyCallbackHandler)
e.GET("/api/alby/info", albyHttpSvc.albyInfoHandler)
e.GET("/api/alby/rates", albyHttpSvc.albyBitcoinRateHandler)
e.GET("/api/alby/blog/latest", albyHttpSvc.albyBlogLatestHandler)
readOnlyApiGroup.GET("/alby/me", albyHttpSvc.albyMeHandler)
fullAccessApiGroup.POST("/alby/link-account", albyHttpSvc.albyLinkAccountHandler)
fullAccessApiGroup.POST("/alby/auto-channel", albyHttpSvc.autoChannelHandler)
Expand Down Expand Up @@ -96,6 +97,17 @@ func (albyHttpSvc *AlbyHttpService) albyBitcoinRateHandler(c echo.Context) error
return c.JSON(http.StatusOK, rate)
}

func (albyHttpSvc *AlbyHttpService) albyBlogLatestHandler(c echo.Context) error {
post, err := albyHttpSvc.albySvc.GetLatestBlogPost(c.Request().Context())
if err != nil {
logger.Logger.WithError(err).Error("Failed to get latest blog post")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to get latest blog post: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, post)
}

func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error {
code := c.QueryParam("code")

Expand Down
2 changes: 1 addition & 1 deletion http/http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
ContentTypeNosniff: "nosniff",
XFrameOptions: "DENY",
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com",
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com https://framerusercontent.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com",
ReferrerPolicy: "no-referrer",
}))
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Expand Down
61 changes: 61 additions & 0 deletions tests/mocks/AlbyService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions wails/wails_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,17 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: rate, Error: ""}
case "/api/alby/blog/latest":
post, err := app.svc.GetAlbySvc().GetLatestBlogPost(ctx)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to get latest blog post")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: post, Error: ""}
case "/api/apps":
switch method {
case "POST":
Expand Down
Loading