forked from tangled.org/core
this repo has no description
1package strings 2 3import ( 4 "fmt" 5 "log/slog" 6 "net/http" 7 "path" 8 "slices" 9 "strconv" 10 "time" 11 12 "tangled.sh/tangled.sh/core/api/tangled" 13 "tangled.sh/tangled.sh/core/appview/config" 14 "tangled.sh/tangled.sh/core/appview/db" 15 "tangled.sh/tangled.sh/core/appview/middleware" 16 "tangled.sh/tangled.sh/core/appview/oauth" 17 "tangled.sh/tangled.sh/core/appview/pages" 18 "tangled.sh/tangled.sh/core/appview/pages/markup" 19 "tangled.sh/tangled.sh/core/eventconsumer" 20 "tangled.sh/tangled.sh/core/idresolver" 21 "tangled.sh/tangled.sh/core/rbac" 22 "tangled.sh/tangled.sh/core/tid" 23 24 "github.com/bluesky-social/indigo/api/atproto" 25 "github.com/bluesky-social/indigo/atproto/identity" 26 "github.com/bluesky-social/indigo/atproto/syntax" 27 lexutil "github.com/bluesky-social/indigo/lex/util" 28 "github.com/go-chi/chi/v5" 29) 30 31type Strings struct { 32 Db *db.DB 33 OAuth *oauth.OAuth 34 Pages *pages.Pages 35 Config *config.Config 36 Enforcer *rbac.Enforcer 37 IdResolver *idresolver.Resolver 38 Logger *slog.Logger 39 Knotstream *eventconsumer.Consumer 40} 41 42func (s *Strings) Router(mw *middleware.Middleware) http.Handler { 43 r := chi.NewRouter() 44 45 r. 46 Get("/", s.timeline) 47 48 r. 49 With(mw.ResolveIdent()). 50 Route("/{user}", func(r chi.Router) { 51 r.Get("/", s.dashboard) 52 53 r.Route("/{rkey}", func(r chi.Router) { 54 r.Get("/", s.contents) 55 r.Delete("/", s.delete) 56 r.Get("/raw", s.contents) 57 r.Get("/edit", s.edit) 58 r.Post("/edit", s.edit) 59 r. 60 With(middleware.AuthMiddleware(s.OAuth)). 61 Post("/comment", s.comment) 62 }) 63 }) 64 65 r. 66 With(middleware.AuthMiddleware(s.OAuth)). 67 Route("/new", func(r chi.Router) { 68 r.Get("/", s.create) 69 r.Post("/", s.create) 70 }) 71 72 return r 73} 74 75func (s *Strings) timeline(w http.ResponseWriter, r *http.Request) { 76 l := s.Logger.With("handler", "timeline") 77 78 strings, err := db.GetStrings(s.Db, 50) 79 if err != nil { 80 l.Error("failed to fetch string", "err", err) 81 w.WriteHeader(http.StatusInternalServerError) 82 return 83 } 84 85 s.Pages.StringsTimeline(w, pages.StringTimelineParams{ 86 LoggedInUser: s.OAuth.GetUser(r), 87 Strings: strings, 88 }) 89} 90 91func (s *Strings) contents(w http.ResponseWriter, r *http.Request) { 92 l := s.Logger.With("handler", "contents") 93 94 id, ok := r.Context().Value("resolvedId").(identity.Identity) 95 if !ok { 96 l.Error("malformed middleware") 97 w.WriteHeader(http.StatusInternalServerError) 98 return 99 } 100 l = l.With("did", id.DID, "handle", id.Handle) 101 102 rkey := chi.URLParam(r, "rkey") 103 if rkey == "" { 104 l.Error("malformed url, empty rkey") 105 w.WriteHeader(http.StatusBadRequest) 106 return 107 } 108 l = l.With("rkey", rkey) 109 110 strings, err := db.GetStrings( 111 s.Db, 112 0, 113 db.FilterEq("did", id.DID), 114 db.FilterEq("rkey", rkey), 115 ) 116 if err != nil { 117 l.Error("failed to fetch string", "err", err) 118 w.WriteHeader(http.StatusInternalServerError) 119 return 120 } 121 if len(strings) < 1 { 122 l.Error("string not found") 123 s.Pages.Error404(w) 124 return 125 } 126 if len(strings) != 1 { 127 l.Error("incorrect number of records returned", "len(strings)", len(strings)) 128 w.WriteHeader(http.StatusInternalServerError) 129 return 130 } 131 string := strings[0] 132 133 if path.Base(r.URL.Path) == "raw" { 134 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 135 if string.Filename != "" { 136 w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", string.Filename)) 137 } 138 w.Header().Set("Content-Length", strconv.Itoa(len(string.Contents))) 139 140 _, err = w.Write([]byte(string.Contents)) 141 if err != nil { 142 l.Error("failed to write raw response", "err", err) 143 } 144 return 145 } 146 147 var showRendered, renderToggle bool 148 if markup.GetFormat(string.Filename) == markup.FormatMarkdown { 149 renderToggle = true 150 showRendered = r.URL.Query().Get("code") != "true" 151 } 152 153 s.Pages.SingleString(w, pages.SingleStringParams{ 154 LoggedInUser: s.OAuth.GetUser(r), 155 RenderToggle: renderToggle, 156 ShowRendered: showRendered, 157 String: string, 158 Stats: string.Stats(), 159 Owner: id, 160 }) 161} 162 163func (s *Strings) dashboard(w http.ResponseWriter, r *http.Request) { 164 l := s.Logger.With("handler", "dashboard") 165 166 id, ok := r.Context().Value("resolvedId").(identity.Identity) 167 if !ok { 168 l.Error("malformed middleware") 169 w.WriteHeader(http.StatusInternalServerError) 170 return 171 } 172 l = l.With("did", id.DID, "handle", id.Handle) 173 174 all, err := db.GetStrings( 175 s.Db, 176 0, 177 db.FilterEq("did", id.DID), 178 ) 179 if err != nil { 180 l.Error("failed to fetch strings", "err", err) 181 w.WriteHeader(http.StatusInternalServerError) 182 return 183 } 184 185 slices.SortFunc(all, func(a, b db.String) int { 186 if a.Created.After(b.Created) { 187 return -1 188 } else { 189 return 1 190 } 191 }) 192 193 profile, err := db.GetProfile(s.Db, id.DID.String()) 194 if err != nil { 195 l.Error("failed to fetch user profile", "err", err) 196 w.WriteHeader(http.StatusInternalServerError) 197 return 198 } 199 loggedInUser := s.OAuth.GetUser(r) 200 followStatus := db.IsNotFollowing 201 if loggedInUser != nil { 202 followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String()) 203 } 204 205 followStats, err := db.GetFollowerFollowingCount(s.Db, id.DID.String()) 206 if err != nil { 207 l.Error("failed to get follow stats", "err", err) 208 } 209 210 s.Pages.StringsDashboard(w, pages.StringsDashboardParams{ 211 LoggedInUser: s.OAuth.GetUser(r), 212 Card: pages.ProfileCard{ 213 UserDid: id.DID.String(), 214 UserHandle: id.Handle.String(), 215 Profile: profile, 216 FollowStatus: followStatus, 217 FollowersCount: followStats.Followers, 218 FollowingCount: followStats.Following, 219 }, 220 Strings: all, 221 }) 222} 223 224func (s *Strings) edit(w http.ResponseWriter, r *http.Request) { 225 l := s.Logger.With("handler", "edit") 226 227 user := s.OAuth.GetUser(r) 228 229 id, ok := r.Context().Value("resolvedId").(identity.Identity) 230 if !ok { 231 l.Error("malformed middleware") 232 w.WriteHeader(http.StatusInternalServerError) 233 return 234 } 235 l = l.With("did", id.DID, "handle", id.Handle) 236 237 rkey := chi.URLParam(r, "rkey") 238 if rkey == "" { 239 l.Error("malformed url, empty rkey") 240 w.WriteHeader(http.StatusBadRequest) 241 return 242 } 243 l = l.With("rkey", rkey) 244 245 // get the string currently being edited 246 all, err := db.GetStrings( 247 s.Db, 248 0, 249 db.FilterEq("did", id.DID), 250 db.FilterEq("rkey", rkey), 251 ) 252 if err != nil { 253 l.Error("failed to fetch string", "err", err) 254 w.WriteHeader(http.StatusInternalServerError) 255 return 256 } 257 if len(all) != 1 { 258 l.Error("incorrect number of records returned", "len(strings)", len(all)) 259 w.WriteHeader(http.StatusInternalServerError) 260 return 261 } 262 first := all[0] 263 264 // verify that the logged in user owns this string 265 if user.Did != id.DID.String() { 266 l.Error("unauthorized request", "expected", id.DID, "got", user.Did) 267 w.WriteHeader(http.StatusUnauthorized) 268 return 269 } 270 271 switch r.Method { 272 case http.MethodGet: 273 // return the form with prefilled fields 274 s.Pages.PutString(w, pages.PutStringParams{ 275 LoggedInUser: s.OAuth.GetUser(r), 276 Action: "edit", 277 String: first, 278 }) 279 case http.MethodPost: 280 fail := func(msg string, err error) { 281 l.Error(msg, "err", err) 282 s.Pages.Notice(w, "error", msg) 283 } 284 285 filename := r.FormValue("filename") 286 if filename == "" { 287 fail("Empty filename.", nil) 288 return 289 } 290 291 content := r.FormValue("content") 292 if content == "" { 293 fail("Empty contents.", nil) 294 return 295 } 296 297 description := r.FormValue("description") 298 299 // construct new string from form values 300 entry := db.String{ 301 Did: first.Did, 302 Rkey: first.Rkey, 303 Filename: filename, 304 Description: description, 305 Contents: content, 306 Created: first.Created, 307 } 308 309 record := entry.AsRecord() 310 311 client, err := s.OAuth.AuthorizedClient(r) 312 if err != nil { 313 fail("Failed to create record.", err) 314 return 315 } 316 317 // first replace the existing record in the PDS 318 ex, err := client.RepoGetRecord(r.Context(), "", tangled.StringNSID, entry.Did.String(), entry.Rkey) 319 if err != nil { 320 fail("Failed to updated existing record.", err) 321 return 322 } 323 resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{ 324 Collection: tangled.StringNSID, 325 Repo: entry.Did.String(), 326 Rkey: entry.Rkey, 327 SwapRecord: ex.Cid, 328 Record: &lexutil.LexiconTypeDecoder{ 329 Val: &record, 330 }, 331 }) 332 if err != nil { 333 fail("Failed to updated existing record.", err) 334 return 335 } 336 l := l.With("aturi", resp.Uri) 337 l.Info("edited string") 338 339 // if that went okay, updated the db 340 if err = db.AddString(s.Db, entry); err != nil { 341 fail("Failed to update string.", err) 342 return 343 } 344 345 // if that went okay, redir to the string 346 s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+entry.Rkey) 347 } 348 349} 350 351func (s *Strings) create(w http.ResponseWriter, r *http.Request) { 352 l := s.Logger.With("handler", "create") 353 user := s.OAuth.GetUser(r) 354 355 switch r.Method { 356 case http.MethodGet: 357 s.Pages.PutString(w, pages.PutStringParams{ 358 LoggedInUser: s.OAuth.GetUser(r), 359 Action: "new", 360 }) 361 case http.MethodPost: 362 fail := func(msg string, err error) { 363 l.Error(msg, "err", err) 364 s.Pages.Notice(w, "error", msg) 365 } 366 367 filename := r.FormValue("filename") 368 if filename == "" { 369 fail("Empty filename.", nil) 370 return 371 } 372 373 content := r.FormValue("content") 374 if content == "" { 375 fail("Empty contents.", nil) 376 return 377 } 378 379 description := r.FormValue("description") 380 381 string := db.String{ 382 Did: syntax.DID(user.Did), 383 Rkey: tid.TID(), 384 Filename: filename, 385 Description: description, 386 Contents: content, 387 Created: time.Now(), 388 } 389 390 record := string.AsRecord() 391 392 client, err := s.OAuth.AuthorizedClient(r) 393 if err != nil { 394 fail("Failed to create record.", err) 395 return 396 } 397 398 resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{ 399 Collection: tangled.StringNSID, 400 Repo: user.Did, 401 Rkey: string.Rkey, 402 Record: &lexutil.LexiconTypeDecoder{ 403 Val: &record, 404 }, 405 }) 406 if err != nil { 407 fail("Failed to create record.", err) 408 return 409 } 410 l := l.With("aturi", resp.Uri) 411 l.Info("created record") 412 413 // insert into DB 414 if err = db.AddString(s.Db, string); err != nil { 415 fail("Failed to create string.", err) 416 return 417 } 418 419 // successful 420 s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+string.Rkey) 421 } 422} 423 424func (s *Strings) delete(w http.ResponseWriter, r *http.Request) { 425 l := s.Logger.With("handler", "create") 426 user := s.OAuth.GetUser(r) 427 fail := func(msg string, err error) { 428 l.Error(msg, "err", err) 429 s.Pages.Notice(w, "error", msg) 430 } 431 432 id, ok := r.Context().Value("resolvedId").(identity.Identity) 433 if !ok { 434 l.Error("malformed middleware") 435 w.WriteHeader(http.StatusInternalServerError) 436 return 437 } 438 l = l.With("did", id.DID, "handle", id.Handle) 439 440 rkey := chi.URLParam(r, "rkey") 441 if rkey == "" { 442 l.Error("malformed url, empty rkey") 443 w.WriteHeader(http.StatusBadRequest) 444 return 445 } 446 447 if user.Did != id.DID.String() { 448 fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 449 return 450 } 451 452 if err := db.DeleteString( 453 s.Db, 454 db.FilterEq("did", user.Did), 455 db.FilterEq("rkey", rkey), 456 ); err != nil { 457 fail("Failed to delete string.", err) 458 return 459 } 460 461 s.Pages.HxRedirect(w, "/strings/"+user.Handle) 462} 463 464func (s *Strings) comment(w http.ResponseWriter, r *http.Request) { 465}