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