···
+
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview/config"
+
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/middleware"
+
"tangled.sh/tangled.sh/core/appview/oauth"
+
"tangled.sh/tangled.sh/core/appview/pages"
+
"tangled.sh/tangled.sh/core/appview/pages/markup"
+
"tangled.sh/tangled.sh/core/eventconsumer"
+
"tangled.sh/tangled.sh/core/idresolver"
+
"tangled.sh/tangled.sh/core/rbac"
+
"tangled.sh/tangled.sh/core/tid"
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/identity"
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
lexutil "github.com/bluesky-social/indigo/lex/util"
+
"github.com/go-chi/chi/v5"
+
Enforcer *rbac.Enforcer
+
IdResolver *idresolver.Resolver
+
Knotstream *eventconsumer.Consumer
+
func (s *Strings) Router(mw *middleware.Middleware) http.Handler {
+
With(mw.ResolveIdent()).
+
Route("/{user}", func(r chi.Router) {
+
r.Get("/", s.dashboard)
+
r.Route("/{rkey}", func(r chi.Router) {
+
r.Delete("/", s.delete)
+
r.Get("/raw", s.contents)
+
r.Post("/edit", s.edit)
+
With(middleware.AuthMiddleware(s.OAuth)).
+
Post("/comment", s.comment)
+
With(middleware.AuthMiddleware(s.OAuth)).
+
Route("/new", func(r chi.Router) {
+
func (s *Strings) contents(w http.ResponseWriter, r *http.Request) {
+
l := s.Logger.With("handler", "contents")
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
+
l.Error("malformed middleware")
+
w.WriteHeader(http.StatusInternalServerError)
+
l = l.With("did", id.DID, "handle", id.Handle)
+
rkey := chi.URLParam(r, "rkey")
+
l.Error("malformed url, empty rkey")
+
w.WriteHeader(http.StatusBadRequest)
+
l = l.With("rkey", rkey)
+
strings, err := db.GetStrings(
+
db.FilterEq("did", id.DID),
+
db.FilterEq("rkey", rkey),
+
l.Error("failed to fetch string", "err", err)
+
w.WriteHeader(http.StatusInternalServerError)
+
l.Error("incorrect number of records returned", "len(strings)", len(strings))
+
w.WriteHeader(http.StatusInternalServerError)
+
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))
+
l.Error("failed to write raw response", "err", err)
+
var showRendered, renderToggle bool
+
if markup.GetFormat(string.Filename) == markup.FormatMarkdown {
+
showRendered = r.URL.Query().Get("code") != "true"
+
s.Pages.SingleString(w, pages.SingleStringParams{
+
LoggedInUser: s.OAuth.GetUser(r),
+
RenderToggle: renderToggle,
+
ShowRendered: showRendered,
+
func (s *Strings) dashboard(w http.ResponseWriter, r *http.Request) {
+
l := s.Logger.With("handler", "dashboard")
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
+
l.Error("malformed middleware")
+
w.WriteHeader(http.StatusInternalServerError)
+
l = l.With("did", id.DID, "handle", id.Handle)
+
all, err := db.GetStrings(
+
db.FilterEq("did", id.DID),
+
l.Error("failed to fetch strings", "err", err)
+
w.WriteHeader(http.StatusInternalServerError)
+
slices.SortFunc(all, func(a, b db.String) int {
+
if a.Created.After(b.Created) {
+
profile, err := db.GetProfile(s.Db, id.DID.String())
+
l.Error("failed to fetch user profile", "err", err)
+
w.WriteHeader(http.StatusInternalServerError)
+
loggedInUser := s.OAuth.GetUser(r)
+
followStatus := db.IsNotFollowing
+
if loggedInUser != nil {
+
followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String())
+
followers, following, err := db.GetFollowerFollowing(s.Db, id.DID.String())
+
l.Error("failed to get follow stats", "err", err)
+
s.Pages.StringsDashboard(w, pages.StringsDashboardParams{
+
LoggedInUser: s.OAuth.GetUser(r),
+
Card: pages.ProfileCard{
+
UserDid: id.DID.String(),
+
UserHandle: id.Handle.String(),
+
FollowStatus: followStatus,
+
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)
+
l.Error("malformed middleware")
+
w.WriteHeader(http.StatusInternalServerError)
+
l = l.With("did", id.DID, "handle", id.Handle)
+
rkey := chi.URLParam(r, "rkey")
+
l.Error("malformed url, empty rkey")
+
w.WriteHeader(http.StatusBadRequest)
+
l = l.With("rkey", rkey)
+
// get the string currently being edited
+
all, err := db.GetStrings(
+
db.FilterEq("did", id.DID),
+
db.FilterEq("rkey", rkey),
+
l.Error("failed to fetch string", "err", err)
+
w.WriteHeader(http.StatusInternalServerError)
+
l.Error("incorrect number of records returned", "len(strings)", len(all))
+
w.WriteHeader(http.StatusInternalServerError)
+
// 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 the form with prefilled fields
+
s.Pages.PutString(w, pages.PutStringParams{
+
LoggedInUser: s.OAuth.GetUser(r),
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
s.Pages.Notice(w, "error", msg)
+
filename := r.FormValue("filename")
+
fail("Empty filename.", nil)
+
if !strings.Contains(filename, ".") {
+
// TODO: make this a htmx form validation
+
fail("No extension provided for filename.", nil)
+
content := r.FormValue("content")
+
fail("Empty contents.", nil)
+
description := r.FormValue("description")
+
// construct new string from form values
+
Description: description,
+
Created: first.Created,
+
record := entry.AsRecord()
+
client, err := s.OAuth.AuthorizedClient(r)
+
fail("Failed to create record.", err)
+
// first replace the existing record in the PDS
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.StringNSID, entry.Did.String(), entry.Rkey)
+
fail("Failed to updated existing record.", err)
+
resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{
+
Collection: tangled.StringNSID,
+
Repo: entry.Did.String(),
+
Record: &lexutil.LexiconTypeDecoder{
+
fail("Failed to updated existing record.", err)
+
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)
+
// if that went okay, redir to the string
+
s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+entry.Rkey)
+
func (s *Strings) create(w http.ResponseWriter, r *http.Request) {
+
l := s.Logger.With("handler", "create")
+
user := s.OAuth.GetUser(r)
+
s.Pages.PutString(w, pages.PutStringParams{
+
LoggedInUser: s.OAuth.GetUser(r),
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
s.Pages.Notice(w, "error", msg)
+
filename := r.FormValue("filename")
+
fail("Empty filename.", nil)
+
if !strings.Contains(filename, ".") {
+
// TODO: make this a htmx form validation
+
fail("No extension provided for filename.", nil)
+
content := r.FormValue("content")
+
fail("Empty contents.", nil)
+
description := r.FormValue("description")
+
Did: syntax.DID(user.Did),
+
Description: description,
+
record := string.AsRecord()
+
client, err := s.OAuth.AuthorizedClient(r)
+
fail("Failed to create record.", err)
+
resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{
+
Collection: tangled.StringNSID,
+
Record: &lexutil.LexiconTypeDecoder{
+
fail("Failed to create record.", err)
+
l := l.With("aturi", resp.Uri)
+
l.Info("created record")
+
if err = db.AddString(s.Db, string); err != nil {
+
fail("Failed to create string.", err)
+
s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+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)
+
l.Error("malformed middleware")
+
w.WriteHeader(http.StatusInternalServerError)
+
l = l.With("did", id.DID, "handle", id.Handle)
+
rkey := chi.URLParam(r, "rkey")
+
l.Error("malformed url, empty rkey")
+
w.WriteHeader(http.StatusBadRequest)
+
if user.Did != id.DID.String() {
+
fail("You cannot delete this gist", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String()))
+
if err := db.DeleteString(
+
db.FilterEq("did", user.Did),
+
db.FilterEq("rkey", rkey),
+
fail("Failed to delete string.", err)
+
s.Pages.HxRedirect(w, "/strings/"+user.Handle)
+
func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {