Feat add actors to meilisearch (#1048)
* 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 <>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,5 +13,5 @@ start*.*
|
|||||||
|
|
||||||
data*/
|
data*/
|
||||||
.planning/
|
.planning/
|
||||||
.claude
|
.claude/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
@@ -69,7 +69,7 @@ func GetActorByHandle(app core.App, ctx context.Context, handle string, includeF
|
|||||||
if domain != "" {
|
if domain != "" {
|
||||||
filter += "domain={:domain}"
|
filter += "domain={:domain}"
|
||||||
} else {
|
} else {
|
||||||
filter += "isLocal=true"
|
filter += "is_local=true"
|
||||||
}
|
}
|
||||||
|
|
||||||
var dbActor *core.Record
|
var dbActor *core.Record
|
||||||
@@ -81,7 +81,7 @@ func GetActorByHandle(app core.App, ctx context.Context, handle string, includeF
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbActor = core.NewRecord(collection)
|
dbActor = core.NewRecord(collection)
|
||||||
dbActor.Set("isLocal", false)
|
dbActor.Set("is_local", false)
|
||||||
iri, err := iriFromHandle(ctx, domain, username)
|
iri, err := iriFromHandle(ctx, domain, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -105,7 +105,7 @@ func GetActorByIRI(app core.App, ctx context.Context, iri string, includeFollows
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbActor = core.NewRecord(collection)
|
dbActor = core.NewRecord(collection)
|
||||||
dbActor.Set("isLocal", false)
|
dbActor.Set("is_local", false)
|
||||||
dbActor.Set("iri", iri)
|
dbActor.Set("iri", iri)
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -167,7 +167,7 @@ func assembleActor(app core.App, ctx context.Context, dbActor *core.Record, incl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private := false
|
private := false
|
||||||
if dbActor.GetBool("isLocal") {
|
if dbActor.GetBool("is_local") {
|
||||||
user, err := app.FindRecordById("users", dbActor.GetString("user"))
|
user, err := app.FindRecordById("users", dbActor.GetString("user"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func processTrailAnnounceActivity(app core.App, actor *core.Record, activity pub
|
|||||||
}
|
}
|
||||||
|
|
||||||
var trail *core.Record
|
var trail *core.Record
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
trail, err = util.TrailFromActivity(activity, app, actor)
|
trail, err = util.TrailFromActivity(activity, app, actor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -210,7 +210,7 @@ func processListAnnounceActivity(app core.App, actor *core.Record, activity pub.
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list *core.Record
|
var list *core.Record
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
list, err = util.ListFromActivity(activity, app, actor)
|
list, err = util.ListFromActivity(activity, app, actor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -531,7 +531,7 @@ func processCreateOrUpdateCommentActivity(activity pub.Activity, app core.App, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
// no need to do anything else if the actor is local
|
// no need to do anything else if the actor is local
|
||||||
if actor.GetBool("isLocal") {
|
if actor.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +645,7 @@ func processCreateOrUpdateSummitLogActivity(activity pub.Activity, app core.App,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// no need to do anything else if the actor is local
|
// no need to do anything else if the actor is local
|
||||||
if actor.GetBool("isLocal") {
|
if actor.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func CreateCommentDeleteActivity(app core.App, client meilisearch.ServiceManager
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func CreateCommentDeleteActivity(app core.App, client meilisearch.ServiceManager
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if commentTrailAuthor.GetBool("isLocal") {
|
if commentTrailAuthor.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ func CreateSummitLogDeleteActivity(app core.App, r *core.Record) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ func CreateListDeleteActivity(app core.App, r *core.Record) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ func CreateListDeleteActivity(app core.App, r *core.Record) error {
|
|||||||
|
|
||||||
func ProcessDeleteActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
func ProcessDeleteActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||||
// no need to do anything if the actor is local
|
// no need to do anything if the actor is local
|
||||||
if actor.GetBool("isLocal") {
|
if actor.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func ProcessFollowActivity(app core.App, actor *core.Record, activity pub.Activi
|
|||||||
// a remote actor has requested the follow
|
// a remote actor has requested the follow
|
||||||
// this means we have not yet created a follow entry in our db
|
// this means we have not yet created a follow entry in our db
|
||||||
// we accept it immediately
|
// we accept it immediately
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
followCollection, err := app.FindCollectionByNameOrId("follows")
|
followCollection, err := app.FindCollectionByNameOrId("follows")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func ProcessLikeActivity(app core.App, actor *core.Record, activity pub.Activity
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
trailLikeCollection, err := app.FindCollectionByNameOrId("trail_like")
|
trailLikeCollection, err := app.FindCollectionByNameOrId("trail_like")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func ProcessUndoActivity(app core.App, actor *core.Record, activity pub.Activity
|
|||||||
|
|
||||||
func processUnfollowActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
func processUnfollowActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||||
// this was a local follow
|
// this was a local follow
|
||||||
if actor.GetBool("isLocal") {
|
if actor.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ func processUnfollowActivity(app core.App, actor *core.Record, activity pub.Acti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func processUnlikeActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
func processUnlikeActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||||
if actor.GetBool("isLocal") {
|
if actor.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
db/hooks/activitypub_actor.go
Normal file
48
db/hooks/activitypub_actor.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"pocketbase/util"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/meilisearch/meilisearch-go"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateActorHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||||
|
return func(e *core.RecordEvent) error {
|
||||||
|
err := e.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.IndexActors([]*core.Record{e.Record}, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateActorHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||||
|
return func(e *core.RecordEvent) error {
|
||||||
|
err := e.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.UpdateActor(e.Record, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteActorHandler(client meilisearch.ServiceManager) func(e *core.RecordEvent) error {
|
||||||
|
return func(e *core.RecordEvent) error {
|
||||||
|
task, err := client.Index("actors").DeleteDocument(e.Record.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := 500 * time.Millisecond
|
||||||
|
_, err = client.WaitForTask(task.TaskUID, interval)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error waiting for task completion: %v", err)
|
||||||
|
}
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ func CreateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEve
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
// this happens if someone fetches a remote list
|
// this happens if someone fetches a remote list
|
||||||
// we create a stub list record for later reference
|
// we create a stub list record for later reference
|
||||||
// no need to create an activity for that
|
// no need to create an activity for that
|
||||||
@@ -75,7 +75,7 @@ func UpdateListHandler(client meilisearch.ServiceManager) func(e *core.RecordEve
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
// this happens if someone fetches a remote list
|
// this happens if someone fetches a remote list
|
||||||
// we create a stub list record for later reference
|
// we create a stub list record for later reference
|
||||||
// no need to create an activity for that
|
// no need to create an activity for that
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func CreateTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.Reco
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
// this happens if someone likes a remote trail
|
// this happens if someone likes a remote trail
|
||||||
// we create a local copy
|
// we create a local copy
|
||||||
// no need to create an activity for that
|
// no need to create an activity for that
|
||||||
@@ -107,7 +107,7 @@ func DeleteTrailLikeHandler(client meilisearch.ServiceManager) func(e *core.Reco
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
// this happens if someone likes a remote trail
|
// this happens if someone likes a remote trail
|
||||||
// we create a local copy
|
// we create a local copy
|
||||||
// no need to create an activity for that
|
// no need to create an activity for that
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func CreateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEv
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userActor.GetBool("isLocal") {
|
if !userActor.GetBool("is_local") {
|
||||||
// this happens if someone fetches a remote list
|
// this happens if someone fetches a remote list
|
||||||
// we create a stub list record for later reference
|
// we create a stub list record for later reference
|
||||||
// no need to create an activity for that
|
// no need to create an activity for that
|
||||||
@@ -92,7 +92,7 @@ func UpdateTrailHandler(client meilisearch.ServiceManager) func(e *core.RecordEv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !userActor.GetBool("isLocal") {
|
if !userActor.GetBool("is_local") {
|
||||||
// this happens if someone fetches a remote trail
|
// this happens if someone fetches a remote trail
|
||||||
// we create a stub trail record for later reference
|
// we create a stub trail record for later reference
|
||||||
// no need to create an activity for that
|
// no need to create an activity for that
|
||||||
|
|||||||
37
db/main.go
37
db/main.go
@@ -90,6 +90,10 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
|||||||
app.OnRecordAfterCreateSuccess("users").BindFunc(hooks.CreateUserHandler(client))
|
app.OnRecordAfterCreateSuccess("users").BindFunc(hooks.CreateUserHandler(client))
|
||||||
app.OnRecordAfterUpdateSuccess("users").BindFunc(hooks.UpdateUserHandler(client))
|
app.OnRecordAfterUpdateSuccess("users").BindFunc(hooks.UpdateUserHandler(client))
|
||||||
|
|
||||||
|
app.OnRecordAfterCreateSuccess("activitypub_actors").BindFunc(hooks.CreateActorHandler(client))
|
||||||
|
app.OnRecordAfterUpdateSuccess("activitypub_actors").BindFunc(hooks.UpdateActorHandler(client))
|
||||||
|
app.OnRecordAfterDeleteSuccess("activitypub_actors").BindFunc(hooks.DeleteActorHandler(client))
|
||||||
|
|
||||||
app.OnRecordAfterCreateSuccess("trails").BindFunc(hooks.CreateTrailHandler(client))
|
app.OnRecordAfterCreateSuccess("trails").BindFunc(hooks.CreateTrailHandler(client))
|
||||||
app.OnRecordAfterUpdateSuccess("trails").BindFunc(hooks.UpdateTrailHandler(client))
|
app.OnRecordAfterUpdateSuccess("trails").BindFunc(hooks.UpdateTrailHandler(client))
|
||||||
app.OnRecordAfterDeleteSuccess("trails").BindFunc(hooks.DeleteTrailHandler(client))
|
app.OnRecordAfterDeleteSuccess("trails").BindFunc(hooks.DeleteTrailHandler(client))
|
||||||
@@ -315,6 +319,12 @@ func initMeilisearchConfig(client meilisearch.ServiceManager) {
|
|||||||
SortableAttributes: []string{"created", "name"},
|
SortableAttributes: []string{"created", "name"},
|
||||||
RankingRules: []string{"words", "typo", "proximity", "attribute", "sort", "exactness"},
|
RankingRules: []string{"words", "typo", "proximity", "attribute", "sort", "exactness"},
|
||||||
},
|
},
|
||||||
|
"actors": {
|
||||||
|
SearchableAttributes: []string{"username", "preferred_username", "domain"},
|
||||||
|
FilterableAttributes: []string{"id"},
|
||||||
|
SortableAttributes: []string{},
|
||||||
|
RankingRules: []string{"words", "typo", "proximity", "attribute", "sort", "exactness"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for indexName, settings := range configs {
|
for indexName, settings := range configs {
|
||||||
@@ -404,5 +414,32 @@ func initMeilisearchDocuments(app core.App, client meilisearch.ServiceManager) e
|
|||||||
page++
|
page++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Actors ---
|
||||||
|
if _, err := client.Index("actors").DeleteAllDocuments(nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
page = 0
|
||||||
|
for {
|
||||||
|
actors := []*core.Record{}
|
||||||
|
err := app.RecordQuery("activitypub_actors").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(page * pageSize).
|
||||||
|
All(&actors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(actors) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := util.IndexActors(actors, client); err != nil {
|
||||||
|
app.Logger().Warn(fmt.Sprintf("Unable to index actor page %d: %v", page, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
52
db/migrations/1780734977_updated_activitypub_actors.go
Normal file
52
db/migrations/1780734977_updated_activitypub_actors.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update field
|
||||||
|
if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{
|
||||||
|
"help": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2193750486",
|
||||||
|
"name": "is_local",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
}`)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_1295301207")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update field
|
||||||
|
if err := collection.Fields.AddMarshaledJSONAt(14, []byte(`{
|
||||||
|
"help": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "bool2193750486",
|
||||||
|
"name": "isLocal",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "bool"
|
||||||
|
}`)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ func RemoteTrailCommentsList(e *core.RequestEvent) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sync remote data first (Fetch + Save)
|
// Sync remote data first (Fetch + Save)
|
||||||
if trail.GetString("iri") != "" && !trailAuthor.GetBool("isLocal") {
|
if trail.GetString("iri") != "" && !trailAuthor.GetBool("is_local") {
|
||||||
_ = syncRemoteComments(e, trail)
|
_ = syncRemoteComments(e, trail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func SearchToken(client meilisearch.ServiceManager) func(e *core.RequestEvent) e
|
|||||||
"trails": map[string]string{
|
"trails": map[string]string{
|
||||||
"filter": "public = true OR author = " + userActor.Id + " OR shares = " + userActor.Id,
|
"filter": "public = true OR author = " + userActor.Id + " OR shares = " + userActor.Id,
|
||||||
},
|
},
|
||||||
|
"actors": map[string]string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1516,7 +1516,7 @@ func buildMergedCommentText(app core.App, comment *core.Record) string {
|
|||||||
if authorID := comment.GetString("author"); authorID != "" {
|
if authorID := comment.GetString("author"); authorID != "" {
|
||||||
if author, err := app.FindRecordById("activitypub_actors", authorID); err == nil {
|
if author, err := app.FindRecordById("activitypub_actors", authorID); err == nil {
|
||||||
authorHandle = "@" + author.GetString("preferred_username")
|
authorHandle = "@" + author.GetString("preferred_username")
|
||||||
if !author.GetBool("isLocal") && author.GetString("domain") != "" {
|
if !author.GetBool("is_local") && author.GetString("domain") != "" {
|
||||||
authorHandle += "@" + author.GetString("domain")
|
authorHandle += "@" + author.GetString("domain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func ActorFromUser(app core.App, u *core.Record) (*core.Record, error) {
|
|||||||
record.Set("outbox", id+"/outbox")
|
record.Set("outbox", id+"/outbox")
|
||||||
record.Set("followers", id+"/followers")
|
record.Set("followers", id+"/followers")
|
||||||
record.Set("following", id+"/following")
|
record.Set("following", id+"/following")
|
||||||
record.Set("isLocal", true)
|
record.Set("is_local", true)
|
||||||
record.Set("public_key", string(pubPem))
|
record.Set("public_key", string(pubPem))
|
||||||
record.Set("private_key", privEncrypted)
|
record.Set("private_key", privEncrypted)
|
||||||
record.Set("user", u.Id)
|
record.Set("user", u.Id)
|
||||||
@@ -147,7 +147,7 @@ func TrailFromActivity(activity pub.Activity, app core.App, actor *core.Record)
|
|||||||
// the local record; a remote actor must not be able to reference or attach
|
// the local record; a remote actor must not be able to reference or attach
|
||||||
// side effects (feeds/shares/notifications) to local content by id.
|
// side effects (feeds/shares/notifications) to local content by id.
|
||||||
if IsLocalIRI(iri) {
|
if IsLocalIRI(iri) {
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
return nil, fmt.Errorf("refusing remote activity referencing local trail %q", iri)
|
return nil, fmt.Errorf("refusing remote activity referencing local trail %q", iri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +440,7 @@ func ListFromActivity(activity pub.Activity, app core.App, actor *core.Record) (
|
|||||||
|
|
||||||
// Own content must never be ingested as if it were remote (see TrailFromActivity).
|
// Own content must never be ingested as if it were remote (see TrailFromActivity).
|
||||||
if IsLocalIRI(iri) {
|
if IsLocalIRI(iri) {
|
||||||
if !actor.GetBool("isLocal") {
|
if !actor.GetBool("is_local") {
|
||||||
return nil, fmt.Errorf("refusing remote activity referencing local list %q", iri)
|
return nil, fmt.Errorf("refusing remote activity referencing local list %q", iri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record, includeShares bool) (map[string]interface{}, error) {
|
func documentFromTrailRecord(r *core.Record, author *core.Record, includeShares bool) (map[string]interface{}, error) {
|
||||||
photos := r.GetStringSlice("photos")
|
photos := r.GetStringSlice("photos")
|
||||||
thumbnail := ""
|
thumbnail := ""
|
||||||
if len(photos) > 0 {
|
if len(photos) > 0 {
|
||||||
@@ -42,7 +42,7 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
|
|||||||
bounds := getStoredBounds(r)
|
bounds := getStoredBounds(r)
|
||||||
|
|
||||||
domain := ""
|
domain := ""
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
domain = author.GetString("domain")
|
domain = author.GetString("domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
|
|||||||
totalDuration := 0.0
|
totalDuration := 0.0
|
||||||
trails := len(r.GetStringSlice("trails"))
|
trails := len(r.GetStringSlice("trails"))
|
||||||
|
|
||||||
if r.GetString("iri") != "" && !author.GetBool("isLocal") {
|
if r.GetString("iri") != "" && !author.GetBool("is_local") {
|
||||||
doc, err := documentFromRemoteRecord(r, "lists")
|
doc, err := documentFromRemoteRecord(r, "lists")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
totalElevationGain = doc["elevation_gain"].(float64)
|
totalElevationGain = doc["elevation_gain"].(float64)
|
||||||
@@ -181,7 +181,7 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
|
|||||||
}
|
}
|
||||||
|
|
||||||
domain := ""
|
domain := ""
|
||||||
if !author.GetBool("isLocal") {
|
if !author.GetBool("is_local") {
|
||||||
domain = author.GetString("domain")
|
domain = author.GetString("domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +222,21 @@ func documentFromListRecord(r *core.Record, author *core.Record, includeShares b
|
|||||||
return document, nil
|
return document, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func documentFromActorRecord(r *core.Record) (map[string]any, error) {
|
||||||
|
|
||||||
|
document := map[string]any{
|
||||||
|
"id": r.Id,
|
||||||
|
"username": r.GetString("username"),
|
||||||
|
"preferred_username": r.GetString("preferred_username"),
|
||||||
|
"domain": r.GetString("domain"),
|
||||||
|
"iri": r.GetString("iri"),
|
||||||
|
"icon": r.GetString("icon"),
|
||||||
|
"is_local": r.GetBool("is_local"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return document, nil
|
||||||
|
}
|
||||||
|
|
||||||
func documentFromRemoteRecord(r *core.Record, index string) (map[string]any, error) {
|
func documentFromRemoteRecord(r *core.Record, index string) (map[string]any, error) {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
@@ -309,7 +324,7 @@ func IndexTrails(app core.App, trails []*core.Record, client meilisearch.Service
|
|||||||
|
|
||||||
author := r.ExpandedOne("author")
|
author := r.ExpandedOne("author")
|
||||||
|
|
||||||
doc, err := documentFromTrailRecord(app, r, author, true)
|
doc, err := documentFromTrailRecord(r, author, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -334,7 +349,7 @@ func UpdateTrail(app core.App, r *core.Record, author *core.Record, client meili
|
|||||||
return fmt.Errorf("meilisearch update trail: failed to expand category: %v", errs)
|
return fmt.Errorf("meilisearch update trail: failed to expand category: %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := documentFromTrailRecord(app, r, author, false)
|
doc, err := documentFromTrailRecord(r, author, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -424,6 +439,37 @@ func UpdateList(app core.App, r *core.Record, author *core.Record, client meilis
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IndexActors(actors []*core.Record, client meilisearch.ServiceManager) error {
|
||||||
|
documents := make([]map[string]any, len(actors))
|
||||||
|
|
||||||
|
for i, r := range actors {
|
||||||
|
|
||||||
|
doc, err := documentFromActorRecord(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
documents[i] = doc
|
||||||
|
}
|
||||||
|
if _, err := client.Index("actors").AddDocuments(documents, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateActor(r *core.Record, client meilisearch.ServiceManager) error {
|
||||||
|
documents, err := documentFromActorRecord(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = client.Index("actors").UpdateDocuments(documents, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateListShares(listId string, shares []string, client meilisearch.ServiceManager) error {
|
func UpdateListShares(listId string, shares []string, client meilisearch.ServiceManager) error {
|
||||||
documents := []map[string]interface{}{
|
documents := []map[string]interface{}{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func SendNotification(app core.App, notification Notification, recipient *core.R
|
|||||||
if notification.Author == recipient.Id {
|
if notification.Author == recipient.Id {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !recipient.GetBool("isLocal") {
|
if !recipient.GetBool("is_local") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
permissions, err := getNotificationPermissions(app, recipient.GetString("user"), notification.Type)
|
permissions, err := getNotificationPermissions(app, recipient.GetString("user"), notification.Type)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { Actor } from '$lib/models/activitypub/actor'
|
|||||||
import { normalizeLocale } from '$lib/i18n/locales'
|
import { normalizeLocale } from '$lib/i18n/locales'
|
||||||
import { handleError } from '$lib/util/api_util'
|
import { handleError } from '$lib/util/api_util'
|
||||||
|
|
||||||
|
const SEARCH_TOKEN_VERSION = 1;
|
||||||
|
|
||||||
function csrf(allowedPaths: string[]): Handle {
|
function csrf(allowedPaths: string[]): Handle {
|
||||||
return async ({ event, resolve }) => {
|
return async ({ event, resolve }) => {
|
||||||
@@ -85,12 +86,12 @@ const auth: Handle = async ({ event, resolve }) => {
|
|||||||
const currentUserId = pb.authStore.record?.id || 'public';
|
const currentUserId = pb.authStore.record?.id || 'public';
|
||||||
|
|
||||||
if (meiliCookie) {
|
if (meiliCookie) {
|
||||||
const [token, ownerId] = meiliCookie.split('|');
|
const [token, ownerId, version] = meiliCookie.split('|');
|
||||||
|
|
||||||
if (ownerId === currentUserId) {
|
if (ownerId === currentUserId && Number(version) === SEARCH_TOKEN_VERSION) {
|
||||||
meilisearchToken = token;
|
meilisearchToken = token;
|
||||||
} else {
|
} else {
|
||||||
// Identity mismatch (e.g. just logged in/out)
|
// Identity mismatch (e.g. just logged in/out) or stale token version
|
||||||
event.cookies.delete('meilisearch_token', { path: '/' });
|
event.cookies.delete('meilisearch_token', { path: '/' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,7 @@ const auth: Handle = async ({ event, resolve }) => {
|
|||||||
try {
|
try {
|
||||||
const tokenResponse = await pb.send("/search/token", { method: "GET", fetch: event.fetch });
|
const tokenResponse = await pb.send("/search/token", { method: "GET", fetch: event.fetch });
|
||||||
meilisearchToken = tokenResponse.token
|
meilisearchToken = tokenResponse.token
|
||||||
event.cookies.set('meilisearch_token', `${meilisearchToken}|${currentUserId}`, {
|
event.cookies.set('meilisearch_token', `${meilisearchToken}|${currentUserId}|${SEARCH_TOKEN_VERSION}`, {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
maxAge: 60 * 60 * 24,
|
maxAge: 60 * 60 * 24,
|
||||||
@@ -143,7 +144,7 @@ const auth: Handle = async ({ event, resolve }) => {
|
|||||||
|
|
||||||
if (pb.authStore.record) {
|
if (pb.authStore.record) {
|
||||||
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.record.id}"`, { requestKey: null })
|
settings = await pb.collection('settings').getFirstListItem<Settings>(`user="${pb.authStore.record.id}"`, { requestKey: null })
|
||||||
actor = await pb.collection("activitypub_actors").getFirstListItem(`isLocal=1&&user='${pb.authStore.record.id}'`)
|
actor = await pb.collection("activitypub_actors").getFirstListItem(`is_local=1&&user='${pb.authStore.record.id}'`)
|
||||||
}
|
}
|
||||||
const meiliHost = env.MEILI_URL;
|
const meiliHost = env.MEILI_URL;
|
||||||
if (!meiliHost) {
|
if (!meiliHost) {
|
||||||
@@ -190,4 +191,4 @@ const removeLinkFromHeaders: Handle =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const handle = sequence(csrf(['/api/v1']), auth, removeLinkFromHeaders)
|
export const handle = sequence(csrf(['/api/v1']), auth, removeLinkFromHeaders)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Actor } from "$lib/models/activitypub/actor";
|
import type { Actor, ActorSearchResult } from "$lib/models/activitypub/actor";
|
||||||
import { searchActors } from "$lib/stores/search_store";
|
import { searchActors } from "$lib/stores/search_store";
|
||||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const actors: Actor[] = await searchActors(q, includeSelf);
|
const actors: ActorSearchResult[] = await searchActors(q, includeSelf);
|
||||||
searchItems = actors.map((a) => ({
|
searchItems = actors.map((a) => ({
|
||||||
text: a.username,
|
text: a.username,
|
||||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
description: `@${a.preferred_username}${a.is_local ? "" : "@" + a.domain}`,
|
||||||
value: a,
|
value: a,
|
||||||
icon: "user",
|
icon: "user",
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import Toggle from "./toggle.svelte";
|
import Toggle from "./toggle.svelte";
|
||||||
import type { SearchItem } from "./search.svelte";
|
import type { SearchItem } from "./search.svelte";
|
||||||
import type { SuggestionProps } from "@tiptap/suggestion";
|
import type { SuggestionProps } from "@tiptap/suggestion";
|
||||||
import type { Actor } from "$lib/models/activitypub/actor";
|
import type { Actor, ActorSearchResult } from "$lib/models/activitypub/actor";
|
||||||
import { searchActors } from "$lib/stores/search_store";
|
import { searchActors } from "$lib/stores/search_store";
|
||||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||||
|
|
||||||
@@ -103,13 +103,13 @@
|
|||||||
allowToIncludeChar: true,
|
allowToIncludeChar: true,
|
||||||
items: async ({ query }) => {
|
items: async ({ query }) => {
|
||||||
try {
|
try {
|
||||||
const actors: Actor[] = await searchActors(
|
const actors: ActorSearchResult[] = await searchActors(
|
||||||
query,
|
query,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
return actors.map((a) => ({
|
return actors.map((a) => ({
|
||||||
text: a.username!,
|
text: a.username!,
|
||||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
description: `@${a.preferred_username}${a.is_local ? "" : "@" + a.domain}`,
|
||||||
value: a,
|
value: a,
|
||||||
icon:
|
icon:
|
||||||
a.icon ||
|
a.icon ||
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
return (_: Event, item: SearchItem) => {
|
return (_: Event, item: SearchItem) => {
|
||||||
props.command({
|
props.command({
|
||||||
id: item.value.iri,
|
id: item.value.iri,
|
||||||
label: `${item.value.preferred_username}${item.value.isLocal ? "" : "@" + item.value.domain}`,
|
label: `${item.value.preferred_username}${item.value.is_local ? "" : "@" + item.value.domain}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function shareList(item: SelectItem) {
|
async function shareList(item: SelectItem) {
|
||||||
if (!item.value.isLocal && !list.public) {
|
if (!item.value.is_local && !list.public) {
|
||||||
displayShareError = true;
|
displayShareError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
<p>
|
<p>
|
||||||
{`@${share.expand.actor.preferred_username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
{`@${share.expand.actor.preferred_username}${share.expand.actor.is_local ? "" : "@" + share.expand.actor.domain}`}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
class="basis-full text-sm text-center text-gray-500"
|
class="basis-full text-sm text-center text-gray-500"
|
||||||
@@ -170,13 +170,13 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
class:tooltip={!share.expand.actor.isLocal}
|
class:tooltip={!share.expand.actor.is_local}
|
||||||
data-title={$_("remote-users-cannot-edit")}
|
data-title={$_("remote-users-cannot-edit")}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
bind:value={share.permission}
|
bind:value={share.permission}
|
||||||
items={permissionSelectItems}
|
items={permissionSelectItems}
|
||||||
disabled={!share.expand.actor.isLocal}
|
disabled={!share.expand.actor.is_local}
|
||||||
onchange={(value) =>
|
onchange={(value) =>
|
||||||
updateSharePermission(share, value)}
|
updateSharePermission(share, value)}
|
||||||
></Select>
|
></Select>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
<p class="font-semibold text-base">
|
<p class="font-semibold text-base">
|
||||||
{`@${share.expand.actor.username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
{`@${share.expand.actor.username}${share.expand.actor.is_local ? "" : "@" + share.expand.actor.domain}`}
|
||||||
</p>
|
</p>
|
||||||
<span class="mx-2 text-sm text-gray-500"
|
<span class="mx-2 text-sm text-gray-500"
|
||||||
>{$_("can")}</span
|
>{$_("can")}</span
|
||||||
|
|||||||
@@ -190,13 +190,13 @@
|
|||||||
<p
|
<p
|
||||||
class="tooltip flex justify-center"
|
class="tooltip flex justify-center"
|
||||||
data-title="@{log.expand.author.preferred_username}{log.expand.author
|
data-title="@{log.expand.author.preferred_username}{log.expand.author
|
||||||
.isLocal
|
.is_local
|
||||||
? ''
|
? ''
|
||||||
: '@' + log.expand.author.domain}"
|
: '@' + log.expand.author.domain}"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/profile/@{log.expand.author.preferred_username?.toLowerCase()}{log
|
href="/profile/@{log.expand.author.preferred_username?.toLowerCase()}{log
|
||||||
.expand.author.isLocal
|
.expand.author.is_local
|
||||||
? ''
|
? ''
|
||||||
: '@' + log.expand.author.domain}"
|
: '@' + log.expand.author.domain}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
{trail.expand.author.preferred_username}{trail.expand.author
|
{trail.expand.author.preferred_username}{trail.expand.author
|
||||||
.isLocal
|
.is_local
|
||||||
? ""
|
? ""
|
||||||
: "@" + trail.expand.author.domain}
|
: "@" + trail.expand.author.domain}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.value.isLocal && !trail.public) {
|
if (!item.value.is_local && !trail.public) {
|
||||||
displayShareError = true;
|
displayShareError = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -167,11 +167,11 @@
|
|||||||
<img
|
<img
|
||||||
class="rounded-full w-8 aspect-square mr-2"
|
class="rounded-full w-8 aspect-square mr-2"
|
||||||
src={share.expand.actor.icon ||
|
src={share.expand.actor.icon ||
|
||||||
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.username}&backgroundType=gradientLinear`}
|
`https://api.dicebear.com/7.x/initials/svg?seed=${share.expand.actor.preferred_username}&backgroundType=gradientLinear`}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
<p>
|
<p>
|
||||||
{`@${share.expand.actor.username}${share.expand.actor.isLocal ? "" : "@" + share.expand.actor.domain}`}
|
{`@${share.expand.actor.username}${share.expand.actor.is_local ? "" : "@" + share.expand.actor.domain}`}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
class="basis-full text-sm text-gray-500 text-end"
|
class="basis-full text-sm text-gray-500 text-end"
|
||||||
@@ -179,13 +179,13 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
class:tooltip={!share.expand.actor.isLocal}
|
class:tooltip={!share.expand.actor.is_local}
|
||||||
data-title={$_("remote-users-cannot-edit")}
|
data-title={$_("remote-users-cannot-edit")}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={share.permission}
|
value={share.permission}
|
||||||
items={permissionSelectItems}
|
items={permissionSelectItems}
|
||||||
disabled={!share.expand.actor.isLocal}
|
disabled={!share.expand.actor.is_local}
|
||||||
onchange={(value) =>
|
onchange={(value) =>
|
||||||
updateSharePermission(share, value)}
|
updateSharePermission(share, value)}
|
||||||
></Select>
|
></Select>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
{#if trail.expand && trail.expand.author}
|
{#if trail.expand && trail.expand.author}
|
||||||
<div class="author-icon">
|
<div class="author-icon">
|
||||||
<img
|
<img
|
||||||
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} @${trail.expand.author.preferred_username}${trail.expand.author.isLocal ? "" : "@" + trail.expand.author.domain}`}
|
title={`${trail.public ? $_("public") + " " : ""}${$_("by")} @${trail.expand.author.preferred_username}${trail.expand.author.is_local ? "" : "@" + trail.expand.author.domain}`}
|
||||||
class="rounded-full w-5 aspect-square mx-1 inline"
|
class="rounded-full w-5 aspect-square mx-1 inline"
|
||||||
src={trail.expand.author.icon ||
|
src={trail.expand.author.icon ||
|
||||||
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.preferred_username}&backgroundType=gradientLinear`}
|
||||||
|
|||||||
@@ -15,11 +15,21 @@ export interface Actor {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
followers?: string;
|
followers?: string;
|
||||||
following?: string;
|
following?: string;
|
||||||
isLocal: boolean;
|
is_local: boolean;
|
||||||
public_key: string;
|
public_key: string;
|
||||||
last_fetched: string;
|
last_fetched: string;
|
||||||
user?: string
|
user?: string
|
||||||
expand?: {
|
expand?: {
|
||||||
user?: User
|
user?: User
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActorSearchResult {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
preferred_username: string;
|
||||||
|
domain: string;
|
||||||
|
is_local: boolean;
|
||||||
|
iri: string;
|
||||||
|
icon?: string;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { Actor } from "$lib/models/activitypub/actor";
|
import type { ActorSearchResult } from "$lib/models/activitypub/actor";
|
||||||
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
|
import { defaultTrailSearchAttributes, type TrailSearchResult } from "$lib/models/trail";
|
||||||
import { APIError } from "$lib/util/api_util";
|
import { APIError } from "$lib/util/api_util";
|
||||||
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
import type { Hits, MultiSearchParams, MultiSearchResponse, MultiSearchResult, SearchParams, SearchResponse } from "meilisearch";
|
||||||
import type { ListResult } from "pocketbase";
|
|
||||||
|
|
||||||
export type LocationSearchResult = {
|
export type LocationSearchResult = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -262,16 +261,16 @@ export async function searchMulti(options: MultiSearchParams): Promise<MultiSear
|
|||||||
return response.results
|
return response.results
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchActors(q: string, includeSelf: boolean = true): Promise<Actor[]> {
|
export async function searchActors(q: string, includeSelf: boolean = true): Promise<ActorSearchResult[]> {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/v1/search/actor?q=${q}&includeSelf=${includeSelf}`,)
|
const r = await fetch(`/api/v1/search/actor?q=${q}&includeSelf=${includeSelf}`,)
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const response: ListResult<Actor> = await r.json()
|
const response: SearchResponse<ActorSearchResult> = await r.json()
|
||||||
|
|
||||||
return response.items
|
return response.hits
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
||||||
|
|||||||
@@ -725,7 +725,7 @@ export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Pr
|
|||||||
expand: {
|
expand: {
|
||||||
author: {
|
author: {
|
||||||
collectionId: "activitypub_actors",
|
collectionId: "activitypub_actors",
|
||||||
isLocal: (h.domain?.length ?? 0) == 0,
|
is_local: (h.domain?.length ?? 0) == 0,
|
||||||
id: h.author,
|
id: h.author,
|
||||||
icon: h.author_avatar,
|
icon: h.author_avatar,
|
||||||
preferred_username: h.author_name,
|
preferred_username: h.author_name,
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ export function splitUsername(handle: string, localDomain?: string) {
|
|||||||
return [user, domain]
|
return [user, domain]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidPubHandle(handle: string): boolean {
|
||||||
|
// Regex breakdown:
|
||||||
|
// ^@? - Optional leading '@'
|
||||||
|
// [a-zA-Z0-9_.-]+ - Username: alphanumeric, underscores, dots, hyphens (minimum 1 char)
|
||||||
|
// @ - Literal '@' separator
|
||||||
|
// ([a-zA-Z0-9-]+\.)+ - Domain segments (e.g., "mastodon.")
|
||||||
|
// [a-zA-Z]{2,}$ - TLD (e.g., "social", "com" - minimum 2 chars)
|
||||||
|
const activityPubRegex = /^@?[a-zA-Z0-9_.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
|
||||||
|
|
||||||
|
return activityPubRegex.test(handle);
|
||||||
|
}
|
||||||
|
|
||||||
export function isRemoteHandle(handle: string, origin: string) {
|
export function isRemoteHandle(handle: string, origin: string) {
|
||||||
const [, domain] = splitUsername(handle, origin);
|
const [, domain] = splitUsername(handle, origin);
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
const actors = await searchActors(q);
|
const actors = await searchActors(q);
|
||||||
searchDropdownItems = actors.map((a) => ({
|
searchDropdownItems = actors.map((a) => ({
|
||||||
text: a.username,
|
text: a.username,
|
||||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
description: `@${a.preferred_username}${a.is_local ? "" : "@" + a.domain}`,
|
||||||
value: a,
|
value: a,
|
||||||
icon:
|
icon:
|
||||||
a.icon ||
|
a.icon ||
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
goto(`/lists/${item.value}`);
|
goto(`/lists/${item.value}`);
|
||||||
} else if (item.value.preferred_username) {
|
} else if (item.value.preferred_username) {
|
||||||
goto(
|
goto(
|
||||||
`/profile/@${item.value.preferred_username}${item.value.isLocal ? "" : "@" + item.value.domain}`,
|
`/profile/@${item.value.preferred_username}${item.value.is_local ? "" : "@" + item.value.domain}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
goto(`/map/?lat=${item.value.lat}&lon=${item.value.lon}`);
|
goto(`/map/?lat=${item.value.lat}&lon=${item.value.lon}`);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
|
|
||||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||||
|
|
||||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=1`)
|
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=1`)
|
||||||
const user: UserAnonymous = await event.locals.pb.collection("users_anonymous").getOne(actor.user!)
|
const user: UserAnonymous = await event.locals.pb.collection("users_anonymous").getOne(actor.user!)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
|
|
||||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||||
|
|
||||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
|
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=true`)
|
||||||
|
|
||||||
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `followee='${actor.id}'&&status='accepted'`, expand: "follower" })
|
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `followee='${actor.id}'&&status='accepted'`, expand: "follower" })
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
|
|
||||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||||
|
|
||||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
|
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=true`)
|
||||||
|
|
||||||
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `follower='${actor.id}'&&status='accepted'`, expand: "followee" })
|
const followers: ListResult<Follow> = await event.locals.pb.collection("follows").getList(intPage, 10, { sort: "-created", filter: `follower='${actor.id}'&&status='accepted'`, expand: "followee" })
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
const [username, domain] = splitUsername(fullUsername, env.ORIGIN)
|
||||||
|
|
||||||
|
|
||||||
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&isLocal=true`)
|
const actor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`preferred_username:lower='${username?.toLowerCase()}'&&is_local=true`)
|
||||||
|
|
||||||
const filter = `actor='${actor.iri}'&&type='Create'${safeSearchParams.filter ? '&&' + safeSearchParams.filter : ''}`
|
const filter = `actor='${actor.iri}'&&type='Create'${safeSearchParams.filter ? '&&' + safeSearchParams.filter : ''}`
|
||||||
const activities: ListResult<Activity> = await event.locals.pb.collection("activitypub_activities").getList(page, perPage, { sort: safeSearchParams.sort ?? "-created", filter })
|
const activities: ListResult<Activity> = await event.locals.pb.collection("activitypub_activities").getList(page, perPage, { sort: safeSearchParams.sort ?? "-created", filter })
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function PUT(event: RequestEvent) {
|
|||||||
const followerActor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`user = '${event.locals.user.id}'`)
|
const followerActor: Actor = await event.locals.pb.collection("activitypub_actors").getFirstListItem(`user = '${event.locals.user.id}'`)
|
||||||
const followeeActor: Actor = await event.locals.pb.collection("activitypub_actors").getOne(safeData.followee);
|
const followeeActor: Actor = await event.locals.pb.collection("activitypub_actors").getOne(safeData.followee);
|
||||||
|
|
||||||
const follow = await event.locals.pb.collection("follows").create({ follower: followerActor.id, followee: followeeActor.id, status: followeeActor.isLocal ? "accepted" : "pending" })
|
const follow = await event.locals.pb.collection("follows").create({ follower: followerActor.id, followee: followeeActor.id, status: followeeActor.is_local ? "accepted" : "pending" })
|
||||||
|
|
||||||
return json(follow);
|
return json(follow);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
||||||
|
|
||||||
let feed: ListResult<FeedItem>;
|
let feed: ListResult<FeedItem>;
|
||||||
if (actor.isLocal) {
|
if (actor.is_local) {
|
||||||
feed = await event.locals.pb.collection(Collection.profile_feed)
|
feed = await event.locals.pb.collection(Collection.profile_feed)
|
||||||
.getList<FeedItem>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `actor='${actor.id}'` })
|
.getList<FeedItem>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `actor='${actor.id}'` })
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export async function POST(event: RequestEvent) {
|
|||||||
const data = await event.request.json()
|
const data = await event.request.json()
|
||||||
|
|
||||||
let r: SearchResponse<ListSearchResult>;
|
let r: SearchResponse<ListSearchResult>;
|
||||||
if (actor.isLocal) {
|
if (actor.is_local) {
|
||||||
r = await event.locals.ms.index("lists").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
|
r = await event.locals.ms.index("lists").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
|
||||||
} else {
|
} else {
|
||||||
const origin = new URL(actor.iri).origin
|
const origin = new URL(actor.iri).origin
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function GET(event: RequestEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let summitLogs: SummitLog[];
|
let summitLogs: SummitLog[];
|
||||||
if (actor.isLocal) {
|
if (actor.is_local) {
|
||||||
summitLogs = await event.locals.pb.collection(Collection.summit_logs)
|
summitLogs = await event.locals.pb.collection(Collection.summit_logs)
|
||||||
.getFullList<SummitLog>(safeSearchParams.page, { ...safeSearchParams })
|
.getFullList<SummitLog>(safeSearchParams.page, { ...safeSearchParams })
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function POST(event: RequestEvent) {
|
|||||||
const data = await event.request.json()
|
const data = await event.request.json()
|
||||||
|
|
||||||
let r: SearchResponse<TrailSearchResult>;
|
let r: SearchResponse<TrailSearchResult>;
|
||||||
if (actor.isLocal) {
|
if (actor.is_local) {
|
||||||
r = await event.locals.ms.index("trails").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
|
r = await event.locals.ms.index("trails").search(data.q, { ...data.options, filter: `author = ${actor.id}` });
|
||||||
} else {
|
} else {
|
||||||
const origin = new URL(actor.iri).origin
|
const origin = new URL(actor.iri).origin
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import type { Actor } from '$lib/models/activitypub/actor';
|
import type { Actor, ActorSearchResult } from '$lib/models/activitypub/actor';
|
||||||
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
import { getActorResponseForHandle } from '$lib/util/activitypub_server_util';
|
||||||
import { splitUsername } from '$lib/util/activitypub_util';
|
import { isValidPubHandle, splitUsername } from '$lib/util/activitypub_util';
|
||||||
import { handleError } from '$lib/util/api_util';
|
import { handleError } from '$lib/util/api_util';
|
||||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||||
import { ClientResponseError, type ListResult } from "pocketbase"
|
import { ClientResponseError, type ListResult } from "pocketbase"
|
||||||
|
import type { SearchResponse } from "meilisearch";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/v1/search/actor:
|
* /api/v1/search/actor:
|
||||||
* get:
|
* get:
|
||||||
* summary: Search actors
|
* summary: Search actors
|
||||||
* description: Searches for ActivityPub actors by username, combining local and federated results
|
* description: |
|
||||||
|
* Searches for ActivityPub actors by username. If the query is a valid
|
||||||
|
* federated handle (e.g. `user@domain.tld`), it attempts a direct
|
||||||
|
* ActivityPub lookup first and returns that result immediately. If the
|
||||||
|
* handle lookup fails, or if the query is not a handle, it falls back to
|
||||||
|
* a local Meilisearch index query. Returns a Meilisearch-shaped
|
||||||
|
* `SearchResponse` in both cases.
|
||||||
* tags:
|
* tags:
|
||||||
* - Search
|
* - Search
|
||||||
* parameters:
|
* parameters:
|
||||||
@@ -19,55 +26,126 @@ import { ClientResponseError, type ListResult } from "pocketbase"
|
|||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
|
* description: |
|
||||||
|
* Search query. Can be a plain username substring or a fully-qualified
|
||||||
|
* ActivityPub handle (`user@domain.tld`). Handles trigger a federated
|
||||||
|
* lookup before falling back to local search.
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 3
|
||||||
|
* description: |
|
||||||
|
* Maximum number of results to return from the local index. Has no
|
||||||
|
* effect when a federated handle is resolved successfully (always
|
||||||
|
* returns exactly one hit).
|
||||||
* - in: query
|
* - in: query
|
||||||
* name: includeSelf
|
* name: includeSelf
|
||||||
* schema:
|
* schema:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
|
* default: true
|
||||||
|
* description: |
|
||||||
|
* When `false` and the request is authenticated, the authenticated
|
||||||
|
* user's own actor is excluded from local search results.
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Array of matching actors
|
* description: |
|
||||||
|
* Meilisearch-shaped response containing matching actors. The `hits`
|
||||||
|
* array contains actor objects with `id`, `domain`, `is_local`,
|
||||||
|
* `preferred_username`, `username`, and `icon` fields.
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: array
|
* type: object
|
||||||
* items:
|
* properties:
|
||||||
* type: object
|
* hits:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* domain:
|
||||||
|
* type: string
|
||||||
|
* is_local:
|
||||||
|
* type: boolean
|
||||||
|
* preferred_username:
|
||||||
|
* type: string
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* icon:
|
||||||
|
* type: string
|
||||||
|
* query:
|
||||||
|
* type: string
|
||||||
|
* processingTimeMs:
|
||||||
|
* type: integer
|
||||||
|
* estimatedTotalHits:
|
||||||
|
* type: integer
|
||||||
|
* totalHits:
|
||||||
|
* type: integer
|
||||||
|
* totalPages:
|
||||||
|
* type: integer
|
||||||
|
* page:
|
||||||
|
* type: integer
|
||||||
* 400:
|
* 400:
|
||||||
* description: Bad Request
|
* description: Missing required `q` parameter.
|
||||||
|
* 404:
|
||||||
|
* description: Federated fetch failed with a network error.
|
||||||
* 500:
|
* 500:
|
||||||
* description: Internal Server Error
|
* description: Internal server error.
|
||||||
*/
|
*/
|
||||||
export async function GET(event: RequestEvent) {
|
export async function GET(event: RequestEvent) {
|
||||||
|
if (!event.locals.user) {
|
||||||
|
return error(401, "Unauthorized")
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!event.url.searchParams.has("q")) {
|
if (!event.url.searchParams.has("q")) {
|
||||||
throw new ClientResponseError({ status: 400, response: "Bad request" });
|
return error(404, "Bad request: missing required parameter 'q'")
|
||||||
|
|
||||||
}
|
}
|
||||||
const q = event.url.searchParams.get("q")
|
const q = event.url.searchParams.get("q")!
|
||||||
|
const limit = event.url.searchParams.get("limit")
|
||||||
|
|
||||||
const [user, domain] = splitUsername(q!)
|
if (isValidPubHandle(q)) {
|
||||||
|
try {
|
||||||
|
const { actor } = await getActorResponseForHandle(event, q!);
|
||||||
|
|
||||||
let filter = `username~'${user}'`;
|
const actorSearchResult = <ActorSearchResult>{
|
||||||
|
id: actor.id,
|
||||||
|
domain: actor.domain,
|
||||||
|
is_local: actor.is_local,
|
||||||
|
preferred_username: actor.preferred_username,
|
||||||
|
username: actor.username,
|
||||||
|
iri: actor.iri,
|
||||||
|
icon: actor.icon
|
||||||
|
};
|
||||||
|
|
||||||
|
return json(<SearchResponse>{
|
||||||
|
hits: [actorSearchResult],
|
||||||
|
processingTimeMs: 0,
|
||||||
|
query: q,
|
||||||
|
estimatedTotalHits: 1,
|
||||||
|
totalHits: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// Actor could not be found via the handle
|
||||||
|
// At least search our local registry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filterText = "";
|
||||||
|
|
||||||
if (event.url.searchParams.get("includeSelf") == "false" && event.locals.pb.authStore.record) {
|
if (event.url.searchParams.get("includeSelf") == "false" && event.locals.pb.authStore.record) {
|
||||||
filter += `&& id != "${event.locals.pb.authStore.record.actor}"`
|
filterText = `id != ${event.locals.pb.authStore.record.actor}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await event.locals.pb.collection("activitypub_actors").getList<Actor>(1, 3, { filter: filter })
|
const r = await event.locals.ms.index("actors").search(q, { filter: filterText, limit: limit ?? 3 });
|
||||||
|
|
||||||
try {
|
|
||||||
const { actor } = await getActorResponseForHandle(event, q!);
|
|
||||||
|
|
||||||
if (!response.items.find(i => i.iri == actor.iri)) {
|
return json(r)
|
||||||
response.items.push(actor)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return json({ items: response.items })
|
|
||||||
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
<ul class="space-y-4">
|
<ul class="space-y-4">
|
||||||
{#each follows.items as follow}
|
{#each follows.items as follow}
|
||||||
<a
|
<a
|
||||||
href="/profile/@{follow.preferred_username}{follow.isLocal
|
href="/profile/@{follow.preferred_username}{follow.is_local
|
||||||
? ''
|
? ''
|
||||||
: '@' + follow.domain}"
|
: '@' + follow.domain}"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user