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