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("/xrpc/app.bsky.labeler.getServices", 203 func(c *gin.Context) { 204 dids := c.QueryArray("dids") 205 206 labelers := make([]*appbsky.LabelerGetServices_Output_Views_Elem, 0, len(dids)) 207 //profiles := make([]*appbsky.ActorDefs_ProfileViewDetailed, 0, len(dids)) 208 209 for _, v := range dids { 210 did, err := utils.NewDID(v) 211 if err != nil { 212 continue 213 } 214 labelerprofile, _, _ := appbskyactordefs.ProfileView(ctx, did, sl, BSKYIMAGECDN_URL) 215 labelerserviceresponse, _ := agnostic.RepoGetRecord(ctx, sl, "", "app.bsky.labeler.service", string(did), "self") 216 var labelerservice appbsky.LabelerService 217 if err := json.Unmarshal(*labelerserviceresponse.Value, &labelerservice); err != nil { 218 continue 219 } 220 221 a := "account" 222 b := "record" 223 c := "chat" 224 225 placeholderTypes := []*string{&a, &b, &c} 226 227 labeler := &appbsky.LabelerGetServices_Output_Views_Elem{ 228 LabelerDefs_LabelerView: &appbsky.LabelerDefs_LabelerView{ 229 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerView"` 230 LexiconTypeID: "app.bsky.labeler.defs#labelerView", 231 // Cid string `json:"cid" cborgen:"cid"` 232 Cid: *labelerserviceresponse.Cid, 233 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 234 Creator: labelerprofile, 235 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 236 IndexedAt: labelerservice.CreatedAt, 237 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 238 Labels: nil, // seems to always be empty? 239 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 240 LikeCount: nil, // placeholder sorry 241 // Uri string `json:"uri" cborgen:"uri"` 242 Uri: labelerserviceresponse.Uri, 243 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 244 Viewer: nil, 245 }, 246 LabelerDefs_LabelerViewDetailed: &appbsky.LabelerDefs_LabelerViewDetailed{ 247 // LexiconTypeID string `json:"$type" cborgen:"$type,const=app.bsky.labeler.defs#labelerViewDetailed"` 248 LexiconTypeID: "app.bsky.labeler.defs#labelerViewDetailed", 249 // Cid string `json:"cid" cborgen:"cid"` 250 Cid: *labelerserviceresponse.Cid, 251 // Creator *ActorDefs_ProfileView `json:"creator" cborgen:"creator"` 252 Creator: labelerprofile, 253 // IndexedAt string `json:"indexedAt" cborgen:"indexedAt"` 254 IndexedAt: labelerservice.CreatedAt, 255 // Labels []*comatproto.LabelDefs_Label `json:"labels,omitempty" cborgen:"labels,omitempty"` 256 Labels: nil, // seems to always be empty? 257 // LikeCount *int64 `json:"likeCount,omitempty" cborgen:"likeCount,omitempty"` 258 LikeCount: nil, // placeholder sorry 259 // Policies *LabelerDefs_LabelerPolicies `json:"policies" cborgen:"policies"` 260 Policies: labelerservice.Policies, 261 // // reasonTypes: The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed. 262 // ReasonTypes []*string `json:"reasonTypes,omitempty" cborgen:"reasonTypes,omitempty"` 263 ReasonTypes: nil, //usually not even present 264 // // subjectCollections: Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type. 265 // SubjectCollections []string `json:"subjectCollections,omitempty" cborgen:"subjectCollections,omitempty"` 266 SubjectCollections: nil, //usually not even present 267 // // subjectTypes: The set of subject types (account, record, etc) this service accepts reports on. 268 // SubjectTypes []*string `json:"subjectTypes,omitempty" cborgen:"subjectTypes,omitempty"` 269 SubjectTypes: placeholderTypes, 270 // Uri string `json:"uri" cborgen:"uri"` 271 Uri: labelerserviceresponse.Uri, 272 // Viewer *LabelerDefs_LabelerViewerState `json:"viewer,omitempty" cborgen:"viewer,omitempty"` 273 Viewer: nil, 274 }, 275 } 276 labelers = append(labelers, labeler) 277 } 278 279 c.JSON(http.StatusOK, &appbsky.LabelerGetServices_Output{ 280 Views: labelers, 281 }) 282 }) 283 284 router.GET("/", func(c *gin.Context) { 285 log.Println("hello worldio !") 286 clientUUID := sticket.GetUUIDFromRequest(c.Request) 287 hasSticket := clientUUID != "" 288 if hasSticket { 289 go func(targetUUID string) { 290 // simulated heavy processing 291 time.Sleep(2 * time.Second) 292 293 lateData := map[string]any{ 294 "postId": 101, 295 "newComments": []string{ 296 "Wow great tutorial!", 297 "I am stuck on step 1.", 298 }, 299 } 300 301 success := mailbox.SendToClient(targetUUID, "post_thread_update", lateData) 302 if success { 303 log.Println("Successfully sent late data via Sticket") 304 } else { 305 log.Println("Failed to send late data (client disconnected?)") 306 } 307 }(clientUUID) 308 } 309 }) 310 router.Run(":7152") 311} 312 313func getPostThreadV2(w http.ResponseWriter, r *http.Request) { 314 log.Println("hello worldio !") 315} 316 317type DidResponse struct { 318 Context []string `json:"@context"` 319 ID string `json:"id"` 320 Service []did.Service `json:"service"` 321} 322 323/* 324 { 325 id: "#bsky_appview", 326 type: "BskyAppView", 327 serviceEndpoint: endpoint, 328 }, 329*/ 330func GetWellKnownDID(c *gin.Context) { 331 // Use a custom struct to fix missing omitempty on did.Document 332 serviceEndpoint := serviceWebHost 333 serviceDID, err := did.ParseDID(serviceWebDID) 334 if err != nil { 335 log.Println(fmt.Errorf("error parsing serviceDID: %w", err)) 336 return 337 } 338 serviceID, err := did.ParseDID("#bsky_appview") 339 if err != nil { 340 panic(err) 341 } 342 didDoc := did.Document{ 343 Context: []string{did.CtxDIDv1}, 344 ID: serviceDID, 345 Service: []did.Service{ 346 { 347 ID: serviceID, 348 Type: "BskyAppView", 349 ServiceEndpoint: serviceEndpoint, 350 }, 351 }, 352 } 353 didResponse := DidResponse{ 354 Context: didDoc.Context, 355 ID: didDoc.ID.String(), 356 Service: didDoc.Service, 357 } 358 c.JSON(http.StatusOK, didResponse) 359}