bluesky appview implementation using microcosm and other services server.reddwarf.app
appview bluesky reddwarf microcosm
1package main 2 3import ( 4 "context" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "io" 9 "log" 10 "net/http" 11 "time" 12 13 did "github.com/whyrusleeping/go-did" 14 "tangled.org/whey.party/red-dwarf-server/auth" 15 "tangled.org/whey.party/red-dwarf-server/microcosm/constellation" 16 "tangled.org/whey.party/red-dwarf-server/microcosm/slingshot" 17 appbskyactordefs "tangled.org/whey.party/red-dwarf-server/shims/lex/app/bsky/actor/defs" 18 "tangled.org/whey.party/red-dwarf-server/shims/utils" 19 "tangled.org/whey.party/red-dwarf-server/sticket" 20 "tangled.org/whey.party/red-dwarf-server/store" 21 22 // "github.com/bluesky-social/indigo/atproto/atclient" 23 // comatproto "github.com/bluesky-social/indigo/api/atproto" 24 appbsky "github.com/bluesky-social/indigo/api/bsky" 25 // "github.com/bluesky-social/indigo/atproto/atclient" 26 // "github.com/bluesky-social/indigo/atproto/identity" 27 // "github.com/bluesky-social/indigo/atproto/syntax" 28 "github.com/bluesky-social/indigo/api/agnostic" 29 "github.com/gin-contrib/cors" 30 "github.com/gin-gonic/gin" 31 // "github.com/bluesky-social/jetstream/pkg/models" 32) 33 34var ( 35 JETSTREAM_URL string 36 SPACEDUST_URL string 37 SLINGSHOT_URL string 38 CONSTELLATION_URL string 39) 40 41func initURLs(prod bool) { 42 if !prod { 43 JETSTREAM_URL = "wss://jetstream.whey.party/subscribe" 44 SPACEDUST_URL = "wss://spacedust.whey.party/subscribe" 45 SLINGSHOT_URL = "https://slingshot.whey.party" 46 CONSTELLATION_URL = "https://constellation.whey.party" 47 } else { 48 JETSTREAM_URL = "ws://localhost:6008/subscribe" 49 SPACEDUST_URL = "ws://localhost:9998/subscribe" 50 SLINGSHOT_URL = "http://localhost:7729" 51 CONSTELLATION_URL = "http://localhost:7728" 52 } 53} 54 55const ( 56 BSKYIMAGECDN_URL = "https://cdn.bsky.app" 57 BSKYVIDEOCDN_URL = "https://video.bsky.app" 58 serviceWebDID = "did:web:server.reddwarf.app" 59 serviceWebHost = "https://server.reddwarf.app" 60) 61 62func main() { 63 log.Println("red-dwarf-server started") 64 prod := flag.Bool("prod", false, "use production URLs instead of localhost") 65 flag.Parse() 66 67 initURLs(*prod) 68 69 ctx := context.Background() 70 mailbox := sticket.New() 71 sl := slingshot.NewSlingshot(SLINGSHOT_URL) 72 cs := constellation.NewConstellation(CONSTELLATION_URL) 73 // spacedust is type definitions only 74 // jetstream types is probably available from jetstream/pkg/models 75 76 router := gin.New() 77 router.Use(gin.Logger()) 78 router.Use(gin.Recovery()) 79 router.Use(cors.Default()) 80 81 router.GET("/.well-known/did.json", GetWellKnownDID) 82 83 auther, err := auth.NewAuth( 84 100_000, 85 time.Hour*12, 86 5, 87 serviceWebDID, //+"#bsky_appview", 88 ) 89 if err != nil { 90 log.Fatalf("Failed to create Auth: %v", err) 91 } 92 93 router.Use(auther.AuthenticateGinRequestViaJWT) 94 95 responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self") 96 if err != nil { 97 log.Println(err) 98 } 99 100 log.Println(responsewow.Uri) 101 102 var didtest *utils.DID 103 didval, errdid := utils.NewDID("did:web:did12.whey.party") 104 if errdid != nil { 105 didtest = nil 106 } else { 107 didtest = &didval 108 } 109 profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, BSKYIMAGECDN_URL) 110 111 log.Println(*profiletest.DisplayName) 112 log.Println(*profiletest.Avatar) 113 114 router.GET("/ws", func(c *gin.Context) { 115 mailbox.HandleWS(c.Writer, c.Request) 116 }) 117 118 kv := store.NewKV() 119 120 // sad attempt to get putpref working. tldr it wont work without a client fork 121 // https://bsky.app/profile/did:web:did12.whey.party/post/3m75xtomd722n 122 router.GET("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 123 c.Status(200) 124 }) 125 router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 126 c.Status(200) 127 }) 128 router.POST("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 129 c.Status(200) 130 131 userDID := c.GetString("user_did") 132 body, err := io.ReadAll(c.Request.Body) 133 if err != nil { 134 c.JSON(400, gin.H{"error": "invalid body"}) 135 return 136 } 137 138 kv.Set(userDID, body) 139 140 }) 141 142 router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) { 143 userDID := c.GetString("user_did") 144 val, ok := kv.Get(userDID) 145 if !ok { 146 c.JSON(200, gin.H{"preferences": []any{}}) 147 return 148 } 149 150 c.Data(200, "application/json", val) 151 152 }) 153 154 bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur") 155 156 profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL) 157 158 data, err := json.MarshalIndent(profiletest2, "", " ") 159 if err != nil { 160 panic(err) 161 } 162 fmt.Println(string(data)) 163 164 router.GET("/xrpc/app.bsky.actor.getProfiles", 165 func(c *gin.Context) { 166 actors := c.QueryArray("actors") 167 168 profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(actors)) 169 170 for _, v := range actors { 171 did, err := utils.NewDID(v) 172 if err != nil { 173 continue 174 } 175 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL) 176 profiles = append(profiles, profile) 177 } 178 179 c.JSON(http.StatusOK, &appbsky.ActorGetProfiles_Output{ 180 Profiles: profiles, 181 }) 182 }) 183 184 router.GET("/xrpc/app.bsky.actor.getProfile", 185 func(c *gin.Context) { 186 actor := c.Query("actor") 187 did, err := utils.NewDID(actor) 188 if err != nil { 189 c.JSON(http.StatusBadRequest, nil) 190 return 191 } 192 profile, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, did, sl, cs, BSKYIMAGECDN_URL) 193 c.JSON(http.StatusOK, profile) 194 }) 195 196 // really bad actually 197 router.GET("/xrpc/app.bsky.notification.listNotifications", 198 func(c *gin.Context) { 199 c.JSON(http.StatusOK, nil) 200 }) 201 202 router.GET("/", func(c *gin.Context) { 203 log.Println("hello worldio !") 204 clientUUID := sticket.GetUUIDFromRequest(c.Request) 205 hasSticket := clientUUID != "" 206 if hasSticket { 207 go func(targetUUID string) { 208 // simulated heavy processing 209 time.Sleep(2 * time.Second) 210 211 lateData := map[string]any{ 212 "postId": 101, 213 "newComments": []string{ 214 "Wow great tutorial!", 215 "I am stuck on step 1.", 216 }, 217 } 218 219 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 220 if success { 221 log.Println("Successfully sent late data via Sticket") 222 } else { 223 log.Println("Failed to send late data (client disconnected?)") 224 } 225 }(clientUUID) 226 } 227 }) 228 router.Run(":7152") 229} 230 231func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 232 log.Println("hello worldio !") 233} 234 235type DidResponse struct { 236 Context []string `json:"@context"` 237 ID string `json:"id"` 238 Service []did.Service `json:"service"` 239} 240 241/* 242 { 243 id: "#bsky_appview", 244 type: "BskyAppView", 245 serviceEndpoint: endpoint, 246 }, 247*/ 248func GetWellKnownDID(c *gin.Context) { 249 // Use a custom struct to fix missing omitempty on did.Document 250 serviceEndpoint := serviceWebHost 251 serviceDID, err := did.ParseDID(serviceWebDID) 252 if err != nil { 253 log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 254 return 255 } 256 serviceID, err := did.ParseDID("#bsky_appview") 257 if err != nil { 258 panic(err) 259 } 260 didDoc := did.Document{ 261 Context: []string{did.CtxDIDv1}, 262 ID: serviceDID, 263 Service: []did.Service{ 264 { 265 ID: serviceID, 266 Type: "BskyAppView", 267 ServiceEndpoint: serviceEndpoint, 268 }, 269 }, 270 } 271 didResponse := DidResponse{ 272 Context: didDoc.Context, 273 ID: didDoc.ID.String(), 274 Service: didDoc.Service, 275 } 276 c.JSON(http.StatusOK, didResponse) 277}