---
cursor.go | 6 +-
filters.go | 55 ++++++++++++++-
handlers.go | 2 +-
models.go | 34 +++++----
repository.go | 121 ++++++++++-----------------------
templates/partials/footer.html | 6 +-
templates/partials/head.html | 4 +-
view.go | 17 +++--
8 files changed, 130 insertions(+), 115 deletions(-)
diff --git a/cursor.go b/cursor.go
index a1ae9f2c..7b4daabc 100644
--- a/cursor.go
+++ b/cursor.go
@@ -3,11 +3,13 @@ package brutalinks
import (
"sort"
"time"
+
+ vocab "github.com/go-ap/activitypub"
)
type Cursor struct {
- after Hash
- before Hash
+ after vocab.IRI
+ before vocab.IRI
items RenderableList
total uint
}
diff --git a/filters.go b/filters.go
index d446bc87..9cf6ef32 100644
--- a/filters.go
+++ b/filters.go
@@ -4,10 +4,13 @@ import (
"context"
"fmt"
"net/http"
+ "net/url"
+ "strconv"
vocab "github.com/go-ap/activitypub"
"github.com/go-ap/errors"
"github.com/go-ap/filters"
+ "github.com/go-ap/filters/index"
"github.com/go-chi/chi/v5"
"github.com/mariusor/qstring"
"gitlab.com/golang-commonmark/puny"
@@ -90,8 +93,58 @@ func FollowedChecks(next http.Handler) http.Handler {
})
}
+const (
+ refBase = 36
+ keyAfter = "after"
+ keyBefore = "before"
+)
+
+type cursorRef uint64
+
+func (c cursorRef) Match(it vocab.Item) bool {
+ ref := index.HashFn(it.GetLink())
+ return uint64(c) == ref
+}
+
+func (c cursorRef) String() string {
+ return strconv.FormatUint(uint64(c), refBase)
+}
+
+var _ filters.Check = cursorRef(0)
+
+func RefValue(r uint64) cursorRef {
+ return cursorRef(r)
+}
+
+func IRIRef(i vocab.IRI) cursorRef {
+ return cursorRef(index.HashFn(i))
+}
+
+func FromURL(u url.URL) filters.Checks {
+ q := u.Query()
+ appendChecks := func(checks *filters.Checks, check func(...filters.Check) filters.Check, vv []string) {
+ for _, v := range vv {
+ if r, err := strconv.ParseUint(v, refBase, 64); err == nil {
+ *checks = append(*checks, check(RefValue(r)))
+ }
+ }
+ }
+ checks := make(filters.Checks, 0, len(q))
+ for k, vv := range q {
+ if k == keyAfter {
+ appendChecks(&checks, filters.After, vv)
+ q.Del(k)
+ }
+ if k == keyBefore {
+ appendChecks(&checks, filters.Before, vv)
+ q.Del(k)
+ }
+ }
+ return append(filters.FromValues(q), checks...)
+}
+
func requestChecks(r *http.Request) filters.Checks {
- return append(filters.FromURL(*r.URL), filters.WithMaxCount(MaxContentItems))
+ return append(FromURL(*r.URL), filters.WithMaxCount(MaxContentItems))
}
func defaultChecks(r *http.Request) filters.Checks {
diff --git a/handlers.go b/handlers.go
index 2565d3a9..24aacc19 100644
--- a/handlers.go
+++ b/handlers.go
@@ -1180,7 +1180,7 @@ func (h *handler) HandleShow(w http.ResponseWriter, r *http.Request) {
}
}
if cursor := ContextCursor(r.Context()); cursor != nil {
- if mod, ok := m.(Paginator); ok {
+ if mod, ok := m.(CursorSetter); ok {
mod.SetCursor(cursor)
}
}
diff --git a/models.go b/models.go
index 32e18902..052f2721 100644
--- a/models.go
+++ b/models.go
@@ -7,10 +7,14 @@ import (
vocab "github.com/go-ap/activitypub"
)
-type Paginator interface {
+type CursorSetter interface {
SetCursor(*Cursor)
- NextPage() Hash
- PrevPage() Hash
+}
+
+type Paginator interface {
+ CursorSetter
+ NextPage() vocab.IRI
+ PrevPage() vocab.IRI
}
type Model interface {
@@ -25,8 +29,8 @@ type listingModel struct {
ShowChildren bool
children RenderableList
ShowText bool
- after Hash
- before Hash
+ after vocab.IRI
+ before vocab.IRI
sortFn func(list RenderableList) []Renderable
}
@@ -56,11 +60,11 @@ func (m listingModel) ID() Hash {
return Hash{}
}
-func (m listingModel) NextPage() Hash {
+func (m listingModel) NextPage() vocab.IRI {
return m.after
}
-func (m listingModel) PrevPage() Hash {
+func (m listingModel) PrevPage() vocab.IRI {
return m.before
}
@@ -108,19 +112,19 @@ type contentModel struct {
Content Renderable
ShowChildren bool
Message mBox
- after Hash
- before Hash
+ after vocab.IRI
+ before vocab.IRI
}
func (m contentModel) ID() Hash {
return Hash{}
}
-func (m contentModel) NextPage() Hash {
+func (m contentModel) NextPage() vocab.IRI {
return m.after
}
-func (m contentModel) PrevPage() Hash {
+func (m contentModel) PrevPage() vocab.IRI {
return m.before
}
@@ -211,15 +215,15 @@ type moderationModel struct {
Content *ModerationOp
ShowChildren bool
Message mBox
- after Hash
- before Hash
+ after vocab.IRI
+ before vocab.IRI
}
-func (m moderationModel) NextPage() Hash {
+func (m moderationModel) NextPage() vocab.IRI {
return m.after
}
-func (m moderationModel) PrevPage() Hash {
+func (m moderationModel) PrevPage() vocab.IRI {
return m.before
}
diff --git a/repository.go b/repository.go
index 28f0af09..5be18891 100644
--- a/repository.go
+++ b/repository.go
@@ -650,15 +650,21 @@ func (r *repository) loadAccountsOutbox(ctx context.Context, acc *Account) error
return nil
}
+ now := time.Now().UTC()
+ lastUpdated := acc.Metadata.OutboxUpdated
+ if now.Sub(lastUpdated)-5*time.Minute < 0 {
+ return nil
+ }
+
ac := acc.AP()
validTypes := append(vocab.ActivityVocabularyTypes{vocab.LikeType}, vocab.CreateType, vocab.DeleteType)
- check := filters.All(
+ check := []filters.Check{
filters.HasType(validTypes...),
filters.SameAttributedTo(ac.GetLink()),
filters.WithMaxCount(200),
- )
+ }
- result, err := r.b.Search(check)
+ result, err := r.b.Search(check...)
if err != nil {
return err
}
@@ -705,6 +711,7 @@ func (r *repository) loadAccountsOutbox(ctx context.Context, acc *Account) error
// return nil
//})
}
+ acc.Metadata.OutboxUpdated = now
return nil
}
@@ -784,7 +791,7 @@ func irisFromItems(items ...Item) vocab.IRIs {
if it.Deleted() {
continue
}
- iris = append(iris, it.Pub.GetLink())
+ _ = iris.Append(it.AP())
}
return iris
}
@@ -831,12 +838,15 @@ func (r *repository) loadItemsVotes(ctx context.Context, items ...Item) (ItemCol
}
func AccountIRIChecks(accounts ...Account) filters.Check {
- filter := make(filters.Checks, 0, len(accounts))
+ iris := make(vocab.IRIs, 0, len(accounts))
for _, ac := range accounts {
- if ac.Pub == nil {
- continue
+ if it := ac.AP(); !vocab.IsNil(it) {
+ _ = iris.Append(it.GetLink())
}
- filter = append(filter, filters.SameIRI(ac.AP().GetLink()))
+ }
+ filter := make(filters.Checks, 0, len(accounts))
+ for _, i := range iris {
+ filter = append(filter, filters.SameIRI(i))
}
return filters.Any(filter...)
}
@@ -1175,62 +1185,12 @@ func (r *repository) loadItemsAuthors(ctx context.Context, items ...Item) (ItemC
return col, nil
}
-func getCollectionPrevNext(col vocab.CollectionInterface) (prev, next vocab.IRI) {
- qFn := func(i vocab.Item) url.Values {
- if i == nil {
- return url.Values{}
- }
- if u, err := i.GetLink().URL(); err == nil {
- return u.Query()
- }
- return url.Values{}
- }
- beforeFn := func(i vocab.Item) vocab.IRI {
- return vocab.IRI(qFn(i).Get("before"))
- }
- afterFn := func(i vocab.Item) vocab.IRI {
- return vocab.IRI(qFn(i).Get("after"))
- }
- nextFromLastFn := func(i vocab.Item) vocab.IRI {
- return i.GetLink()
- }
- switch col.GetType() {
- case vocab.OrderedCollectionPageType:
- if c, ok := col.(*vocab.OrderedCollectionPage); ok {
- prev = beforeFn(c.Prev)
- if int(c.TotalItems) > len(c.OrderedItems) {
- next = afterFn(c.Next)
- }
- }
- case vocab.OrderedCollectionType:
- if c, ok := col.(*vocab.OrderedCollection); ok {
- if len(c.OrderedItems) > 0 && int(c.TotalItems) > len(c.OrderedItems) {
- next = nextFromLastFn(c.OrderedItems[len(c.OrderedItems)-1])
- }
- }
- case vocab.CollectionPageType:
- if c, ok := col.(*vocab.CollectionPage); ok {
- prev = beforeFn(c.Prev)
- if int(c.TotalItems) > len(c.Items) {
- next = afterFn(c.Next)
- }
- }
- case vocab.CollectionType:
- if c, ok := col.(*vocab.Collection); ok {
- if len(c.Items) > 0 && int(c.TotalItems) > len(c.Items) {
- next = nextFromLastFn(c.Items[len(c.Items)-1])
- }
- }
+func getCollectionPrevNext(col vocab.ItemCollection) (prev, next vocab.IRI) {
+ if len(col) > 0 {
+ prev = col.First().GetLink()
}
- // NOTE(marius): we check if current Collection id contains a cursor, and if `after` points to the same URL
- // we don't take it into consideration.
- if next != "" {
- f := struct {
- Next vocab.IRI `qstring:"after"`
- }{}
- if err := qstring.Unmarshal(qFn(col.GetLink()), &f); err == nil && next.Equals(f.Next, true) {
- next = ""
- }
+ if len(col) > 1 {
+ next = col[len(col)-1].GetLink()
}
return prev, next
}
@@ -1404,8 +1364,6 @@ func (r *repository) LoadSearches(ctx context.Context, deps deps, checks ...filt
result := make(RenderableList, 0)
resM := new(sync.RWMutex)
- var next, prev vocab.IRI
-
results, err := r.b.Search(checks...)
if err != nil {
return emptyCursor, err
@@ -1507,6 +1465,7 @@ func (r *repository) LoadSearches(ctx context.Context, deps deps, checks ...filt
return emptyCursor, err
}
+ next, prev := getCollectionPrevNext(results)
if len(deferredRemote) > 0 {
searchIRIs := make(filters.Checks, 0, len(deferredRemote))
for _, iri := range deferredRemote {
@@ -1601,8 +1560,8 @@ func (r *repository) LoadSearches(ctx context.Context, deps deps, checks ...filt
})
return Cursor{
- after: HashFromIRI(next),
- before: HashFromIRI(prev),
+ after: next,
+ before: prev,
items: result,
total: uint(len(result)),
}, nil
@@ -2238,11 +2197,6 @@ func (r *repository) ValidateRemoteAccount(ctx context.Context, acc *Account) er
}
func (r *repository) LoadAccountDetails(ctx context.Context, acc *Account) error {
- now := time.Now().UTC()
- lastUpdated := acc.Metadata.OutboxUpdated
- if now.Sub(lastUpdated)-5*time.Minute < 0 {
- return nil
- }
var err error
ltx := log.Ctx{"handle": acc.Handle, "hash": acc.Hash}
r.infoFn(ltx)("loading account details")
@@ -2250,18 +2204,17 @@ func (r *repository) LoadAccountDetails(ctx context.Context, acc *Account) error
if err = r.loadAccountsOutbox(ctx, acc); err != nil {
r.infoFn(ltx, log.Ctx{"err": err.Error()})("unable to load outbox")
}
- if len(acc.Followers) == 0 {
- // TODO(marius): this needs to be moved to where we're handling all Inbox activities, not on page load
- if err = r.loadAccountsFollowers(ctx, acc); err != nil {
- r.infoFn(ltx, log.Ctx{"err": err.Error()})("unable to load followers")
- }
- }
- if len(acc.Following) == 0 {
- if err = r.loadAccountsFollowing(ctx, acc); err != nil {
- r.infoFn(ltx, log.Ctx{"err": err.Error()})("unable to load following")
- }
- }
- acc.Metadata.OutboxUpdated = now
+ //if len(acc.Followers) == 0 {
+ // // TODO(marius): this needs to be moved to where we're handling all Inbox activities, not on page load
+ // if err = r.loadAccountsFollowers(ctx, acc); err != nil {
+ // r.infoFn(ltx, log.Ctx{"err": err.Error()})("unable to load followers")
+ // }
+ //}
+ //if len(acc.Following) == 0 {
+ // if err = r.loadAccountsFollowing(ctx, acc); err != nil {
+ // r.infoFn(ltx, log.Ctx{"err": err.Error()})("unable to load following")
+ // }
+ //}
return err
}
diff --git a/templates/partials/footer.html b/templates/partials/footer.html
index 346fadca..d4dec80e 100644
--- a/templates/partials/footer.html
@@ -1,11 +1,11 @@
{{ if and (CanPaginate .) -}}
-{{ if (or .PrevPage.IsValid .NextPage.IsValid) }}
+{{ if (or (ne .PrevPage "") (ne .NextPage "")) }}
<nav class="pagination">View more:
<ul>
- {{ if .PrevPage.IsValid -}}
+ {{ if ne .PrevPage "" -}}
<li><a href="{{ .PrevPage | PrevPageLink }}" rel="prev prefetch">{{icon "angle-double-right" "v-mirror"}}prev</a></li>
{{- end -}}
- {{ if .NextPage.IsValid -}}
+ {{ if ne .NextPage "" -}}
<li><a href="{{.NextPage | NextPageLink }}" rel="next prefetch">next{{icon "angle-double-right"}}</a></li>
{{- end}}
</ul>
diff --git a/templates/partials/head.html b/templates/partials/head.html
index 4722e005..baa9d8e4 100644
--- a/templates/partials/head.html
+++ b/templates/partials/head.html
@@ -2,10 +2,10 @@
<title>{{ .Title }}</title>
<style>{{- style "/css/inline.css" -}}</style>
{{- if and (not Config.Env.IsDev) (CanPaginate .) -}}
-{{- if .PrevPage.IsValid }}
+{{- if ne .PrevPage "" }}
<link href="{{ .PrevPage | PrevPageLink }}" rel="prev prefetch" />
{{end -}}
-{{- if .NextPage.IsValid }}
+{{- if ne .NextPage "" }}
<link href="{{ .NextPage | NextPageLink }}" rel="next prefetch" />
{{end -}}
{{end -}}
diff --git a/view.go b/view.go
index d9ea63e6..3f500cd9 100644
--- a/view.go
+++ b/view.go
@@ -23,6 +23,7 @@ import (
log "git.sr.ht/~mariusor/lw"
vocab "github.com/go-ap/activitypub"
"github.com/go-ap/errors"
+ "github.com/go-ap/filters"
"github.com/gorilla/csrf"
"github.com/mariusor/qstring"
"github.com/mariusor/render"
@@ -298,7 +299,7 @@ func (v *view) RenderTemplate(r *http.Request, w http.ResponseWriter, name strin
v.errFn(log.Ctx{"err": err, "model": m})("failed to render template %s", name)
return errors.Annotatef(err, "failed to render template")
}
- io.Copy(w, &wrt)
+ _, _ = io.Copy(w, &wrt)
return nil
}
@@ -881,16 +882,18 @@ func rejectLink(f FollowRequest) string {
return path.Join(followLink(f), "reject")
}
-func nextPageLink(p Hash) template.HTML {
- if p.String() != "" {
- return template.HTML(fmt.Sprintf("?after=%s", p))
+func nextPageLink(p vocab.IRI) template.HTML {
+ if p != "" {
+ u := filters.ToValues(filters.After(IRIRef(p)))
+ return template.HTML("?" + u.Encode())
}
return ""
}
-func prevPageLink(p Hash) template.HTML {
- if p.String() != "" {
- return template.HTML(fmt.Sprintf("?before=%s", p))
+func prevPageLink(p vocab.IRI) template.HTML {
+ if p != "" {
+ u := filters.ToValues(filters.Before(IRIRef(p)))
+ return template.HTML("?" + u.Encode())
}
return ""
}
--
2.48.1