···
13
+
"tangled.sh/tangled.sh/core/api/tangled"
14
+
"tangled.sh/tangled.sh/core/appview/config"
15
+
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/appview/middleware"
17
+
"tangled.sh/tangled.sh/core/appview/oauth"
18
+
"tangled.sh/tangled.sh/core/appview/pages"
19
+
"tangled.sh/tangled.sh/core/appview/pages/markup"
20
+
"tangled.sh/tangled.sh/core/eventconsumer"
21
+
"tangled.sh/tangled.sh/core/idresolver"
22
+
"tangled.sh/tangled.sh/core/rbac"
23
+
"tangled.sh/tangled.sh/core/tid"
25
+
"github.com/bluesky-social/indigo/api/atproto"
26
+
"github.com/bluesky-social/indigo/atproto/identity"
27
+
"github.com/bluesky-social/indigo/atproto/syntax"
28
+
lexutil "github.com/bluesky-social/indigo/lex/util"
29
+
"github.com/go-chi/chi/v5"
32
+
type Strings struct {
36
+
Config *config.Config
37
+
Enforcer *rbac.Enforcer
38
+
IdResolver *idresolver.Resolver
40
+
Knotstream *eventconsumer.Consumer
43
+
func (s *Strings) Router(mw *middleware.Middleware) http.Handler {
44
+
r := chi.NewRouter()
47
+
With(mw.ResolveIdent()).
48
+
Route("/{user}", func(r chi.Router) {
49
+
r.Get("/", s.dashboard)
51
+
r.Route("/{rkey}", func(r chi.Router) {
52
+
r.Get("/", s.contents)
53
+
r.Delete("/", s.delete)
54
+
r.Get("/raw", s.contents)
55
+
r.Get("/edit", s.edit)
56
+
r.Post("/edit", s.edit)
58
+
With(middleware.AuthMiddleware(s.OAuth)).
59
+
Post("/comment", s.comment)
64
+
With(middleware.AuthMiddleware(s.OAuth)).
65
+
Route("/new", func(r chi.Router) {
66
+
r.Get("/", s.create)
67
+
r.Post("/", s.create)
73
+
func (s *Strings) contents(w http.ResponseWriter, r *http.Request) {
74
+
l := s.Logger.With("handler", "contents")
76
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
78
+
l.Error("malformed middleware")
79
+
w.WriteHeader(http.StatusInternalServerError)
82
+
l = l.With("did", id.DID, "handle", id.Handle)
84
+
rkey := chi.URLParam(r, "rkey")
86
+
l.Error("malformed url, empty rkey")
87
+
w.WriteHeader(http.StatusBadRequest)
90
+
l = l.With("rkey", rkey)
92
+
strings, err := db.GetStrings(
94
+
db.FilterEq("did", id.DID),
95
+
db.FilterEq("rkey", rkey),
98
+
l.Error("failed to fetch string", "err", err)
99
+
w.WriteHeader(http.StatusInternalServerError)
102
+
if len(strings) != 1 {
103
+
l.Error("incorrect number of records returned", "len(strings)", len(strings))
104
+
w.WriteHeader(http.StatusInternalServerError)
107
+
string := strings[0]
109
+
if path.Base(r.URL.Path) == "raw" {
110
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
111
+
if string.Filename != "" {
112
+
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", string.Filename))
114
+
w.Header().Set("Content-Length", strconv.Itoa(len(string.Contents)))
116
+
_, err = w.Write([]byte(string.Contents))
118
+
l.Error("failed to write raw response", "err", err)
123
+
var showRendered, renderToggle bool
124
+
if markup.GetFormat(string.Filename) == markup.FormatMarkdown {
125
+
renderToggle = true
126
+
showRendered = r.URL.Query().Get("code") != "true"
129
+
s.Pages.SingleString(w, pages.SingleStringParams{
130
+
LoggedInUser: s.OAuth.GetUser(r),
131
+
RenderToggle: renderToggle,
132
+
ShowRendered: showRendered,
134
+
Stats: string.Stats(),
139
+
func (s *Strings) dashboard(w http.ResponseWriter, r *http.Request) {
140
+
l := s.Logger.With("handler", "dashboard")
142
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
144
+
l.Error("malformed middleware")
145
+
w.WriteHeader(http.StatusInternalServerError)
148
+
l = l.With("did", id.DID, "handle", id.Handle)
150
+
all, err := db.GetStrings(
152
+
db.FilterEq("did", id.DID),
155
+
l.Error("failed to fetch strings", "err", err)
156
+
w.WriteHeader(http.StatusInternalServerError)
160
+
slices.SortFunc(all, func(a, b db.String) int {
161
+
if a.Created.After(b.Created) {
168
+
profile, err := db.GetProfile(s.Db, id.DID.String())
170
+
l.Error("failed to fetch user profile", "err", err)
171
+
w.WriteHeader(http.StatusInternalServerError)
174
+
loggedInUser := s.OAuth.GetUser(r)
175
+
followStatus := db.IsNotFollowing
176
+
if loggedInUser != nil {
177
+
followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String())
180
+
followers, following, err := db.GetFollowerFollowing(s.Db, id.DID.String())
182
+
l.Error("failed to get follow stats", "err", err)
185
+
s.Pages.StringsDashboard(w, pages.StringsDashboardParams{
186
+
LoggedInUser: s.OAuth.GetUser(r),
187
+
Card: pages.ProfileCard{
188
+
UserDid: id.DID.String(),
189
+
UserHandle: id.Handle.String(),
191
+
FollowStatus: followStatus,
192
+
Followers: followers,
193
+
Following: following,
199
+
func (s *Strings) edit(w http.ResponseWriter, r *http.Request) {
200
+
l := s.Logger.With("handler", "edit")
202
+
user := s.OAuth.GetUser(r)
204
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
206
+
l.Error("malformed middleware")
207
+
w.WriteHeader(http.StatusInternalServerError)
210
+
l = l.With("did", id.DID, "handle", id.Handle)
212
+
rkey := chi.URLParam(r, "rkey")
214
+
l.Error("malformed url, empty rkey")
215
+
w.WriteHeader(http.StatusBadRequest)
218
+
l = l.With("rkey", rkey)
220
+
// get the string currently being edited
221
+
all, err := db.GetStrings(
223
+
db.FilterEq("did", id.DID),
224
+
db.FilterEq("rkey", rkey),
227
+
l.Error("failed to fetch string", "err", err)
228
+
w.WriteHeader(http.StatusInternalServerError)
232
+
l.Error("incorrect number of records returned", "len(strings)", len(all))
233
+
w.WriteHeader(http.StatusInternalServerError)
238
+
// verify that the logged in user owns this string
239
+
if user.Did != id.DID.String() {
240
+
l.Error("unauthorized request", "expected", id.DID, "got", user.Did)
241
+
w.WriteHeader(http.StatusUnauthorized)
246
+
case http.MethodGet:
247
+
// return the form with prefilled fields
248
+
s.Pages.PutString(w, pages.PutStringParams{
249
+
LoggedInUser: s.OAuth.GetUser(r),
253
+
case http.MethodPost:
254
+
fail := func(msg string, err error) {
255
+
l.Error(msg, "err", err)
256
+
s.Pages.Notice(w, "error", msg)
259
+
filename := r.FormValue("filename")
260
+
if filename == "" {
261
+
fail("Empty filename.", nil)
264
+
if !strings.Contains(filename, ".") {
265
+
// TODO: make this a htmx form validation
266
+
fail("No extension provided for filename.", nil)
270
+
content := r.FormValue("content")
272
+
fail("Empty contents.", nil)
276
+
description := r.FormValue("description")
278
+
// construct new string from form values
279
+
entry := db.String{
282
+
Filename: filename,
283
+
Description: description,
285
+
Created: first.Created,
288
+
record := entry.AsRecord()
290
+
client, err := s.OAuth.AuthorizedClient(r)
292
+
fail("Failed to create record.", err)
296
+
// first replace the existing record in the PDS
297
+
ex, err := client.RepoGetRecord(r.Context(), "", tangled.StringNSID, entry.Did.String(), entry.Rkey)
299
+
fail("Failed to updated existing record.", err)
302
+
resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{
303
+
Collection: tangled.StringNSID,
304
+
Repo: entry.Did.String(),
306
+
SwapRecord: ex.Cid,
307
+
Record: &lexutil.LexiconTypeDecoder{
312
+
fail("Failed to updated existing record.", err)
315
+
l := l.With("aturi", resp.Uri)
316
+
l.Info("edited string")
318
+
// if that went okay, updated the db
319
+
if err = db.AddString(s.Db, entry); err != nil {
320
+
fail("Failed to update string.", err)
324
+
// if that went okay, redir to the string
325
+
s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+entry.Rkey)
330
+
func (s *Strings) create(w http.ResponseWriter, r *http.Request) {
331
+
l := s.Logger.With("handler", "create")
332
+
user := s.OAuth.GetUser(r)
335
+
case http.MethodGet:
336
+
s.Pages.PutString(w, pages.PutStringParams{
337
+
LoggedInUser: s.OAuth.GetUser(r),
340
+
case http.MethodPost:
341
+
fail := func(msg string, err error) {
342
+
l.Error(msg, "err", err)
343
+
s.Pages.Notice(w, "error", msg)
346
+
filename := r.FormValue("filename")
347
+
if filename == "" {
348
+
fail("Empty filename.", nil)
351
+
if !strings.Contains(filename, ".") {
352
+
// TODO: make this a htmx form validation
353
+
fail("No extension provided for filename.", nil)
357
+
content := r.FormValue("content")
359
+
fail("Empty contents.", nil)
363
+
description := r.FormValue("description")
365
+
string := db.String{
366
+
Did: syntax.DID(user.Did),
368
+
Filename: filename,
369
+
Description: description,
371
+
Created: time.Now(),
374
+
record := string.AsRecord()
376
+
client, err := s.OAuth.AuthorizedClient(r)
378
+
fail("Failed to create record.", err)
382
+
resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{
383
+
Collection: tangled.StringNSID,
386
+
Record: &lexutil.LexiconTypeDecoder{
391
+
fail("Failed to create record.", err)
394
+
l := l.With("aturi", resp.Uri)
395
+
l.Info("created record")
398
+
if err = db.AddString(s.Db, string); err != nil {
399
+
fail("Failed to create string.", err)
404
+
s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+string.Rkey)
408
+
func (s *Strings) delete(w http.ResponseWriter, r *http.Request) {
409
+
l := s.Logger.With("handler", "create")
410
+
user := s.OAuth.GetUser(r)
411
+
fail := func(msg string, err error) {
412
+
l.Error(msg, "err", err)
413
+
s.Pages.Notice(w, "error", msg)
416
+
id, ok := r.Context().Value("resolvedId").(identity.Identity)
418
+
l.Error("malformed middleware")
419
+
w.WriteHeader(http.StatusInternalServerError)
422
+
l = l.With("did", id.DID, "handle", id.Handle)
424
+
rkey := chi.URLParam(r, "rkey")
426
+
l.Error("malformed url, empty rkey")
427
+
w.WriteHeader(http.StatusBadRequest)
431
+
if user.Did != id.DID.String() {
432
+
fail("You cannot delete this gist", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String()))
436
+
if err := db.DeleteString(
438
+
db.FilterEq("did", user.Did),
439
+
db.FilterEq("rkey", rkey),
441
+
fail("Failed to delete string.", err)
445
+
s.Pages.HxRedirect(w, "/strings/"+user.Handle)
448
+
func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {