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}