Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Go
at test-pr-link
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413package strings
import ( "fmt" "log/slog" "net/http" "path" "strconv" "time"
"tangled.org/core/api/tangled" "tangled.org/core/appview/db" "tangled.org/core/appview/middleware" "tangled.org/core/appview/models" "tangled.org/core/appview/notify" "tangled.org/core/appview/oauth" "tangled.org/core/appview/pages" "tangled.org/core/appview/pages/markup" "tangled.org/core/idresolver" "tangled.org/core/tid"
"github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5"
comatproto "github.com/bluesky-social/indigo/api/atproto" lexutil "github.com/bluesky-social/indigo/lex/util")
type Strings struct { Db *db.DB OAuth *oauth.OAuth Pages *pages.Pages IdResolver *idresolver.Resolver Logger *slog.Logger Notifier notify.Notifier}
func (s *Strings) Router(mw *middleware.Middleware) http.Handler { r := chi.NewRouter()
r. Get("/", s.timeline)
r. With(mw.ResolveIdent()). Route("/{user}", func(r chi.Router) { r.Get("/", s.dashboard)
r.Route("/{rkey}", func(r chi.Router) { r.Get("/", s.contents) r.Delete("/", s.delete) r.Get("/raw", s.contents) r.Get("/edit", s.edit) r.Post("/edit", s.edit) r. With(middleware.AuthMiddleware(s.OAuth)). Post("/comment", s.comment) }) })
r. With(middleware.AuthMiddleware(s.OAuth)). Route("/new", func(r chi.Router) { r.Get("/", s.create) r.Post("/", s.create) })
return r}
func (s *Strings) timeline(w http.ResponseWriter, r *http.Request) { l := s.Logger.With("handler", "timeline")
strings, err := db.GetStrings(s.Db, 50) if err != nil { l.Error("failed to fetch string", "err", err) w.WriteHeader(http.StatusInternalServerError) return }
s.Pages.StringsTimeline(w, pages.StringTimelineParams{ LoggedInUser: s.OAuth.GetUser(r), Strings: strings, })}
func (s *Strings) contents(w http.ResponseWriter, r *http.Request) { l := s.Logger.With("handler", "contents")
id, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { l.Error("malformed middleware") w.WriteHeader(http.StatusInternalServerError) return } l = l.With("did", id.DID, "handle", id.Handle)
rkey := chi.URLParam(r, "rkey") if rkey == "" { l.Error("malformed url, empty rkey") w.WriteHeader(http.StatusBadRequest) return } l = l.With("rkey", rkey)
strings, err := db.GetStrings( s.Db, 0, db.FilterEq("did", id.DID), db.FilterEq("rkey", rkey), ) if err != nil { l.Error("failed to fetch string", "err", err) w.WriteHeader(http.StatusInternalServerError) return } if len(strings) < 1 { l.Error("string not found") s.Pages.Error404(w) return } if len(strings) != 1 { l.Error("incorrect number of records returned", "len(strings)", len(strings)) w.WriteHeader(http.StatusInternalServerError) return } string := strings[0]
if path.Base(r.URL.Path) == "raw" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") if string.Filename != "" { w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", string.Filename)) } w.Header().Set("Content-Length", strconv.Itoa(len(string.Contents)))
_, err = w.Write([]byte(string.Contents)) if err != nil { l.Error("failed to write raw response", "err", err) } return }
var showRendered, renderToggle bool if markup.GetFormat(string.Filename) == markup.FormatMarkdown { renderToggle = true showRendered = r.URL.Query().Get("code") != "true" }
s.Pages.SingleString(w, pages.SingleStringParams{ LoggedInUser: s.OAuth.GetUser(r), RenderToggle: renderToggle, ShowRendered: showRendered, String: string, Stats: string.Stats(), Owner: id, })}
func (s *Strings) dashboard(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, fmt.Sprintf("/%s?tab=strings", chi.URLParam(r, "user")), http.StatusFound)}
func (s *Strings) edit(w http.ResponseWriter, r *http.Request) { l := s.Logger.With("handler", "edit")
user := s.OAuth.GetUser(r)
id, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { l.Error("malformed middleware") w.WriteHeader(http.StatusInternalServerError) return } l = l.With("did", id.DID, "handle", id.Handle)
rkey := chi.URLParam(r, "rkey") if rkey == "" { l.Error("malformed url, empty rkey") w.WriteHeader(http.StatusBadRequest) return } l = l.With("rkey", rkey)
// get the string currently being edited all, err := db.GetStrings( s.Db, 0, db.FilterEq("did", id.DID), db.FilterEq("rkey", rkey), ) if err != nil { l.Error("failed to fetch string", "err", err) w.WriteHeader(http.StatusInternalServerError) return } if len(all) != 1 { l.Error("incorrect number of records returned", "len(strings)", len(all)) w.WriteHeader(http.StatusInternalServerError) return } first := all[0]
// verify that the logged in user owns this string if user.Did != id.DID.String() { l.Error("unauthorized request", "expected", id.DID, "got", user.Did) w.WriteHeader(http.StatusUnauthorized) return }
switch r.Method { case http.MethodGet: // return the form with prefilled fields s.Pages.PutString(w, pages.PutStringParams{ LoggedInUser: s.OAuth.GetUser(r), Action: "edit", String: first, }) case http.MethodPost: fail := func(msg string, err error) { l.Error(msg, "err", err) s.Pages.Notice(w, "error", msg) }
filename := r.FormValue("filename") if filename == "" { fail("Empty filename.", nil) return }
content := r.FormValue("content") if content == "" { fail("Empty contents.", nil) return }
description := r.FormValue("description")
// construct new string from form values entry := models.String{ Did: first.Did, Rkey: first.Rkey, Filename: filename, Description: description, Contents: content, Created: first.Created, }
record := entry.AsRecord()
client, err := s.OAuth.AuthorizedClient(r) if err != nil { fail("Failed to create record.", err) return }
// first replace the existing record in the PDS ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.StringNSID, entry.Did.String(), entry.Rkey) if err != nil { fail("Failed to updated existing record.", err) return } resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ Collection: tangled.StringNSID, Repo: entry.Did.String(), Rkey: entry.Rkey, SwapRecord: ex.Cid, Record: &lexutil.LexiconTypeDecoder{ Val: &record, }, }) if err != nil { fail("Failed to updated existing record.", err) return } l := l.With("aturi", resp.Uri) l.Info("edited string")
// if that went okay, updated the db if err = db.AddString(s.Db, entry); err != nil { fail("Failed to update string.", err) return }
s.Notifier.EditString(r.Context(), &entry)
// if that went okay, redir to the string s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey) }
}
func (s *Strings) create(w http.ResponseWriter, r *http.Request) { l := s.Logger.With("handler", "create") user := s.OAuth.GetUser(r)
switch r.Method { case http.MethodGet: s.Pages.PutString(w, pages.PutStringParams{ LoggedInUser: s.OAuth.GetUser(r), Action: "new", }) case http.MethodPost: fail := func(msg string, err error) { l.Error(msg, "err", err) s.Pages.Notice(w, "error", msg) }
filename := r.FormValue("filename") if filename == "" { fail("Empty filename.", nil) return }
content := r.FormValue("content") if content == "" { fail("Empty contents.", nil) return }
description := r.FormValue("description")
string := models.String{ Did: syntax.DID(user.Did), Rkey: tid.TID(), Filename: filename, Description: description, Contents: content, Created: time.Now(), }
record := string.AsRecord()
client, err := s.OAuth.AuthorizedClient(r) if err != nil { fail("Failed to create record.", err) return }
resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ Collection: tangled.StringNSID, Repo: user.Did, Rkey: string.Rkey, Record: &lexutil.LexiconTypeDecoder{ Val: &record, }, }) if err != nil { fail("Failed to create record.", err) return } l := l.With("aturi", resp.Uri) l.Info("created record")
// insert into DB if err = db.AddString(s.Db, string); err != nil { fail("Failed to create string.", err) return }
s.Notifier.NewString(r.Context(), &string)
// successful s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey) }}
func (s *Strings) delete(w http.ResponseWriter, r *http.Request) { l := s.Logger.With("handler", "create") user := s.OAuth.GetUser(r) fail := func(msg string, err error) { l.Error(msg, "err", err) s.Pages.Notice(w, "error", msg) }
id, ok := r.Context().Value("resolvedId").(identity.Identity) if !ok { l.Error("malformed middleware") w.WriteHeader(http.StatusInternalServerError) return } l = l.With("did", id.DID, "handle", id.Handle)
rkey := chi.URLParam(r, "rkey") if rkey == "" { l.Error("malformed url, empty rkey") w.WriteHeader(http.StatusBadRequest) return }
if user.Did != id.DID.String() { fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) return }
if err := db.DeleteString( s.Db, db.FilterEq("did", user.Did), db.FilterEq("rkey", rkey), ); err != nil { fail("Failed to delete string.", err) return }
s.Notifier.DeleteString(r.Context(), user.Did, rkey)
s.Pages.HxRedirect(w, "/strings/"+user.Did)}
func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {}