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