Browse Source

Handle pagination for the federated actions & followers responses (#1731)

* Add pagination for admin social list

* Use Paginated API for followers tab on frontend
pull/1759/head
Gabe Kangas 3 years ago committed by GitHub
parent
commit
5e6bc50b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      activitypub/controllers/followers.go
  2. 2
      activitypub/outbox/outbox.go
  3. 12
      activitypub/persistence/followers.go
  4. 11
      activitypub/persistence/persistence.go
  5. 13
      controllers/admin/federation.go
  6. 10
      controllers/followers.go
  7. 7
      controllers/pagination.go
  8. 3
      db/query.sql
  9. 11
      db/query.sql.go
  10. 39
      router/middleware/pagination.go
  11. 6
      router/router.go
  12. 40
      webroot/js/components/federation/followers.js

2
activitypub/controllers/followers.go

@ -98,7 +98,7 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere
return nil, errors.Wrap(err, "unable to get follower count") return nil, errors.Wrap(err, "unable to get follower count")
} }
followers, err := persistence.GetFederationFollowers(followersPageSize, (pageInt-1)*followersPageSize) followers, _, err := persistence.GetFederationFollowers(followersPageSize, (pageInt-1)*followersPageSize)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "unable to get federation followers") return nil, errors.Wrap(err, "unable to get federation followers")
} }

2
activitypub/outbox/outbox.go

@ -171,7 +171,7 @@ func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
func SendToFollowers(payload []byte) error { func SendToFollowers(payload []byte) error {
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername()) localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
followers, err := persistence.GetFederationFollowers(-1, 0) followers, _, err := persistence.GetFederationFollowers(-1, 0)
if err != nil { if err != nil {
log.Errorln("unable to fetch followers to send to", err) log.Errorln("unable to fetch followers to send to", err)
return errors.New("unable to fetch followers to send payload to") return errors.New("unable to fetch followers to send payload to")

12
activitypub/persistence/followers.go

@ -6,6 +6,7 @@ import (
"github.com/owncast/owncast/db" "github.com/owncast/owncast/db"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -44,14 +45,19 @@ func GetFollowerCount() (int64, error) {
} }
// GetFederationFollowers will return a slice of the followers we keep track of locally. // GetFederationFollowers will return a slice of the followers we keep track of locally.
func GetFederationFollowers(limit int, offset int) ([]models.Follower, error) { func GetFederationFollowers(limit int, offset int) ([]models.Follower, int, error) {
ctx := context.Background() ctx := context.Background()
total, err := _datastore.GetQueries().GetFollowerCount(ctx)
if err != nil {
return nil, 0, errors.Wrap(err, "unable to fetch total number of followers")
}
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{ followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
Limit: int32(limit), Limit: int32(limit),
Offset: int32(offset), Offset: int32(offset),
}) })
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
followers := make([]models.Follower, 0) followers := make([]models.Follower, 0)
@ -69,7 +75,7 @@ func GetFederationFollowers(limit int, offset int) ([]models.Follower, error) {
followers = append(followers, singleFollower) followers = append(followers, singleFollower)
} }
return followers, nil return followers, int(total), nil
} }
// GetPendingFollowRequests will return pending follow requests. // GetPendingFollowRequests will return pending follow requests.

11
activitypub/persistence/persistence.go

@ -319,18 +319,23 @@ func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType s
// GetInboundActivities will return a collection of saved, federated activities // GetInboundActivities will return a collection of saved, federated activities
// limited and offset by the values provided to support pagination. // limited and offset by the values provided to support pagination.
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, error) { func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
ctx := context.Background() ctx := context.Background()
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{ rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
Limit: int32(limit), Limit: int32(limit),
Offset: int32(offset), Offset: int32(offset),
}) })
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
activities := make([]models.FederatedActivity, 0) activities := make([]models.FederatedActivity, 0)
total, err := _datastore.GetQueries().GetInboundActivityCount(context.Background())
if err != nil {
return nil, 0, errors.Wrap(err, "unable to fetch total activity count")
}
for _, row := range rows { for _, row := range rows {
singleActivity := models.FederatedActivity{ singleActivity := models.FederatedActivity{
IRI: row.Iri, IRI: row.Iri,
@ -341,7 +346,7 @@ func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, er
activities = append(activities, singleActivity) activities = append(activities, singleActivity)
} }
return activities, nil return activities, int(total), nil
} }
// HasPreviouslyHandledInboundActivity will return if we have previously handled // HasPreviouslyHandledInboundActivity will return if we have previously handled

13
controllers/admin/federation.go

@ -160,12 +160,19 @@ func SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) {
// GetFederatedActions will return the saved list of accepted inbound // GetFederatedActions will return the saved list of accepted inbound
// federated activities. // federated activities.
func GetFederatedActions(w http.ResponseWriter, r *http.Request) { func GetFederatedActions(page int, pageSize int, w http.ResponseWriter, r *http.Request) {
activities, err := persistence.GetInboundActivities(100, 0) offset := pageSize * page
activities, total, err := persistence.GetInboundActivities(pageSize, offset)
if err != nil { if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error()) controllers.WriteSimpleResponse(w, false, err.Error())
return return
} }
controllers.WriteResponse(w, activities) response := controllers.PaginatedResponse{
Total: total,
Results: activities,
}
controllers.WriteResponse(w, response)
} }

10
controllers/followers.go

@ -7,12 +7,16 @@ import (
) )
// GetFollowers will handle an API request to fetch the list of followers (non-activitypub response). // GetFollowers will handle an API request to fetch the list of followers (non-activitypub response).
func GetFollowers(w http.ResponseWriter, r *http.Request) { func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) {
followers, err := persistence.GetFederationFollowers(-1, 0) followers, total, err := persistence.GetFederationFollowers(limit, offset)
if err != nil { if err != nil {
WriteSimpleResponse(w, false, "unable to fetch followers") WriteSimpleResponse(w, false, "unable to fetch followers")
return return
} }
WriteResponse(w, followers) response := PaginatedResponse{
Total: total,
Results: followers,
}
WriteResponse(w, response)
} }

7
controllers/pagination.go

@ -0,0 +1,7 @@
package controllers
// PaginatedResponse is a structure for returning a total count with results.
type PaginatedResponse struct {
Total int `json:"total"`
Results interface{} `json:"results"`
}

3
db/query.sql

@ -47,6 +47,9 @@ INSERT INTO ap_outbox(iri, value, type, live_notification) values($1, $2, $3, $4
-- name: AddToAcceptedActivities :exec -- name: AddToAcceptedActivities :exec
INSERT INTO ap_accepted_activities(iri, actor, type, timestamp) values($1, $2, $3, $4); INSERT INTO ap_accepted_activities(iri, actor, type, timestamp) values($1, $2, $3, $4);
-- name: GetInboundActivityCount :one
SELECT count(*) FROM ap_accepted_activities;
-- name: GetInboundActivitiesWithOffset :many -- name: GetInboundActivitiesWithOffset :many
SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestamp DESC LIMIT $1 OFFSET $2; SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestamp DESC LIMIT $1 OFFSET $2;

11
db/query.sql.go

@ -280,6 +280,17 @@ func (q *Queries) GetInboundActivitiesWithOffset(ctx context.Context, arg GetInb
return items, nil return items, nil
} }
const getInboundActivityCount = `-- name: GetInboundActivityCount :one
SELECT count(*) FROM ap_accepted_activities
`
func (q *Queries) GetInboundActivityCount(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, getInboundActivityCount)
var count int64
err := row.Scan(&count)
return count, err
}
const getLocalPostCount = `-- name: GetLocalPostCount :one const getLocalPostCount = `-- name: GetLocalPostCount :one
SElECT count(*) FROM ap_outbox SElECT count(*) FROM ap_outbox
` `

39
router/middleware/pagination.go

@ -0,0 +1,39 @@
package middleware
import (
"net/http"
"strconv"
)
// PaginatedHandlerFunc is a handler for endpoints that require pagination.
type PaginatedHandlerFunc func(int, int, http.ResponseWriter, *http.Request)
// HandlePagination is a middleware handler that pulls pagination values
// and passes them along.
func HandlePagination(handler PaginatedHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Default 50 items per page
limitString := r.URL.Query().Get("limit")
if limitString == "" {
limitString = "50"
}
limit, err := strconv.Atoi(limitString)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Default first page 0
offsetString := r.URL.Query().Get("offset")
if offsetString == "" {
offsetString = "0"
}
offset, err := strconv.Atoi(offsetString)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
handler(offset, limit, w, r)
}
}

6
router/router.go

@ -77,7 +77,7 @@ func Start() error {
http.HandleFunc("/api/remotefollow", controllers.RemoteFollow) http.HandleFunc("/api/remotefollow", controllers.RemoteFollow)
// return followers // return followers
http.HandleFunc("/api/followers", controllers.GetFollowers) http.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers))
// Authenticated admin requests // Authenticated admin requests
@ -127,7 +127,7 @@ func Start() error {
http.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators)) http.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators))
// return followers // return followers
http.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(controllers.GetFollowers)) http.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers)))
// Get a list of pending follow requests // Get a list of pending follow requests
http.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests)) http.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests))
@ -310,7 +310,7 @@ func Start() error {
http.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage)) http.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage))
// Return federated activities // Return federated activities
http.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(admin.GetFederatedActions)) http.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions)))
// ActivityPub has its own router // ActivityPub has its own router
activitypub.Start(data.GetDatastore()) activitypub.Start(data.GetDatastore())

40
webroot/js/components/federation/followers.js

@ -2,7 +2,6 @@ import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js'; import htm from '/js/web_modules/htm.js';
import { URL_FOLLOWERS } from '/js/utils/constants.js'; import { URL_FOLLOWERS } from '/js/utils/constants.js';
const html = htm.bind(h); const html = htm.bind(h);
import { paginateArray } from '../../utils/helpers.js';
export default class FollowerList extends Component { export default class FollowerList extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -10,6 +9,8 @@ export default class FollowerList extends Component {
this.state = { this.state = {
followers: [], followers: [],
followersPage: 0, followersPage: 0,
currentPage: 0,
total: 0,
}; };
} }
@ -22,23 +23,26 @@ export default class FollowerList extends Component {
} }
async getFollowers() { async getFollowers() {
const response = await fetch(URL_FOLLOWERS); const { currentPage } = this.state;
const limit = 16;
const offset = currentPage * limit;
const u = `${URL_FOLLOWERS}?offset=${offset}&limit=${limit}`;
const response = await fetch(u);
const followers = await response.json(); const followers = await response.json();
this.setState({ this.setState({
followers: followers, followers: followers.results,
total: response.total,
}); });
} }
changeFollowersPage(page) { changeFollowersPage(page) {
this.setState({ followersPage: page }); this.setState({ currentPage: page });
this.getFollowers();
} }
render() { render() {
const FOLLOWER_PAGE_SIZE = 16; const { followers, total, currentPage } = this.state;
const { followersPage } = this.state;
const { followers } = this.state;
if (!followers) { if (!followers) {
return null; return null;
} }
@ -57,21 +61,15 @@ export default class FollowerList extends Component {
</p> </p>
</div>`; </div>`;
const paginatedFollowers = paginateArray(
followers,
followersPage + 1,
FOLLOWER_PAGE_SIZE
);
const paginationControls = const paginationControls =
paginatedFollowers.totalPages > 1 && total > 1 &&
Array(paginatedFollowers.totalPages) Array(total)
.fill() .fill()
.map((x, n) => { .map((x, n) => {
const activePageClass = const activePageClass =
n === followersPage && n === currentPage &&
'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white'; 'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white';
return html` <li class="page-item active"> return html` <li class="page-item active w-10">
<a <a
class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}" class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}"
onClick=${() => this.changeFollowersPage(n)} onClick=${() => this.changeFollowersPage(n)}
@ -85,13 +83,13 @@ export default class FollowerList extends Component {
<div> <div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
${followers.length === 0 && noFollowersInfo} ${followers.length === 0 && noFollowersInfo}
${paginatedFollowers.items.map((follower) => { ${followers.map((follower) => {
return html` <${SingleFollower} user=${follower} /> `; return html` <${SingleFollower} user=${follower} /> `;
})} })}
</div> </div>
<div class="flex"> <div class="flex">
<nav aria-label="Page navigation example"> <nav aria-label="Tab pages">
<ul class="flex list-style-none"> <ul class="flex list-style-none flex-wrap">
${paginationControls} ${paginationControls}
</ul> </ul>
</nav> </nav>

Loading…
Cancel
Save