* initial commit * update api docs * adds search token versioning * make id filterable * isLocal -> is_local * add iri to actor index * update gitignore * Merge remote-tracking branch 'origin/main' into feat-add-actors-to-meilisearch * fix sharing for newly discovered actors * fix ActorSearchResult type --------- Co-authored-by: Christian Beutel <>
189 lines
4.4 KiB
Go
189 lines
4.4 KiB
Go
package routes
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"pocketbase/federation"
|
|
"pocketbase/util"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
)
|
|
|
|
func RemoteTrailCommentsList(e *core.RequestEvent) error {
|
|
trailID := e.Request.PathValue("id")
|
|
expandQuery := e.Request.URL.Query().Get("expand")
|
|
sort := e.Request.URL.Query().Get("sort")
|
|
|
|
if sort == "" {
|
|
sort = "-created"
|
|
}
|
|
|
|
page, _ := strconv.Atoi(e.Request.URL.Query().Get("page"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
perPage, _ := strconv.Atoi(e.Request.URL.Query().Get("perPage"))
|
|
if perPage < 1 {
|
|
perPage = 30
|
|
}
|
|
|
|
trail, err := e.App.FindRecordById("trails", trailID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trailAuthor, err := e.App.FindRecordById("activitypub_actors", trail.GetString("author"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sync remote data first (Fetch + Save)
|
|
if trail.GetString("iri") != "" && !trailAuthor.GetBool("is_local") {
|
|
_ = syncRemoteComments(e, trail)
|
|
}
|
|
|
|
// 1. Calculate Offset
|
|
offset := (page - 1) * perPage
|
|
|
|
// 2. Fetch the records using FindRecordsByFilter
|
|
records, err := e.App.FindRecordsByFilter(
|
|
"comments",
|
|
"trail = {:trailId}",
|
|
sort,
|
|
perPage,
|
|
offset,
|
|
dbx.Params{"trailId": trail.Id},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqInfo, err := e.RequestInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filteredRecords := []*core.Record{}
|
|
for _, record := range records {
|
|
canAccess, _ := e.App.CanAccessRecord(record, reqInfo, record.Collection().ListRule)
|
|
if canAccess {
|
|
filteredRecords = append(filteredRecords, record)
|
|
}
|
|
}
|
|
|
|
// 3. Get total count for pagination metadata
|
|
var totalItems int
|
|
err = e.App.DB().
|
|
Select("count(*)").
|
|
From("comments").
|
|
Where(dbx.HashExp{"trail": trail.Id}).
|
|
Row(&totalItems)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 4. Handle Expand
|
|
if expandQuery != "" {
|
|
errs := e.App.ExpandRecords(filteredRecords, strings.Split(expandQuery, ","), nil)
|
|
if len(errs) > 0 {
|
|
fmt.Printf("Expand errors: %v\n", errs)
|
|
}
|
|
}
|
|
|
|
// 5. Manually construct the response object
|
|
return e.JSON(http.StatusOK, map[string]any{
|
|
"page": page,
|
|
"perPage": perPage,
|
|
"totalItems": totalItems,
|
|
"totalPages": (totalItems + perPage - 1) / perPage,
|
|
"items": filteredRecords,
|
|
})
|
|
}
|
|
|
|
func syncRemoteComments(e *core.RequestEvent, trail *core.Record) error {
|
|
client := util.SafeHTTPClient()
|
|
|
|
var userActor *core.Record
|
|
if e.Auth != nil {
|
|
userActor, _ = e.App.FindFirstRecordByData("activitypub_actors", "user", e.Auth.Id)
|
|
}
|
|
|
|
ctx, err := util.GetSafeActorContext(e.Request, userActor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trailIRI := trail.GetString("iri")
|
|
u, _ := url.Parse(trailIRI)
|
|
|
|
remoteTrailID := path.Base(u.Path)
|
|
|
|
remoteURL := fmt.Sprintf("%s://%s/api/v1/comment?filter=trail='%s'&expand=author", u.Scheme, u.Host, remoteTrailID)
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "GET", remoteURL, nil)
|
|
res, err := client.Do(req)
|
|
if err != nil || res.StatusCode != 200 {
|
|
if errors.Is(err, util.ErrRateLimited) {
|
|
return e.TooManyRequestsError("Too many requests", err)
|
|
}
|
|
return fmt.Errorf("remote fetch failed: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
var remoteData struct {
|
|
Items []map[string]any `json:"items"`
|
|
}
|
|
if err := json.NewDecoder(res.Body).Decode(&remoteData); err != nil {
|
|
return err
|
|
}
|
|
|
|
collection, _ := e.App.FindCollectionByNameOrId("comments")
|
|
|
|
return e.App.RunInTransaction(func(txApp core.App) error {
|
|
for _, raw := range remoteData.Items {
|
|
remoteIRI, _ := raw["iri"].(string)
|
|
if remoteIRI == "" {
|
|
remoteID, _ := raw["id"].(string)
|
|
remoteIRI = fmt.Sprintf("%s://%s/api/v1/comment/%s", u.Scheme, u.Host, remoteID)
|
|
}
|
|
|
|
// Find existing record by IRI or ID to avoid duplicates
|
|
commentRecord, _ := txApp.FindFirstRecordByData("comments", "iri", remoteIRI)
|
|
if commentRecord == nil {
|
|
commentRecord = core.NewRecord(collection)
|
|
commentRecord.Set("iri", remoteIRI)
|
|
commentRecord.Set("trail", trail.Id)
|
|
}
|
|
|
|
// Resolve federated author
|
|
if expand, ok := raw["expand"].(map[string]any); ok {
|
|
if author, ok := expand["author"].(map[string]any); ok {
|
|
authorIRI, _ := author["iri"].(string)
|
|
actor, err := federation.GetActorByIRI(txApp, ctx, authorIRI, false)
|
|
if err == nil {
|
|
raw["author"] = actor.Id
|
|
}
|
|
}
|
|
}
|
|
|
|
delete(raw, "id")
|
|
delete(raw, "trail")
|
|
delete(raw, "expand")
|
|
delete(raw, "iri")
|
|
commentRecord.Load(raw)
|
|
|
|
if err := txApp.Save(commentRecord); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|