this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add files

+384
+142
feed_handlers.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "log/slog" 7 + "net/http" 8 + "strconv" 9 + ) 10 + 11 + type FeedReponse struct { 12 + Cursor string `json:"cursor"` 13 + Feed []FeedItem `json:"feed"` 14 + } 15 + 16 + type FeedItem struct { 17 + Post string `json:"post"` 18 + FeedContext string `json:"feedContext"` 19 + } 20 + 21 + type WellKnownResponse struct { 22 + Context []string `json:"@context"` 23 + Id string `json:"id"` 24 + Service []WellKnownService `json:"service"` 25 + } 26 + 27 + type WellKnownService struct { 28 + Id string `json:"id"` 29 + Type string `json:"type"` 30 + ServiceEndpoint string `json:"serviceEndpoint"` 31 + } 32 + 33 + func (s *Server) HandleGetFeedSkeleton(w http.ResponseWriter, r *http.Request) { 34 + slog.Info("got request for feed skeleton", "host", r.RemoteAddr) 35 + params := r.URL.Query() 36 + 37 + feed := params.Get("feed") 38 + if feed == "" { 39 + slog.Error("missing feed query param", "host", r.RemoteAddr) 40 + http.Error(w, "missing feed query param", http.StatusBadRequest) 41 + return 42 + } 43 + slog.Info("request for feed", "feed", feed) 44 + 45 + limitStr := params.Get("limit") 46 + limit := 50 47 + if limitStr != "" { 48 + var err error 49 + limit, err = strconv.Atoi(limitStr) 50 + if err != nil { 51 + slog.Error("convert limit query param", "error", err) 52 + http.Error(w, "invalid limit query param", http.StatusBadRequest) 53 + return 54 + } 55 + if limit < 1 || limit > 100 { 56 + limit = 50 57 + } 58 + } 59 + 60 + cursor := params.Get("cursor") 61 + usersDID, err := getRequestUserDID(r) 62 + if err != nil { 63 + slog.Error("validate auth", "error", err) 64 + http.Error(w, "validate auth", http.StatusUnauthorized) 65 + return 66 + } 67 + if usersDID == "" { 68 + slog.Error("missing users DID from request") 69 + http.Error(w, "validate auth", http.StatusUnauthorized) 70 + return 71 + } 72 + 73 + resp, err := s.feeder.GetFeed(r.Context(), usersDID, feed, cursor, limit) 74 + if err != nil { 75 + slog.Error("get feed", "error", err, "feed", feed) 76 + http.Error(w, "error getting feed", http.StatusInternalServerError) 77 + return 78 + } 79 + 80 + b, err := json.Marshal(resp) 81 + if err != nil { 82 + slog.Error("marshall error", "error", err, "host", r.RemoteAddr) 83 + http.Error(w, "failed to encode resp", http.StatusInternalServerError) 84 + return 85 + } 86 + 87 + w.Header().Set("Content-Type", "application/json") 88 + 89 + w.Write(b) 90 + } 91 + 92 + type DescribeFeedResponse struct { 93 + DID string `json:"did"` 94 + Feeds []FeedRespsonse `json:"feeds"` 95 + } 96 + 97 + type FeedRespsonse struct { 98 + URI string `json:"uri"` 99 + } 100 + 101 + func (s *Server) HandleDescribeFeedGenerator(w http.ResponseWriter, r *http.Request) { 102 + slog.Info("got request for describe feed", "host", r.RemoteAddr) 103 + resp := DescribeFeedResponse{ 104 + DID: fmt.Sprintf("did:web:%s", s.feedHost), 105 + Feeds: []FeedRespsonse{ 106 + { 107 + URI: fmt.Sprintf("at://%s/app.bsky.feed.generator/wills-test", s.feedDidBase), 108 + }, 109 + }, 110 + } 111 + 112 + b, err := json.Marshal(resp) 113 + if err != nil { 114 + http.Error(w, "failed to encode resp", http.StatusInternalServerError) 115 + return 116 + } 117 + 118 + w.Write(b) 119 + } 120 + 121 + func (s *Server) HandleWellKnown(w http.ResponseWriter, r *http.Request) { 122 + slog.Info("got request for well known", "host", r.RemoteAddr) 123 + resp := WellKnownResponse{ 124 + Context: []string{"https://www.w3.org/ns/did/v1"}, 125 + Id: fmt.Sprintf("did:web:%s", s.feedHost), 126 + Service: []WellKnownService{ 127 + { 128 + Id: "#bsky_fg", 129 + Type: "BskyFeedGenerator", 130 + ServiceEndpoint: fmt.Sprintf("https://%s", s.feedHost), 131 + }, 132 + }, 133 + } 134 + 135 + b, err := json.Marshal(resp) 136 + if err != nil { 137 + http.Error(w, "failed to encode resp", http.StatusInternalServerError) 138 + return 139 + } 140 + 141 + w.Write(b) 142 + }
+242
frontend_handlers.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "io" 9 + "log/slog" 10 + "net/http" 11 + "strconv" 12 + "strings" 13 + 14 + "github.com/willdot/bskyfeedgen/frontend" 15 + "github.com/willdot/bskyfeedgen/store" 16 + ) 17 + 18 + const ( 19 + bskyBaseURL = "https://bsky.social/xrpc" 20 + ) 21 + 22 + type loginRequest struct { 23 + Handle string `json:"handle"` 24 + AppPassword string `json:"appPassword"` 25 + } 26 + 27 + type BskyAuth struct { 28 + AccessJwt string `json:"accessJwt"` 29 + Did string `json:"did"` 30 + } 31 + 32 + func (s *Server) HandleSubscriptions(w http.ResponseWriter, r *http.Request) { 33 + didCookie, err := r.Cookie(didCookieName) 34 + if err != nil { 35 + slog.Error("read DID cookie", "error", err) 36 + frontend.Login("", "").Render(r.Context(), w) 37 + return 38 + } 39 + if didCookie == nil { 40 + slog.Error("missing DID cookie") 41 + frontend.Login("", "").Render(r.Context(), w) 42 + return 43 + } 44 + 45 + usersDid := didCookie.Value 46 + 47 + slog.Info("did request", "did", usersDid) 48 + 49 + subs, err := s.feeder.GetSubscriptionsForUser(r.Context(), usersDid) 50 + if err != nil { 51 + slog.Error("error getting subscriptions for user", "error", err) 52 + frontend.Subscriptions("failed to get subscriptions", nil).Render(r.Context(), w) 53 + return 54 + } 55 + 56 + subResp := make([]store.Subscription, 0, len(subs)) 57 + for _, sub := range subs { 58 + splitStr := strings.Split(sub.SubscribedPostURI, "/") 59 + 60 + if len(splitStr) != 5 { 61 + slog.Error("subscription URI was not expected - expected to have 5 strings after spliting by /", "uri", sub.SubscribedPostURI) 62 + continue 63 + } 64 + 65 + did := splitStr[2] 66 + 67 + handle, err := resolveDid(did) 68 + if err != nil { 69 + slog.Error("resolving did", "error", err, "did", did) 70 + handle = did 71 + } 72 + 73 + slog.Info("sub id", "id", sub.ID) 74 + 75 + uri := fmt.Sprintf("https://bsky.app/profile/%s/post/%s", handle, splitStr[4]) 76 + sub.SubscribedPostURI = uri 77 + subResp = append(subResp, sub) 78 + } 79 + 80 + frontend.Subscriptions("", subResp).Render(r.Context(), w) 81 + } 82 + 83 + func (s *Server) HandleDeleteSubscription(w http.ResponseWriter, r *http.Request) { 84 + sub := r.PathValue("id") 85 + 86 + slog.Info("deleting sub", "sub", sub) 87 + 88 + didCookie, err := r.Cookie(didCookieName) 89 + if err != nil { 90 + slog.Error("read DID cookie", "error", err) 91 + frontend.Login("", "").Render(r.Context(), w) 92 + return 93 + } 94 + if didCookie == nil { 95 + slog.Error("missing DID cookie") 96 + frontend.Login("", "").Render(r.Context(), w) 97 + return 98 + } 99 + 100 + usersDid := didCookie.Value 101 + 102 + id, err := strconv.Atoi(sub) 103 + if err != nil { 104 + slog.Error("failed to convert sub ID to int", "error", err) 105 + http.Error(w, "invalid ID", http.StatusBadRequest) 106 + return 107 + } 108 + 109 + err = s.feeder.DeleteSubscriptionByIdAndUser(usersDid, id) 110 + if err != nil { 111 + slog.Error("delete subscription for user", "error", err, "subscription URI", sub) 112 + http.Error(w, "failed to delete subscription", http.StatusInternalServerError) 113 + return 114 + } 115 + 116 + w.WriteHeader(http.StatusAccepted) 117 + w.Write([]byte("{}")) 118 + } 119 + 120 + func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { 121 + b, err := io.ReadAll(r.Body) 122 + if err != nil { 123 + slog.Error("failed to read body", "error", err) 124 + frontend.LoginForm("", "bad request").Render(r.Context(), w) 125 + return 126 + } 127 + 128 + var loginReq loginRequest 129 + err = json.Unmarshal(b, &loginReq) 130 + if err != nil { 131 + slog.Error("failed to unmarshal body", "error", err) 132 + frontend.LoginForm("", "bad request").Render(r.Context(), w) 133 + return 134 + } 135 + url := fmt.Sprintf("%s/com.atproto.server.createsession", bskyBaseURL) 136 + 137 + requestData := map[string]interface{}{ 138 + "identifier": loginReq.Handle, 139 + "password": loginReq.AppPassword, 140 + } 141 + 142 + data, err := json.Marshal(requestData) 143 + if err != nil { 144 + slog.Error("failed marshal POST request to sign into Bsky", "error", err) 145 + frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 146 + return 147 + } 148 + 149 + reader := bytes.NewReader(data) 150 + 151 + req, err := http.NewRequest("POST", url, reader) 152 + if err != nil { 153 + slog.Error("failed to create POST request to sign into Bsky", "error", err) 154 + frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 155 + return 156 + } 157 + 158 + req.Header.Add("Content-Type", "application/json") 159 + 160 + // TODO: create a client somewhere 161 + res, err := http.DefaultClient.Do(req) 162 + if err != nil { 163 + slog.Error("failed to make POST request to sign into Bsky", "error", err) 164 + frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 165 + return 166 + } 167 + 168 + defer res.Body.Close() 169 + 170 + slog.Info("bsky resp", "code", res.StatusCode) 171 + 172 + if res.StatusCode != 200 { 173 + slog.Error("failed to log into bluesky", "status code", res.StatusCode) 174 + frontend.LoginForm(loginReq.Handle, "not authorized").Render(r.Context(), w) 175 + return 176 + } 177 + 178 + resBody, err := io.ReadAll(res.Body) 179 + if err != nil { 180 + slog.Error("failed read response from Bsky login", "error", err) 181 + frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 182 + return 183 + } 184 + 185 + var loginResp BskyAuth 186 + err = json.Unmarshal(resBody, &loginResp) 187 + if err != nil { 188 + slog.Error("failed unmarshal response from Bsky login", "error", err) 189 + frontend.LoginForm(loginReq.Handle, "internal error").Render(r.Context(), w) 190 + return 191 + } 192 + 193 + http.SetCookie(w, &http.Cookie{ 194 + Name: jwtCookieName, 195 + Value: loginResp.AccessJwt, 196 + }) 197 + 198 + http.SetCookie(w, &http.Cookie{ 199 + Name: didCookieName, 200 + Value: loginResp.Did, 201 + }) 202 + 203 + ctx := context.WithValue(r.Context(), frontend.ContextUsernameKey, loginReq.Handle) 204 + r = r.WithContext(ctx) 205 + 206 + http.Redirect(w, r, "/", http.StatusOK) 207 + } 208 + 209 + func resolveDid(did string) (string, error) { 210 + resp, err := http.DefaultClient.Get(fmt.Sprintf("https://plc.directory/%s", did)) 211 + if err != nil { 212 + return "", fmt.Errorf("error making request to resolve did: %w", err) 213 + } 214 + defer resp.Body.Close() 215 + 216 + if resp.StatusCode != http.StatusOK { 217 + return "", fmt.Errorf("got response %d", resp.StatusCode) 218 + } 219 + 220 + type resolvedDid struct { 221 + Aka []string `json:"alsoKnownAs"` 222 + } 223 + 224 + b, err := io.ReadAll(resp.Body) 225 + if err != nil { 226 + return "", fmt.Errorf("reading response body: %w", err) 227 + } 228 + 229 + var resolved resolvedDid 230 + err = json.Unmarshal(b, &resolved) 231 + if err != nil { 232 + return "", fmt.Errorf("decode response body: %w", err) 233 + } 234 + 235 + if len(resolved.Aka) == 0 { 236 + return "", nil 237 + } 238 + 239 + res := strings.ReplaceAll(resolved.Aka[0], "at://", "") 240 + 241 + return res, nil 242 + }