A locally focused bluesky appview
1package feed
2
3import (
4 "bytes"
5 "log/slog"
6 "net/http"
7 "time"
8
9 "github.com/bluesky-social/indigo/api/bsky"
10 "github.com/bluesky-social/indigo/atproto/identity"
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 cid "github.com/ipfs/go-cid"
13 "github.com/labstack/echo/v4"
14 mh "github.com/multiformats/go-multihash"
15 "github.com/whyrusleeping/konbini/hydration"
16 "github.com/whyrusleeping/konbini/views"
17 "gorm.io/gorm"
18)
19
20// HandleGetFeedGenerator implements app.bsky.feed.getFeedGenerator
21func HandleGetFeedGenerator(c echo.Context, db *gorm.DB, hydrator *hydration.Hydrator, dir identity.Directory) error {
22 ctx := c.Request().Context()
23
24 // Parse parameters
25 feedURI := c.QueryParam("feed")
26 if feedURI == "" {
27 return c.JSON(http.StatusBadRequest, map[string]any{
28 "error": "InvalidRequest",
29 "message": "feed parameter is required",
30 })
31 }
32
33 nu, err := hydrator.NormalizeUri(ctx, feedURI)
34 if err != nil {
35 return err
36 }
37 feedURI = nu
38
39 viewer := getUserDID(c)
40 _ = viewer
41
42 // Extract feed generator DID and rkey from URI
43 did := extractDIDFromURI(feedURI)
44 rkey := extractRkeyFromURI(feedURI)
45
46 if did == "" || rkey == "" {
47 return c.JSON(http.StatusBadRequest, map[string]any{
48 "error": "InvalidRequest",
49 "message": "invalid feed URI format",
50 })
51 }
52
53 // Query feed generator from database
54 type feedGenRow struct {
55 ID uint
56 Did string
57 Raw []byte
58 AuthorDid string
59 Indexed time.Time
60 }
61 var feedGen feedGenRow
62 err = db.Raw(`
63 SELECT fg.id, fg.did, fg.raw, r.did as author_did, indexed
64 FROM feed_generators fg
65 JOIN repos r ON r.id = fg.author
66 WHERE r.did = ? AND fg.rkey = ?
67 `, did, rkey).Scan(&feedGen).Error
68
69 if err != nil || feedGen.ID == 0 {
70 // Track this missing feed generator for fetching
71 hydrator.AddMissingRecord(feedURI, true)
72
73 return c.JSON(http.StatusNotFound, map[string]any{
74 "error": "NotFound",
75 "message": "feed generator not found",
76 })
77 }
78
79 // Decode the feed generator record
80 var feedGenRecord bsky.FeedGenerator
81 if err := feedGenRecord.UnmarshalCBOR(bytes.NewReader(feedGen.Raw)); err != nil {
82 slog.Error("failed to decode feed generator record", "error", err)
83 return c.JSON(http.StatusInternalServerError, map[string]any{
84 "error": "InternalError",
85 "message": "failed to decode feed generator record",
86 })
87 }
88
89 // Compute CID from raw bytes
90 hash, err := mh.Sum(feedGen.Raw, mh.SHA2_256, -1)
91 if err != nil {
92 slog.Error("failed to hash record", "error", err)
93 return c.JSON(http.StatusInternalServerError, map[string]any{
94 "error": "InternalError",
95 "message": "failed to compute CID",
96 })
97 }
98 recordCid := cid.NewCidV1(cid.DagCBOR, hash).String()
99
100 // Hydrate the creator
101 creatorInfo, err := hydrator.HydrateActor(ctx, feedGen.AuthorDid)
102 if err != nil {
103 slog.Error("failed to hydrate creator", "error", err, "did", feedGen.AuthorDid)
104 return c.JSON(http.StatusInternalServerError, map[string]any{
105 "error": "InternalError",
106 "message": "failed to hydrate creator",
107 })
108 }
109
110 // Count likes for this feed generator
111 var likeCount int64
112
113 // Check if viewer has liked this feed generator
114 viewerLike := ""
115
116 // Validate the service DID (check if it's resolvable)
117 serviceDID, err := syntax.ParseDID(feedGenRecord.Did)
118 if err != nil {
119 slog.Error("invalid service DID in feed generator", "error", err, "did", feedGenRecord.Did)
120 return c.JSON(http.StatusInternalServerError, map[string]any{
121 "error": "InternalError",
122 "message": "invalid service DID",
123 })
124 }
125
126 // Try to resolve the service DID to check if it's online/valid
127 isOnline := true
128 isValid := true
129 serviceIdent, err := dir.LookupDID(ctx, serviceDID)
130 if err != nil {
131 slog.Warn("failed to resolve service DID", "error", err, "did", serviceDID)
132 isOnline = false
133 isValid = false
134 } else {
135 // Check if service has an endpoint
136 serviceEndpoint := serviceIdent.PDSEndpoint()
137 if serviceEndpoint == "" {
138 slog.Warn("service has no PDS endpoint", "did", serviceDID)
139 isValid = false
140 }
141 }
142
143 // Build the generator view
144 generatorView := views.GeneratorView(
145 feedURI,
146 recordCid,
147 &feedGenRecord,
148 creatorInfo,
149 likeCount,
150 viewerLike,
151 feedGen.Indexed.Format(time.RFC3339),
152 )
153
154 output := &bsky.FeedGetFeedGenerator_Output{
155 View: generatorView,
156 IsOnline: isOnline,
157 IsValid: isValid,
158 }
159
160 return c.JSON(http.StatusOK, output)
161}