Skip to content

Commit

Permalink
feat(googlereader): Add feed icon URLs endpoint
Browse files Browse the repository at this point in the history
Adds an endpoint to the Google Reader integration to serve
feed icon URLs.
  • Loading branch information
jocmp committed Mar 5, 2025
1 parent ad02f21 commit ce8726f
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 4 deletions.
36 changes: 35 additions & 1 deletion internal/googlereader/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func (r RequestModifiers) String() string {
func Serve(router *mux.Router, store *storage.Storage) {
handler := &handler{store, router}
router.HandleFunc("/accounts/ClientLogin", handler.clientLoginHandler).Methods(http.MethodPost).Name("ClientLogin")
router.HandleFunc("/reader/api/0/icon/{iconHash}", handler.iconHandler).Methods(http.MethodGet).Name("Icon")

middleware := newMiddleware(store)
sr := router.PathPrefix("/reader/api/0").Subrouter()
Expand Down Expand Up @@ -727,6 +728,30 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
})
}

func (h *handler) iconHandler(w http.ResponseWriter, r *http.Request) {
clientIP := request.ClientIP(r)
iconHash := request.RouteStringParam(r, "iconHash")

slog.Debug("[GoogleReader] Handle /icon/{iconHash}",
slog.String("handler", "iconHandler"),
slog.String("client_ip", clientIP),
slog.String("user_agent", r.UserAgent()),
slog.String("icon_hash", iconHash),
)

icon, err := h.store.IconByHash(iconHash)

if err != nil {
json.ServerError(w, r, err)
return
}

builder := response.New(w, r)
builder.WithHeader("Content-Type", icon.MimeType)
builder.WithBody(icon.Content)
builder.Write()
}

func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed, error) {
feedID, err := strconv.ParseInt(stream.ID, 10, 64)
if err != nil {
Expand Down Expand Up @@ -827,6 +852,14 @@ func move(stream Stream, destination Stream, store *storage.Storage, userID int6
return store.UpdateFeed(feed)
}

func (h *handler) feedIconURL(f *model.Feed) string {
if f.Icon != nil {
return config.Opts.RootURL() + route.Path(h.router, "Icon", "iconHash", f.Icon.IconHash)
} else {
return ""
}
}

func (h *handler) editSubscriptionHandler(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
clientIP := request.ClientIP(r)
Expand Down Expand Up @@ -1208,6 +1241,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
json.ServerError(w, r, err)
return
}

result.Subscriptions = make([]subscription, 0)
for _, feed := range feeds {
result.Subscriptions = append(result.Subscriptions, subscription{
Expand All @@ -1216,7 +1250,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
URL: feed.FeedURL,
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
HTMLURL: feed.SiteURL,
IconURL: "", // TODO: Icons are base64 encoded in the DB.
IconURL: h.feedIconURL(feed),
})
}
json.OK(w, r, result)
Expand Down
5 changes: 3 additions & 2 deletions internal/model/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Icons []*Icon

// FeedIcon is a junction table between feeds and icons.
type FeedIcon struct {
FeedID int64 `json:"feed_id"`
IconID int64 `json:"icon_id"`
FeedID int64 `json:"feed_id"`
IconID int64 `json:"icon_id"`
IconHash string `json:"icon_hash"`
}
7 changes: 6 additions & 1 deletion internal/storage/feed_query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
c.title as category_title,
c.hide_globally as category_hidden,
fi.icon_id,
i.hash,
u.timezone,
f.apprise_service_urls,
f.webhook_url,
Expand All @@ -178,6 +179,8 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
categories c ON c.id=f.category_id
LEFT JOIN
feed_icons fi ON fi.feed_id=f.id
LEFT JOIN
icons i ON i.id=fi.icon_id
LEFT JOIN
users u ON u.id=f.user_id
WHERE %s
Expand All @@ -201,6 +204,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
for rows.Next() {
var feed model.Feed
var iconID sql.NullInt64
var iconHash string
var tz string
feed.Category = &model.Category{}

Expand Down Expand Up @@ -237,6 +241,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
&feed.Category.Title,
&feed.Category.HideGlobally,
&iconID,
&iconHash,
&tz,
&feed.AppriseServiceURLs,
&feed.WebhookURL,
Expand All @@ -253,7 +258,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
}

if iconID.Valid {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.Int64}
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.Int64, IconHash: iconHash}
} else {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: 0}
}
Expand Down
24 changes: 24 additions & 0 deletions internal/storage/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
return &icon, nil
}

// IconByHash returns a feed icon.
func (s *Storage) IconByHash(hash string) (*model.Icon, error) {
query := `
SELECT
icons.id,
icons.hash,
icons.mime_type,
icons.content
FROM icons
LEFT JOIN feed_icons ON feed_icons.icon_id=icons.id
LEFT JOIN feeds ON feeds.id=feed_icons.feed_id
WHERE
icons.hash=$1
LIMIT 1
`
var icon model.Icon
err := s.db.QueryRow(query, hash).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content)
if err != nil {
return nil, fmt.Errorf(`store: unable to fetch icon: %v`, err)
}

return &icon, nil
}

// StoreFeedIcon creates or updates a feed icon.
func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
tx, err := s.db.Begin()
Expand Down

0 comments on commit ce8726f

Please sign in to comment.