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.Default() 77 78 router.GET("/.well-known/did.json", GetWellKnownDID) 79 80 auther, err := auth.NewAuth( 81 100_000, 82 time.Hour*12, 83 5, 84 serviceWebDID, 85 ) 86 if err != nil { 87 log.Fatalf("Failed to create Auth: %v", err) 88 } 89 90 router.Use(auther.AuthenticateGinRequestViaJWT) 91 router.Use(cors.New(cors.Config{ 92 AllowAllOrigins: true, 93 AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, 94 AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, 95 ExposeHeaders: []string{"Content-Length"}, 96 AllowCredentials: true, 97 MaxAge: 12 * 3600, 98 })) 99 100 responsewow, err := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.actor.profile", "did:web:did12.whey.party", "self") 101 if err != nil { 102 log.Println(err) 103 } 104 105 log.Println(responsewow.Uri) 106 107 var didtest *utils.DID 108 didval, errdid := utils.NewDID("did:web:did12.whey.party") 109 if errdid != nil { 110 didtest = nil 111 } else { 112 didtest = &didval 113 } 114 profiletest, _, _ := appbskyactordefs.ProfileViewBasic(ctx, *didtest, sl, BSKYIMAGECDN_URL) 115 116 log.Println(*profiletest.DisplayName) 117 log.Println(*profiletest.Avatar) 118 119 router.GET("/ws", func(c *gin.Context) { 120 mailbox.HandleWS(c.Writer, c.Request) 121 }) 122 123 kv := store.NewKV() 124 125 router.PUT("/xrpc/app.bsky.actor.putPreferences", func(c *gin.Context) { 126 userDID := c.GetString("user_did") 127 body, err := io.ReadAll(c.Request.Body) 128 if err != nil { 129 c.JSON(400, gin.H{"error": "invalid body"}) 130 return 131 } 132 133 kv.Set(userDID, body) 134 c.Status(200) 135 136 }) 137 138 router.GET("/xrpc/app.bsky.actor.getPreferences", func(c *gin.Context) { 139 userDID := c.GetString("user_did") 140 val, ok := kv.Get(userDID) 141 if !ok { 142 c.JSON(200, gin.H{"preferences": []any{}}) 143 return 144 } 145 146 c.Data(200, "application/json", val) 147 148 }) 149 150 bskyappdid, _ := utils.NewDID("did:plc:z72i7hdynmk6r22z27h6tvur") 151 152 profiletest2, _, _ := appbskyactordefs.ProfileViewDetailed(ctx, bskyappdid, sl, cs, BSKYIMAGECDN_URL) 153 154 data, err := json.MarshalIndent(profiletest2, "", " ") 155 if err != nil { 156 panic(err) 157 } 158 fmt.Println(string(data)) 159 160 router.GET("/", func(c *gin.Context) { 161 log.Println("hello worldio !") 162 clientUUID := sticket.GetUUIDFromRequest(c.Request) 163 hasSticket := clientUUID != "" 164 if hasSticket { 165 go func(targetUUID string) { 166 // simulated heavy processing 167 time.Sleep(2 * time.Second) 168 169 lateData := map[string]any{ 170 "postId": 101, 171 "newComments": []string{ 172 "Wow great tutorial!", 173 "I am stuck on step 1.", 174 }, 175 } 176 177 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 178 if success { 179 log.Println("Successfully sent late data via Sticket") 180 } else { 181 log.Println("Failed to send late data (client disconnected?)") 182 } 183 }(clientUUID) 184 } 185 }) 186 router.Run(":7152") 187} 188 189func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 190 log.Println("hello worldio !") 191} 192 193type DidResponse struct { 194 Context []string `json:"@context"` 195 ID string `json:"id"` 196 Service []did.Service `json:"service"` 197} 198 199/* 200 { 201 id: "#bsky_appview", 202 type: "BskyAppView", 203 serviceEndpoint: endpoint, 204 }, 205*/ 206func GetWellKnownDID(c *gin.Context) { 207 // Use a custom struct to fix missing omitempty on did.Document 208 serviceEndpoint := serviceWebHost 209 serviceDID, err := did.ParseDID(serviceWebDID) 210 if err != nil { 211 log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 212 return 213 } 214 serviceID, err := did.ParseDID("#bsky_fg") 215 if err != nil { 216 panic(err) 217 } 218 didDoc := did.Document{ 219 Context: []string{did.CtxDIDv1}, 220 ID: serviceDID, 221 Service: []did.Service{ 222 { 223 ID: serviceID, 224 Type: "BskyAppView", 225 ServiceEndpoint: serviceEndpoint, 226 }, 227 }, 228 } 229 didResponse := DidResponse{ 230 Context: didDoc.Context, 231 ID: didDoc.ID.String(), 232 Service: didDoc.Service, 233 } 234 c.JSON(http.StatusOK, didResponse) 235}