Federation (#327)
* initial commit federation * more federation * more federation * more federation * completes follow, accept, undo * add trail create activity * process trail create activity * more trail create activity * adds missing endpoints * adds update and delete activity * adds comment activities * adds activities back * adds summit logs activities * deletes follow counts table * fixes migrations * fixes migrations * fixes migrations * fixes migrations * adds remote profiles * ctd * ctd * we are getting closer... * adds public summit logs * fixes federated trails in lists * adds remote lists * adds list activites * fixes lists * adds notifications * adds iri redirect * fixes comments and summitlogs * fixes follows and profiles * adds asynchronous send * fixes list search * adds encryption key * bug fixes * fixes activity signing * removes custon activity object types * adds html editor * fixes html editor * fixes display issues on mastodon * adds federated sharing * finishes announcements * fixes small summit log issues * adds trail likes * finalizes likes * fixes images for komoot * add disable federation option * adds private profiles * updates docs * adds federated comments * adds trail and comments actvitiypub routes * adds mentions to editor * adds mentions to trails, comments, summit logs * updates theme * updates theme * update docs * update docs * updates docs * fixes various frontend problems * fixes activitypub follows api * updates docs * updates docs --------- Co-authored-by: Christian Beutel <>
This commit is contained in:
207
db/federation/activity.go
Normal file
207
db/federation/activity.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
|
||||
"sync"
|
||||
|
||||
"github.com/go-ap/jsonld"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
func PostActivity(app core.App, actor *core.Record, activity *pub.Activity, recipients []string) error {
|
||||
encryptionKey := os.Getenv("POCKETBASE_ENCRYPTION_KEY")
|
||||
if len(encryptionKey) == 0 {
|
||||
return fmt.Errorf("POCKETBASE_ENCRYPTION_KEY not set")
|
||||
}
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
algs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||
postHeaders := []string{"(request-target)", "Date", "Digest", "Content-Type", "Host"}
|
||||
expiresIn := 60
|
||||
|
||||
body, err := jsonld.WithContext(
|
||||
jsonld.IRI(pub.ActivityBaseURI),
|
||||
jsonld.IRI(pub.SecurityContextURI),
|
||||
).Marshal(activity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decryptedPrivateKey, err := security.Decrypt(actor.GetString("private_key"), encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(decryptedPrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubID := actor.GetString("iri") + "#main-key"
|
||||
|
||||
client := &http.Client{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
sem := semaphore.NewWeighted(5) // Limit to 5 concurrent sends
|
||||
|
||||
slices.Sort(recipients)
|
||||
uniqueRecipients := slices.Compact(recipients)
|
||||
|
||||
for _, v := range uniqueRecipients {
|
||||
|
||||
wg.Add(1)
|
||||
go func(inbox string) {
|
||||
defer wg.Done()
|
||||
|
||||
signer, _, err := httpsig.NewSigner(algs, httpsig.DigestSha256, postHeaders, httpsig.Signature, int64(expiresIn))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := sem.Acquire(context.Background(), 1); err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Semaphore acquire failed: %s", err))
|
||||
return
|
||||
}
|
||||
defer sem.Release(1)
|
||||
|
||||
buf := bytes.NewBuffer(body)
|
||||
req, err := http.NewRequest(http.MethodPost, inbox, buf)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Request creation failed: %s", err))
|
||||
return
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/activity+json")
|
||||
req.Header.Add("Date", strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT"))
|
||||
req.Header.Add("Host", req.Host)
|
||||
|
||||
if err := signer.SignRequest(privateKey, pubID, req, body); err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Signing request failed: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
app.Logger().Error(fmt.Sprintf("Error sending request to inbox %s: %s", inbox, err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
app.Logger().Error(fmt.Sprintf("Inbox %s responded with %d: %s", inbox, resp.StatusCode, body))
|
||||
}
|
||||
|
||||
app.Logger().Info(fmt.Sprintf("Sent %s to %s", activity.Type, inbox))
|
||||
}(v)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessActivity(e *core.RequestEvent) error {
|
||||
|
||||
body, err := io.ReadAll(e.Request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var activity pub.Activity
|
||||
activity.UnmarshalJSON(body)
|
||||
|
||||
actor, err := e.App.FindFirstRecordByData("activitypub_actors", "iri", activity.Actor.GetID().String())
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
actor, err = GetActorByIRI(e.App, activity.Actor.GetID().String(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
verified, err := verifySignature(e.Request, actor.GetString("public_key"))
|
||||
if err != nil || !verified {
|
||||
return e.UnauthorizedError("Invalid http signature", err)
|
||||
}
|
||||
|
||||
switch activity.Type {
|
||||
case pub.FollowType:
|
||||
ProcessFollowActivity(e.App, actor, activity)
|
||||
case pub.AcceptType:
|
||||
ProcessAcceptActivity(e.App, actor, activity)
|
||||
case pub.UndoType:
|
||||
ProcessUndoActivity(e.App, actor, activity)
|
||||
case pub.UpdateType:
|
||||
fallthrough
|
||||
case pub.CreateType:
|
||||
ProcessCreateOrUpdateActivity(e.App, actor, activity)
|
||||
case pub.DeleteType:
|
||||
ProcessDeleteActivity(e.App, actor, activity)
|
||||
case pub.AnnounceType:
|
||||
ProcessAnnounceActivity(e.App, actor, activity)
|
||||
case pub.LikeType:
|
||||
ProcessLikeActivity(e.App, actor, activity)
|
||||
}
|
||||
return e.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func verifySignature(req *http.Request, publicKeyPem string) (bool, error) {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return false, fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(publicKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return false, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
}
|
||||
|
||||
req.URL = &url.URL{
|
||||
Path: req.Header.Get("X-Forwarded-Path"),
|
||||
}
|
||||
|
||||
url, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req.Header.Set("Host", url.Host)
|
||||
req.Host = url.Host
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
v, err := httpsig.NewVerifier(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = v.Verify(publicKey, httpsig.RSA_SHA256)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
284
db/federation/actor.go
Normal file
284
db/federation/actor.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
type WebfingerResponse struct {
|
||||
Subject string `json:"subject"`
|
||||
Links []struct {
|
||||
Rel string `json:"rel"`
|
||||
Href string `json:"href"`
|
||||
} `json:"links"`
|
||||
}
|
||||
|
||||
func SplitHandle(handle string) (string, string) {
|
||||
|
||||
cleaned := strings.TrimPrefix(handle, "@")
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
|
||||
if !strings.Contains(cleaned, "@") {
|
||||
return cleaned, ""
|
||||
}
|
||||
|
||||
parts := strings.SplitN(cleaned, "@", 2)
|
||||
user := parts[0]
|
||||
domain := parts[1]
|
||||
|
||||
return user, domain
|
||||
}
|
||||
|
||||
func GetActorByHandle(app core.App, handle string, includeFollows bool) (*core.Record, error) {
|
||||
username, domain := SplitHandle(handle)
|
||||
|
||||
filter := "username={:username}&&"
|
||||
if domain != "" {
|
||||
filter += "domain={:domain}"
|
||||
} else {
|
||||
filter += "isLocal=true"
|
||||
}
|
||||
|
||||
var dbActor *core.Record
|
||||
dbActor, err := app.FindFirstRecordByFilter("activitypub_actors", filter, dbx.Params{"username": username, "domain": domain})
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_actors")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbActor = core.NewRecord(collection)
|
||||
dbActor.Set("isLocal", false)
|
||||
iri, err := iriFromHandle(domain, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbActor.Set("iri", iri)
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assembleActor(dbActor, app, includeFollows)
|
||||
}
|
||||
|
||||
func GetActorByIRI(app core.App, iri string, includeFollows bool) (*core.Record, error) {
|
||||
var dbActor *core.Record
|
||||
dbActor, err := app.FindFirstRecordByFilter("activitypub_actors", "iri={:iri}", dbx.Params{"iri": iri})
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_actors")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbActor = core.NewRecord(collection)
|
||||
dbActor.Set("isLocal", false)
|
||||
dbActor.Set("iri", iri)
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assembleActor(dbActor, app, includeFollows)
|
||||
}
|
||||
|
||||
func iriFromHandle(domain string, username string) (string, error) {
|
||||
client := &http.Client{}
|
||||
|
||||
webfingerURL := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", domain, username, domain)
|
||||
resp, err := client.Get(webfingerURL)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("webfinger request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var wf WebfingerResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&wf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, link := range wf.Links {
|
||||
if link.Rel == "self" {
|
||||
return link.Href, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no iri in response")
|
||||
}
|
||||
|
||||
func assembleActor(dbActor *core.Record, app core.App, includeFollows bool) (*core.Record, error) {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return nil, fmt.Errorf("ORIGIN environment variable not set")
|
||||
}
|
||||
|
||||
if dbActor.GetBool("isLocal") {
|
||||
user, err := app.FindRecordById("users", dbActor.GetString("user"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings, err := app.FindFirstRecordByData("settings", "user", user.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.GetString("avatar") != "" {
|
||||
dbActor.Set("icon", fmt.Sprintf("%s/api/v1/files/users/%s/%s", origin, user.Id, user.GetString("avatar")))
|
||||
}
|
||||
dbActor.Set("summary", settings.GetString("bio"))
|
||||
followerCount, err := app.CountRecords("follows", dbx.NewExp("followee={:user}", dbx.Params{"user": dbActor.Id}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbActor.Set("followerCount", followerCount)
|
||||
followingCount, err := app.CountRecords("follows", dbx.NewExp("follower={:user}", dbx.Params{"user": dbActor.Id}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbActor.Set("followingCount", followingCount)
|
||||
|
||||
dbActor.Set("last_fetched", time.Now())
|
||||
|
||||
privacy := settings.GetString("privacy")
|
||||
result := make(map[string]interface{})
|
||||
json.Unmarshal([]byte(privacy), &result)
|
||||
|
||||
private := result["account"] == "private"
|
||||
|
||||
if private {
|
||||
return nil, fmt.Errorf("profile is private")
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// check if value is still cached
|
||||
twoHoursAgo := time.Now().Add(-2 * time.Hour)
|
||||
if !includeFollows && dbActor.GetDateTime("last_fetched").Time().After(twoHoursAgo) {
|
||||
return dbActor, nil
|
||||
}
|
||||
pubActor, followers, following, err := fetchRemoteActor(dbActor.GetString("iri"), includeFollows)
|
||||
if err != nil {
|
||||
if dbActor.Id != "" {
|
||||
return dbActor, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
icon := ""
|
||||
if pub.IsObject(pubActor.Icon) {
|
||||
iconObject, err := pub.ToObject(pubActor.Icon)
|
||||
if err == nil && iconObject.URL != nil {
|
||||
icon = iconObject.URL.GetID().String()
|
||||
}
|
||||
}
|
||||
|
||||
parsedUrl, err := url.Parse(dbActor.GetString("iri"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domain := strings.TrimPrefix(parsedUrl.Hostname(), "www.")
|
||||
|
||||
dbActor.Set("domain", domain)
|
||||
dbActor.Set("followers", pubActor.Followers.GetID().String())
|
||||
dbActor.Set("inbox", pubActor.Inbox.GetID().String())
|
||||
dbActor.Set("iri", pubActor.GetID().String())
|
||||
dbActor.Set("username", pubActor.Name.String())
|
||||
dbActor.Set("preferred_username", pubActor.PreferredUsername.String())
|
||||
dbActor.Set("following", pubActor.Following.GetID().String())
|
||||
dbActor.Set("summary", pubActor.Summary.String())
|
||||
dbActor.Set("outbox", pubActor.Outbox.GetID().String())
|
||||
dbActor.Set("icon", icon)
|
||||
dbActor.Set("published", pubActor.Published.String())
|
||||
dbActor.Set("public_key", pubActor.PublicKey.PublicKeyPem)
|
||||
dbActor.Set("last_fetched", time.Now())
|
||||
|
||||
if includeFollows {
|
||||
dbActor.Set("followerCount", int(followers.TotalItems))
|
||||
dbActor.Set("followingCount", int(following.TotalItems))
|
||||
}
|
||||
}
|
||||
|
||||
err := app.Save(dbActor)
|
||||
if err != nil && err.Error() == "iri: Value must be unique." {
|
||||
dbActor, err = app.FindFirstRecordByData("activitypub_actors", "iri", dbActor.GetString("iri"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbActor, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dbActor, nil
|
||||
}
|
||||
|
||||
// Fetches an AP actor and optionally followers/following collections
|
||||
func fetchRemoteActor(iri string, includeFollows bool) (*pub.Actor, *pub.OrderedCollection, *pub.OrderedCollection, error) {
|
||||
client := &http.Client{}
|
||||
headers := map[string]string{
|
||||
"Accept": "application/ld+json",
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", iri, nil)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("actor fetch failed: %v", err)
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, nil, fmt.Errorf("actor fetch failed: status %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var pubActor pub.Actor
|
||||
if err := json.NewDecoder(resp.Body).Decode(&pubActor); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var followers, following pub.OrderedCollection
|
||||
|
||||
if includeFollows {
|
||||
// Fetch followers
|
||||
if data, err := fetchCollection(pubActor.Followers.GetID().String(), headers); err == nil {
|
||||
followers = *data
|
||||
}
|
||||
|
||||
// Fetch following
|
||||
if data, err := fetchCollection(pubActor.Following.GetID().String(), headers); err == nil {
|
||||
following = *data
|
||||
}
|
||||
}
|
||||
|
||||
return &pubActor, &followers, &following, nil
|
||||
}
|
||||
|
||||
func fetchCollection(url string, headers map[string]string) (*pub.OrderedCollection, error) {
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("collection fetch failed for %s: %v", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var collection pub.OrderedCollection
|
||||
if err := json.NewDecoder(resp.Body).Decode(&collection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collection, nil
|
||||
}
|
||||
276
db/federation/announce.go
Normal file
276
db/federation/announce.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"pocketbase/util"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
type AnnounceType string
|
||||
|
||||
const (
|
||||
TrailAnnounceType AnnounceType = "trail"
|
||||
ListAnnounceType AnnounceType = "list"
|
||||
)
|
||||
|
||||
func CreateAnnounceActivity(app core.App, record *core.Record, typ AnnounceType) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
var subject *core.Record
|
||||
var object pub.Item
|
||||
var err error
|
||||
if typ == TrailAnnounceType {
|
||||
subject, err = app.FindRecordById("trails", record.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object, err = util.ObjectFromTrail(app, subject, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if typ == ListAnnounceType {
|
||||
subject, err = app.FindRecordById("lists", record.GetString("list"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object, err = util.ObjectFromList(app, subject)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("unknown announce type")
|
||||
}
|
||||
|
||||
subjectActor, err := app.FindRecordById("activitypub_actors", subject.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objectActor, err := app.FindRecordById("activitypub_actors", record.GetString("actor"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := objectActor.GetString("iri")
|
||||
actor := subjectActor.GetString("iri")
|
||||
|
||||
activity := pub.AnnounceNew(pub.IRI(id), object)
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.Actor = pub.IRI(actor)
|
||||
activity.Tag = pub.ItemCollection{
|
||||
pub.Object{
|
||||
Type: pub.NoteType,
|
||||
Name: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "permission")),
|
||||
Content: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, record.GetString("permission"))),
|
||||
},
|
||||
}
|
||||
|
||||
err = PostActivity(app, subjectActor, activity, []string{objectActor.GetString("inbox")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activityRecord := core.NewRecord(collection)
|
||||
activityRecord.Set("id", recordId)
|
||||
activityRecord.Set("iri", id)
|
||||
activityRecord.Set("to", []string{to})
|
||||
activityRecord.Set("type", string(pub.AnnounceType))
|
||||
activityRecord.Set("object", object)
|
||||
activityRecord.Set("actor", actor)
|
||||
activityRecord.Set("published", time.Now())
|
||||
|
||||
return app.Save(activityRecord)
|
||||
}
|
||||
|
||||
// process incoming announce activity
|
||||
func ProcessAnnounceActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
object := activity.Object.GetID().String()
|
||||
|
||||
if strings.Contains(object, "/api/v1/trail") {
|
||||
processTrailAnnounceActivity(app, actor, activity)
|
||||
|
||||
} else if strings.Contains(object, "/api/v1/list") {
|
||||
processListAnnounceActivity(app, actor, activity)
|
||||
} else {
|
||||
return fmt.Errorf("unknown announce type")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func processTrailAnnounceActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
|
||||
objectActor, err := app.FindFirstRecordByData("activitypub_actors", "iri", activity.To[0].GetID().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var trail *core.Record
|
||||
if !actor.GetBool("isLocal") {
|
||||
trail, err = util.TrailFromActivity(activity, app, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
permission := "view"
|
||||
// tags, err := pub.ToItemCollection(activity.Tag)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// for _, tag := range tags.Collection() {
|
||||
// tagObj, err := pub.ToObject(tag)
|
||||
// if err != nil {
|
||||
// continue
|
||||
// }
|
||||
// name := tagObj.Name.First().Value.String()
|
||||
// content := tagObj.Content.First().Value.String()
|
||||
// if name == "permission" {
|
||||
// permission = content
|
||||
// }
|
||||
// }
|
||||
|
||||
record, err := app.FindFirstRecordByFilter("trail_share", "trail={:trailId}&&actor={:actorId}", dbx.Params{"trailId": trail.Id, "actorId": objectActor.Id})
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
collection, err := app.FindCollectionByNameOrId("trail_share")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record = core.NewRecord(collection)
|
||||
record.Set("trail", trail.Id)
|
||||
record.Set("actor", objectActor.Id)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
record.Set("permission", permission)
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
trailUrl, err := url.Parse(activity.Object.GetID().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trailId := path.Base(trailUrl.Path)
|
||||
trail, err = app.FindRecordById("trails", trailId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
notification := util.Notification{
|
||||
Type: util.TrailShare,
|
||||
Metadata: map[string]string{
|
||||
"id": trail.Id,
|
||||
"trail": trail.GetString("name"),
|
||||
"author": fmt.Sprintf("@%s@%s", actor.GetString("username"), actor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
err = util.SendNotification(app, notification, objectActor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processListAnnounceActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
|
||||
objectActor, err := app.FindFirstRecordByData("activitypub_actors", "iri", activity.To[0].GetID().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var list *core.Record
|
||||
if !actor.GetBool("isLocal") {
|
||||
list, err = util.ListFromActivity(activity, app, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record, err := app.FindFirstRecordByFilter("list_share", "list={:listId}&&actor={:actorId}", dbx.Params{"listId": list.Id, "actorId": objectActor.Id})
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
collection, err := app.FindCollectionByNameOrId("list_share")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record = core.NewRecord(collection)
|
||||
record.Set("list", list.Id)
|
||||
record.Set("actor", objectActor.Id)
|
||||
record.Set("permission", "view")
|
||||
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
listUrl, err := url.Parse(activity.Object.GetID().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listId := path.Base(listUrl.Path)
|
||||
list, err = app.FindRecordById("trails", listId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
notification := util.Notification{
|
||||
Type: util.ListShare,
|
||||
Metadata: map[string]string{
|
||||
"id": list.Id,
|
||||
"list": list.GetString("name"),
|
||||
"author": fmt.Sprintf("@%s@%s", actor.GetString("username"), actor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
err = util.SendNotification(app, notification, objectActor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
849
db/federation/create.go
Normal file
849
db/federation/create.go
Normal file
@@ -0,0 +1,849 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pocketbase/util"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func CreateTrailActivity(app core.App, trail *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
if !trail.GetBool("public") {
|
||||
// only broadcast the trail if it is public
|
||||
return nil
|
||||
}
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
mentionedActors, handles, err := ActorsFromMentions(app, trail.GetString("description"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mentions := []string{}
|
||||
cc := pub.ItemCollection{pub.IRI(trailAuthor.GetString("followers"))}
|
||||
tags := pub.ItemCollection{}
|
||||
for i, m := range mentionedActors {
|
||||
inbox := m.GetString("inbox")
|
||||
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
|
||||
mention.Href = pub.IRI(m.GetString("iri"))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
|
||||
tags.Append(mention)
|
||||
|
||||
mentions = append(mentions, inbox)
|
||||
cc.Append(pub.IRI(inbox))
|
||||
}
|
||||
|
||||
trailObject, err := util.ObjectFromTrail(app, trail, &tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activity := pub.ActivityNew(pub.IRI(id), typ, trailObject)
|
||||
activity.Actor = pub.IRI(trailAuthor.GetString("iri"))
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.CC = cc
|
||||
activity.Published = time.Now()
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("to", []string{to})
|
||||
record.Set("cc", cc)
|
||||
record.Set("type", string(typ))
|
||||
record.Set("object", trailObject)
|
||||
record.Set("actor", trailAuthor.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
follows, err := app.FindRecordsByFilter("follows", "followee={:followee}&&status='accepted'", "", -1, 0, dbx.Params{"followee": trailAuthor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients := mentions
|
||||
for _, f := range follows {
|
||||
follower, err := app.FindRecordById("activitypub_actors", f.GetString("follower"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, follower.GetString("inbox"))
|
||||
}
|
||||
|
||||
return PostActivity(app, trailAuthor, activity, recipients)
|
||||
}
|
||||
|
||||
func CreateCommentActivity(app core.App, comment *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
// author of the comment
|
||||
commentAuthor, err := app.FindRecordById("activitypub_actors", comment.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commentTrail, err := app.FindRecordById("trails", comment.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commentTrailAuthor, err := app.FindRecordById("activitypub_actors", commentTrail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activityRecordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, activityRecordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
mentionedActors, handles, err := ActorsFromMentions(app, comment.GetString("text"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients := []string{}
|
||||
tags := pub.ItemCollection{}
|
||||
for i, m := range mentionedActors {
|
||||
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
|
||||
mention.Href = pub.IRI(m.GetString("iri"))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
|
||||
tags.Append(mention)
|
||||
|
||||
recipients = append(recipients, m.GetString("inbox"))
|
||||
}
|
||||
recipients = append(recipients, commentTrailAuthor.GetString("inbox"))
|
||||
|
||||
cc := pub.ItemCollection{}
|
||||
for _, r := range recipients {
|
||||
cc.Append(pub.IRI(r))
|
||||
}
|
||||
|
||||
author := commentAuthor.GetString("iri")
|
||||
|
||||
commentObject, err := util.ObjectFromComment(app, comment, &tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activity := pub.ActivityNew(pub.IRI(id), typ, commentObject)
|
||||
activity.Actor = pub.IRI(author)
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.CC = cc
|
||||
activity.Published = time.Now()
|
||||
activity.Object = commentObject
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", activityRecordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("to", []string{to})
|
||||
record.Set("cc", recipients)
|
||||
record.Set("type", string(typ))
|
||||
record.Set("object", commentObject)
|
||||
record.Set("actor", author)
|
||||
record.Set("published", time.Now())
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return PostActivity(app, commentAuthor, activity, recipients)
|
||||
|
||||
}
|
||||
|
||||
func CreateSummitLogActivity(app core.App, summitLog *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
summitLogAuthor, err := app.FindRecordById("activitypub_actors", summitLog.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var summitLogAuthorId string
|
||||
// first check if we find the trail locally
|
||||
summitLogTrail, err := app.FindRecordById("trails", summitLog.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !summitLogTrail.GetBool("public") {
|
||||
// only broadcast the log if the trail it belongs to is public
|
||||
return nil
|
||||
}
|
||||
summitLogAuthorId = summitLogTrail.GetString("author")
|
||||
|
||||
summitLogTrailAuthor, err := app.FindRecordById("activitypub_actors", summitLogAuthorId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var trailIRI pub.IRI
|
||||
if summitLogTrailAuthor.GetBool("isLocal") {
|
||||
trailId := summitLog.GetString("trail")
|
||||
trailIRI = pub.IRI(fmt.Sprintf("%s/api/v1/trail/%s", origin, trailId))
|
||||
} else {
|
||||
trailIRI = pub.IRI(summitLogTrail.GetString("iri"))
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := pub.ItemCollection{pub.IRI("https://www.w3.org/ns/activitystreams#Public")}
|
||||
|
||||
// someone else created the summit log on the trail -> inform the trail's author
|
||||
if summitLogAuthor.Id != summitLogTrailAuthor.Id {
|
||||
to.Append(pub.IRI(summitLogTrailAuthor.GetString("iri")))
|
||||
}
|
||||
|
||||
mentionedActors, handles, err := ActorsFromMentions(app, summitLog.GetString("text"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mentions := []string{}
|
||||
cc := pub.ItemCollection{pub.IRI(summitLogAuthor.GetString("followers"))}
|
||||
mentionTags := pub.ItemCollection{}
|
||||
for i, m := range mentionedActors {
|
||||
inbox := m.GetString("inbox")
|
||||
mention := pub.MentionNew(pub.IRI(m.GetString("iri")))
|
||||
mention.Href = pub.IRI(m.GetString("iri"))
|
||||
mention.Name = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, handles[i]))
|
||||
mentionTags.Append(mention)
|
||||
|
||||
mentions = append(mentions, inbox)
|
||||
cc.Append(pub.IRI(inbox))
|
||||
}
|
||||
|
||||
photos := summitLog.GetStringSlice("photos")
|
||||
|
||||
gpx := ""
|
||||
if summitLog.GetString("gpx") != "" {
|
||||
gpx = fmt.Sprintf("%s/api/v1/files/summit_logs/%s/%s", origin, summitLog.Id, summitLog.GetString("gpx"))
|
||||
}
|
||||
|
||||
attachments := make(pub.ItemCollection, max(len(photos), 2))
|
||||
for i := range len(photos) {
|
||||
iri := fmt.Sprintf("%s/api/v1/files/summit_logs/%s/%s", origin, summitLog.Id, photos[i])
|
||||
|
||||
attachments[i] = pub.Document{
|
||||
Type: pub.ImageType,
|
||||
MediaType: "image/jpeg",
|
||||
URL: pub.IRI(iri),
|
||||
}
|
||||
}
|
||||
if gpx != "" {
|
||||
attachments.Append(pub.Document{
|
||||
Type: pub.DocumentType,
|
||||
MediaType: "application/xml+gpx",
|
||||
URL: pub.IRI(gpx),
|
||||
})
|
||||
}
|
||||
|
||||
tags := pub.ItemCollection{
|
||||
pub.Object{
|
||||
Type: pub.NoteType,
|
||||
Name: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "elevation_gain")),
|
||||
Content: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("%fm", summitLog.GetFloat("elevation_gain")))),
|
||||
},
|
||||
pub.Object{
|
||||
Type: pub.NoteType,
|
||||
Name: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "elevation_loss")),
|
||||
Content: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("%fm", summitLog.GetFloat("elevation_loss")))),
|
||||
},
|
||||
pub.Object{
|
||||
Type: pub.NoteType,
|
||||
Name: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "distance")),
|
||||
Content: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("%fm", summitLog.GetFloat("distance")))),
|
||||
},
|
||||
pub.Object{
|
||||
Type: pub.NoteType,
|
||||
Name: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, "duration")),
|
||||
Content: pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, fmt.Sprintf("%fm", summitLog.GetFloat("duration")))),
|
||||
},
|
||||
}
|
||||
|
||||
for _, m := range mentionTags {
|
||||
tags.Append(m)
|
||||
}
|
||||
|
||||
logObject := pub.ObjectNew(pub.NoteType)
|
||||
|
||||
logObject.Content = pub.NaturalLanguageValuesNew(pub.LangRefValueNew(pub.NilLangRef, summitLog.GetString("text")))
|
||||
logObject.AttributedTo = pub.IRI(summitLogAuthor.GetString("iri"))
|
||||
logObject.Published = summitLog.GetDateTime("created").Time()
|
||||
logObject.ID = pub.IRI(fmt.Sprintf("%s/api/v1/summit-log/%s", origin, summitLog.Id))
|
||||
logObject.URL = pub.IRI(fmt.Sprintf("%s/trail/view/@%s/%s", origin, summitLogTrailAuthor.GetString("username"), summitLog.GetString("trail")))
|
||||
logObject.InReplyTo = trailIRI
|
||||
logObject.Tag = tags
|
||||
|
||||
logObject.StartTime = summitLog.GetDateTime("date").Time()
|
||||
logObject.Attachment = attachments
|
||||
|
||||
activity := pub.ActivityNew(pub.IRI(id), typ, logObject)
|
||||
activity.Actor = pub.IRI(summitLogAuthor.GetString("iri"))
|
||||
activity.To = to
|
||||
activity.CC = cc
|
||||
activity.Published = time.Now()
|
||||
|
||||
follows, err := app.FindRecordsByFilter("follows", "followee={:followee}&&status='accepted'", "", -1, 0, dbx.Params{"followee": summitLogAuthor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients := mentions
|
||||
|
||||
for _, f := range follows {
|
||||
follower, err := app.FindRecordById("activitypub_actors", f.GetString("follower"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, follower.GetString("inbox"))
|
||||
}
|
||||
|
||||
if summitLogAuthor.Id != summitLogTrailAuthor.Id {
|
||||
recipients = append(recipients, summitLogTrailAuthor.GetString("inbox"))
|
||||
}
|
||||
|
||||
err = PostActivity(app, summitLogAuthor, activity, recipients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("to", to)
|
||||
record.Set("cc", cc)
|
||||
record.Set("type", string(typ))
|
||||
record.Set("object", logObject)
|
||||
record.Set("actor", summitLogAuthor.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
func CreateListActivity(app core.App, list *core.Record, typ pub.ActivityVocabularyType) error {
|
||||
if !list.GetBool("public") {
|
||||
// only broadcast the list if it is public
|
||||
return nil
|
||||
}
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
// author of the list
|
||||
listAuthor, err := app.FindRecordById("activitypub_actors", list.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activityRecordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, activityRecordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
cc := listAuthor.GetString("followers")
|
||||
author := listAuthor.GetString("iri")
|
||||
|
||||
listObject, err := util.ObjectFromList(app, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activity := pub.ActivityNew(pub.IRI(id), typ, listObject)
|
||||
activity.Actor = pub.IRI(author)
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.CC = pub.ItemCollection{pub.IRI(cc)}
|
||||
activity.Published = time.Now()
|
||||
activity.Object = listObject
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
follows, err := app.FindRecordsByFilter("follows", "followee={:followee}&&status='accepted'", "", -1, 0, dbx.Params{"followee": listAuthor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients := []string{}
|
||||
for _, f := range follows {
|
||||
follower, err := app.FindRecordById("activitypub_actors", f.GetString("follower"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, follower.GetString("inbox"))
|
||||
}
|
||||
|
||||
err = PostActivity(app, listAuthor, activity, recipients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", activityRecordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("to", []string{to})
|
||||
record.Set("cc", []string{cc})
|
||||
record.Set("type", string(typ))
|
||||
record.Set("object", listObject)
|
||||
record.Set("actor", author)
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
func ProcessCreateOrUpdateActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
|
||||
var err error
|
||||
if strings.Contains(activity.Object.GetID().String(), "/api/v1/trail") {
|
||||
err = processCreateOrUpdateTrailActivity(activity, app, actor)
|
||||
} else if strings.Contains(activity.Object.GetID().String(), "/api/v1/summit-log") {
|
||||
err = processCreateOrUpdateSummitLogActivity(activity, app, actor)
|
||||
} else if strings.Contains(activity.Object.GetID().String(), "/api/v1/list") {
|
||||
err = processCreateOrUpdateListActivity(activity, app, actor)
|
||||
} else {
|
||||
err = processCreateOrUpdateCommentActivity(activity, app, actor)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func processCreateOrUpdateTrailActivity(activity pub.Activity, app core.App, actor *core.Record) error {
|
||||
|
||||
// no need to do anything if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
trail, err := util.TrailFromActivity(activity, app, actor)
|
||||
|
||||
trailObject, _ := pub.ToObject(activity.Object)
|
||||
|
||||
for _, t := range trailObject.Tag {
|
||||
if t.GetType() == pub.MentionType {
|
||||
mention := t.(*pub.Mention)
|
||||
mentionedActor, err := app.FindFirstRecordByData("activitypub_actors", "iri", mention.Href.GetID().String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
notification := util.Notification{
|
||||
Type: util.TrailMention,
|
||||
Metadata: map[string]string{
|
||||
"id": trail.Id,
|
||||
"author": fmt.Sprintf("@%s@%s", actor.GetString("username"), actor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, mentionedActor)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func processCreateOrUpdateCommentActivity(activity pub.Activity, app core.App, actor *core.Record) error {
|
||||
|
||||
commentObject, err := pub.ToObject(activity.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if commentObject.InReplyTo == nil {
|
||||
return fmt.Errorf("error processing comment: InReplyTo empty")
|
||||
}
|
||||
|
||||
trailUrl, err := url.Parse(commentObject.InReplyTo.GetLink().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trailId := path.Base(trailUrl.Path)
|
||||
|
||||
var trail *core.Record
|
||||
trail, err = app.FindFirstRecordByFilter("trails", "iri={:iri} || id={:id}", dbx.Params{"id": trailId, "iri": commentObject.InReplyTo.GetID().String()})
|
||||
|
||||
// if the trail is not present on this instance fetch it
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
trailObject, err := util.TrailObjectFromIRI(commentObject.InReplyTo.GetLink().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activity := pub.ActivityNew(pub.IRI("new"), pub.CreateType, trailObject)
|
||||
trail, err = util.TrailFromActivity(*activity, app, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// no need to do anything else if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
record, err := app.FindFirstRecordByData("comments", "iri", commentObject.ID.String())
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
collection, err := app.FindCollectionByNameOrId("comments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record = core.NewRecord(collection)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
record.Set("iri", commentObject.ID.String())
|
||||
record.Set("text", commentObject.Content.First().Value)
|
||||
record.Set("author", actor.Id)
|
||||
record.Set("trail", trail.Id)
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send notifications to all mentioned actors
|
||||
for _, t := range commentObject.Tag {
|
||||
if t.GetType() == pub.MentionType {
|
||||
mention := t.(*pub.Mention)
|
||||
mentionedActor, err := app.FindFirstRecordByData("activitypub_actors", "iri", mention.Href.GetID().String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
notification := util.Notification{
|
||||
Type: util.CommentMention,
|
||||
Metadata: map[string]string{
|
||||
"comment": commentObject.Content.First().Value.String(),
|
||||
"trail_id": trail.Id,
|
||||
"trail_name": trail.GetString("name"),
|
||||
"trail_author": fmt.Sprintf("@%s@%s", trailAuthor.GetString("username"), trailAuthor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, mentionedActor)
|
||||
}
|
||||
}
|
||||
if activity.Type == pub.CreateType {
|
||||
// send a notification to the trail author
|
||||
notification := util.Notification{
|
||||
Type: util.TrailComment,
|
||||
Metadata: map[string]string{
|
||||
"comment": commentObject.Content.First().Value.String(),
|
||||
"trail_id": trail.Id,
|
||||
"trail_name": trail.GetString("name"),
|
||||
"trail_author": fmt.Sprintf("@%s@%s", trailAuthor.GetString("username"), trailAuthor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, trailAuthor)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processCreateOrUpdateSummitLogActivity(activity pub.Activity, app core.App, actor *core.Record) error {
|
||||
logObject, err := pub.ToObject(activity.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trailIRI, err := url.Parse(logObject.InReplyTo.GetID().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trailId := path.Base(trailIRI.Path)
|
||||
|
||||
trail, err := app.FindFirstRecordByFilter("trails", "iri={:iri} || id={:id}", dbx.Params{"id": trailId, "iri": logObject.InReplyTo.GetID().String()})
|
||||
// if the trail is not present on this instance fetch it
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
trailObject, err := util.TrailObjectFromIRI(logObject.InReplyTo.GetLink().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activity := pub.ActivityNew(pub.IRI("new"), pub.CreateType, trailObject)
|
||||
trail, err = util.TrailFromActivity(*activity, app, actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newSummitLog := false
|
||||
record, err := app.FindFirstRecordByData("summit_logs", "iri", logObject.ID.String())
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
collection, err := app.FindCollectionByNameOrId("summit_logs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record = core.NewRecord(collection)
|
||||
newSummitLog = true
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// no need to do anything else if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var distance, duration, elevation_gain, elevation_loss float64
|
||||
tags, err := pub.ToItemCollection(logObject.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tag := range tags.Collection() {
|
||||
tagObj, err := pub.ToObject(tag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
content := tagObj.Content.First().Value.String()
|
||||
switch tagObj.Name.First().Value.String() {
|
||||
case "elevation_gain":
|
||||
elevation_gain, err = strconv.ParseFloat(content[:len(content)-1], 64)
|
||||
case "elevation_loss":
|
||||
elevation_loss, err = strconv.ParseFloat(content[:len(content)-1], 64)
|
||||
case "duration":
|
||||
duration, err = strconv.ParseFloat(content[:len(content)-1], 64)
|
||||
case "distance":
|
||||
distance, err = strconv.ParseFloat(content[:len(content)-1], 64)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
record.Set("date", logObject.StartTime)
|
||||
record.Set("text", logObject.Content.First().Value)
|
||||
record.Set("distance", distance)
|
||||
record.Set("duration", duration)
|
||||
record.Set("elevation_gain", elevation_gain)
|
||||
record.Set("elevation_loss", elevation_loss)
|
||||
record.Set("author", actor.Id)
|
||||
record.Set("trail", trail.Id)
|
||||
record.Set("iri", logObject.ID.String())
|
||||
|
||||
if logObject.Attachment != nil {
|
||||
attachments, err := pub.ToItemCollection(logObject.Attachment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
photoURLs := []string{}
|
||||
gpxURL := ""
|
||||
for _, a := range attachments.Collection() {
|
||||
attachment, err := pub.ToObject(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if attachment.Type == pub.DocumentType && attachment.MediaType == "application/xml+gpx" {
|
||||
gpxURL = attachment.URL.GetLink().String()
|
||||
} else if attachment.Type == pub.ImageType {
|
||||
photoURLs = append(photoURLs, attachment.URL.GetLink().String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(photoURLs) > 0 {
|
||||
photos := make([]*filesystem.File, len(photoURLs))
|
||||
for i, purl := range photoURLs {
|
||||
photo, err := filesystem.NewFileFromURL(context.Background(), purl)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
photos[i] = photo
|
||||
}
|
||||
|
||||
record.Set("photos", photos)
|
||||
}
|
||||
|
||||
if gpxURL != "" {
|
||||
gpx, err := filesystem.NewFileFromURL(context.Background(), gpxURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record.Set("gpx", gpx)
|
||||
}
|
||||
}
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send notifications to all mentioned actors
|
||||
for _, t := range logObject.Tag {
|
||||
if t.GetType() == pub.MentionType {
|
||||
mention := t.(*pub.Mention)
|
||||
mentionedActor, err := app.FindFirstRecordByData("activitypub_actors", "iri", mention.Href.GetID().String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
notification := util.Notification{
|
||||
Type: util.SummitLogMention,
|
||||
Metadata: map[string]string{
|
||||
"trail_id": trail.Id,
|
||||
"trail_name": trail.GetString("name"),
|
||||
"trail_author": fmt.Sprintf("@%s@%s", trailAuthor.GetString("username"), trailAuthor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, mentionedActor)
|
||||
}
|
||||
}
|
||||
|
||||
if newSummitLog {
|
||||
// send a notification to the trail author
|
||||
notification := util.Notification{
|
||||
Type: util.SummitLogCreate,
|
||||
Metadata: map[string]string{
|
||||
"trail_id": trail.Id,
|
||||
"trail_name": trail.GetString("name"),
|
||||
"trail_author": fmt.Sprintf("@%s@%s", trailAuthor.GetString("username"), trailAuthor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, trailAuthor)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processCreateOrUpdateListActivity(activity pub.Activity, app core.App, actor *core.Record) error {
|
||||
|
||||
// no need to do anything if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := util.ListFromActivity(activity, app, actor)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func ActorsFromMentions(app core.App, htmlStr string) ([]*core.Record, []string, error) {
|
||||
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var handles []string
|
||||
var actors []*core.Record
|
||||
|
||||
var f func(*html.Node)
|
||||
f = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "a" {
|
||||
var isMention bool
|
||||
for _, attr := range n.Attr {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, "mention") {
|
||||
isMention = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isMention && n.FirstChild != nil && n.FirstChild.Type == html.TextNode {
|
||||
handle := strings.TrimSpace(n.FirstChild.Data)
|
||||
if strings.HasPrefix(handle, "@") {
|
||||
handles = append(handles, handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
|
||||
f(doc)
|
||||
|
||||
for _, h := range handles {
|
||||
actor, err := GetActorByHandle(app, h, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
actors = append(actors, actor)
|
||||
}
|
||||
|
||||
return actors, handles, nil
|
||||
}
|
||||
377
db/federation/delete.go
Normal file
377
db/federation/delete.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
func CreateTrailDeleteActivity(app core.App, r *core.Record) error {
|
||||
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
author, err := app.FindRecordById("activitypub_actors", r.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
cc := author.GetString("iri") + "/followers"
|
||||
object := fmt.Sprintf("%s/api/v1/trail/%s", origin, r.Id)
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.DeleteType))
|
||||
record.Set("to", to)
|
||||
record.Set("cc", cc)
|
||||
record.Set("object", object)
|
||||
record.Set("actor", author.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activity := pub.DeleteNew(pub.IRI(id), pub.IRI(object))
|
||||
activity.Actor = pub.IRI(author.GetString("iri"))
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.CC = pub.ItemCollection{pub.IRI(cc)}
|
||||
activity.Published = time.Now()
|
||||
|
||||
follows, err := app.FindRecordsByFilter("follows", "followee={:followee}&&status='accepted'", "", -1, 0, dbx.Params{"followee": author.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients := []string{}
|
||||
for _, f := range follows {
|
||||
follower, err := app.FindRecordById("activitypub_actors", f.GetString("follower"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, follower.GetString("inbox"))
|
||||
}
|
||||
|
||||
return PostActivity(app, author, activity, recipients)
|
||||
}
|
||||
|
||||
func CreateCommentDeleteActivity(app core.App, client meilisearch.ServiceManager, r *core.Record) error {
|
||||
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
author, err := app.FindRecordById("activitypub_actors", r.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !author.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
commentTrail, err := app.FindRecordById("trails", r.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commentTrailAuthor, err := app.FindRecordById("activitypub_actors", commentTrail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if commentTrailAuthor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := commentTrailAuthor.GetString("iri")
|
||||
object := fmt.Sprintf("%s/api/v1/comment/%s", origin, r.Id)
|
||||
|
||||
activity := pub.DeleteNew(pub.IRI(id), pub.IRI(object))
|
||||
activity.Actor = pub.IRI(author.GetString("iri"))
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.Published = time.Now()
|
||||
|
||||
err = PostActivity(app, author, activity, []string{to + "/inbox"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.DeleteType))
|
||||
record.Set("to", to)
|
||||
record.Set("object", object)
|
||||
record.Set("actor", author.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
func CreateSummitLogDeleteActivity(app core.App, r *core.Record) error {
|
||||
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
author, err := app.FindRecordById("activitypub_actors", r.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !author.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
summitLogTrail, err := app.FindRecordById("trails", r.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
summitLogTrailAuthor, err := app.FindRecordById("activitypub_actors", summitLogTrail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := summitLogTrailAuthor.GetString("iri")
|
||||
object := fmt.Sprintf("%s/api/v1/summit-log/%s", origin, r.Id)
|
||||
cc := pub.ItemCollection{pub.IRI(author.GetString("iri") + "/followers")}
|
||||
|
||||
activity := pub.DeleteNew(pub.IRI(id), pub.IRI(object))
|
||||
activity.Actor = pub.IRI(author.GetString("iri"))
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.CC = cc
|
||||
activity.Published = time.Now()
|
||||
|
||||
follows, err := app.FindRecordsByFilter("follows", "followee={:followee}&&status='accepted'", "", -1, 0, dbx.Params{"followee": author.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients := []string{}
|
||||
|
||||
for _, f := range follows {
|
||||
follower, err := app.FindRecordById("activitypub_actors", f.GetString("follower"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, follower.GetString("inbox"))
|
||||
}
|
||||
|
||||
if author.Id != summitLogTrailAuthor.Id {
|
||||
recipients = append(recipients, summitLogTrailAuthor.GetString("inbox"))
|
||||
}
|
||||
|
||||
err = PostActivity(app, author, activity, recipients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.DeleteType))
|
||||
record.Set("to", to)
|
||||
record.Set("cc", cc)
|
||||
record.Set("object", object)
|
||||
record.Set("actor", author.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
func CreateListDeleteActivity(app core.App, r *core.Record) error {
|
||||
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
author, err := app.FindRecordById("activitypub_actors", r.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !author.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
to := "https://www.w3.org/ns/activitystreams#Public"
|
||||
cc := author.GetString("iri") + "/followers"
|
||||
object := fmt.Sprintf("%s/api/v1/list/%s", origin, r.Id)
|
||||
|
||||
activity := pub.DeleteNew(pub.IRI(id), pub.IRI(object))
|
||||
activity.Actor = pub.IRI(author.GetString("iri"))
|
||||
activity.To = pub.ItemCollection{pub.IRI(to)}
|
||||
activity.CC = pub.ItemCollection{pub.IRI(cc)}
|
||||
activity.Published = time.Now()
|
||||
|
||||
follows, err := app.FindRecordsByFilter("follows", "followee={:followee}&&status='accepted'", "", -1, 0, dbx.Params{"followee": author.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipients := []string{}
|
||||
for _, f := range follows {
|
||||
follower, err := app.FindRecordById("activitypub_actors", f.GetString("follower"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients = append(recipients, follower.GetString("inbox"))
|
||||
}
|
||||
|
||||
err = PostActivity(app, author, activity, recipients)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.DeleteType))
|
||||
record.Set("to", to)
|
||||
record.Set("cc", cc)
|
||||
record.Set("object", object)
|
||||
record.Set("actor", author.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
func ProcessDeleteActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
// no need to do anything if the actor is local
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
object := activity.Object.GetID().String()
|
||||
|
||||
var err error
|
||||
switch {
|
||||
case strings.Contains(object, "trail"):
|
||||
err = processDeleteTrailActivity(app, activity)
|
||||
case strings.Contains(object, "comment"):
|
||||
err = processDeleteCommentActivity(app, actor, activity)
|
||||
case strings.Contains(object, "summit-log"):
|
||||
err = processDeleteSummitLogActivity(app, actor, activity)
|
||||
case strings.Contains(object, "list"):
|
||||
err = processDeleteListActivity(app, actor, activity)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processDeleteTrailActivity(app core.App, activity pub.Activity) error {
|
||||
|
||||
trailUrl, err := url.Parse(activity.Object.GetID().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordId := path.Base(trailUrl.Path)
|
||||
|
||||
trail, err := app.FindRecordById("trails", recordId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Delete(trail)
|
||||
}
|
||||
|
||||
func processDeleteCommentActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
object := activity.Object.GetID().String()
|
||||
|
||||
comment, err := app.FindFirstRecordByData("comments", "iri", object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if comment.GetString("author") != actor.Id {
|
||||
return fmt.Errorf("actor is not comment author")
|
||||
}
|
||||
|
||||
err = app.Delete(comment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processDeleteSummitLogActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
object := activity.Object.GetID().String()
|
||||
|
||||
summitLog, err := app.FindFirstRecordByData("summit_logs", "iri", object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if summitLog.GetString("author") != actor.Id {
|
||||
return fmt.Errorf("actor is not summit log author")
|
||||
}
|
||||
|
||||
return app.Delete(summitLog)
|
||||
}
|
||||
|
||||
func processDeleteListActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
|
||||
object := activity.Object.GetID().String()
|
||||
list, err := app.FindFirstRecordByData("lists", "iri", object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if list.GetString("author") != actor.Id {
|
||||
return fmt.Errorf("actor is not summit log author")
|
||||
}
|
||||
return app.Delete(list)
|
||||
}
|
||||
160
db/federation/follow.go
Normal file
160
db/federation/follow.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"pocketbase/util"
|
||||
"time"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// create outgoing follow activity
|
||||
func CreateFollowActivity(app core.App, follow *core.Record) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
follower := follow.GetString("follower")
|
||||
followee := follow.GetString("followee")
|
||||
|
||||
followerActor, err := app.FindRecordById("activitypub_actors", follower)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
followeeActor, err := app.FindRecordById("activitypub_actors", followee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
|
||||
activity := pub.FollowNew(pub.IRI(id), pub.IRI(followeeActor.GetString("iri")))
|
||||
activity.Actor = pub.IRI(followerActor.GetString("iri"))
|
||||
|
||||
err = PostActivity(app, followerActor, activity, []string{followeeActor.GetString("inbox")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.FollowType))
|
||||
record.Set("object", followeeActor.GetString("iri"))
|
||||
record.Set("actor", followerActor.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
// process incoming follow activity
|
||||
func ProcessFollowActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
// find the followee in our db
|
||||
object, err := app.FindFirstRecordByData("activitypub_actors", "iri", activity.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// a remote actor has requested the follow
|
||||
// this means we have not yet created a follow entry in our db
|
||||
// we accept it immediately
|
||||
if !actor.GetBool("isLocal") {
|
||||
followCollection, err := app.FindCollectionByNameOrId("follows")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
followRecord := core.NewRecord(followCollection)
|
||||
followRecord.Set("follower", actor.Id)
|
||||
followRecord.Set("followee", object.Id)
|
||||
followRecord.Set("status", "accepted")
|
||||
err = app.Save(followRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
|
||||
// send the accept activity back to the actor's inbox
|
||||
acceptActivity := pub.AcceptNew(pub.IRI(id), activity)
|
||||
acceptActivity.Actor = activity.Object
|
||||
err = PostActivity(app, object, acceptActivity, []string{actor.GetString("inbox")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create record of the accept activity in our db
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.AcceptType))
|
||||
record.Set("object", activity)
|
||||
record.Set("actor", object.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// send a notification to the followee
|
||||
notification := util.Notification{
|
||||
Type: util.NewFollower,
|
||||
Metadata: map[string]string{
|
||||
"follower": fmt.Sprintf("@%s@%s", actor.GetString("username"), actor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, object)
|
||||
|
||||
}
|
||||
|
||||
func ProcessAcceptActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
|
||||
followActivity := activity.Object.(*pub.Activity)
|
||||
|
||||
follower, err := app.FindFirstRecordByData("activitypub_actors", "iri", followActivity.Actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
follow, err := app.FindFirstRecordByFilter("follows", "follower={:follower} && followee={:followee}", dbx.Params{"follower": follower.Id, "followee": actor.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
follow.Set("status", "accepted")
|
||||
err = app.Save(follow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// err = util.SyncOutbox(app, actor)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
118
db/federation/like.go
Normal file
118
db/federation/like.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"pocketbase/util"
|
||||
"time"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// create outgoing follow activity
|
||||
func CreateLikeActivity(app core.App, like *core.Record) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
actor, err := app.FindRecordById("activitypub_actors", like.GetString("actor"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail, err := app.FindRecordById("trails", like.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
object := trail.GetString("iri")
|
||||
|
||||
if object == "" {
|
||||
// trail is local
|
||||
object = fmt.Sprintf("%s/api/v1/trail/%s", origin, trail.Id)
|
||||
}
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
|
||||
activity := pub.LikeNew(pub.IRI(id), pub.IRI(object))
|
||||
activity.Actor = pub.IRI(actor.GetString("iri"))
|
||||
|
||||
err = PostActivity(app, actor, activity, []string{trailAuthor.GetString("inbox")})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.LikeType))
|
||||
record.Set("object", object)
|
||||
record.Set("actor", actor.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
return app.Save(record)
|
||||
}
|
||||
|
||||
// process incoming like activity
|
||||
func ProcessLikeActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
trailId := path.Base(activity.Object.GetID().String())
|
||||
trail, err := app.FindRecordById("trails", trailId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !actor.GetBool("isLocal") {
|
||||
trailLikeCollection, err := app.FindCollectionByNameOrId("trail_like")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likeRecord := core.NewRecord(trailLikeCollection)
|
||||
likeRecord.Set("trail", trail.Id)
|
||||
likeRecord.Set("actor", actor.Id)
|
||||
err = app.Save(likeRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// send a notification to the trail author
|
||||
notification := util.Notification{
|
||||
Type: util.TrailLike,
|
||||
Metadata: map[string]string{
|
||||
"trail_id": trail.Id,
|
||||
"trail_name": trail.GetString("name"),
|
||||
"trail_author": fmt.Sprintf("@%s", trailAuthor.GetString("username")),
|
||||
"liker": fmt.Sprintf("@%s@%s", actor.GetString("username"), actor.GetString("domain")),
|
||||
},
|
||||
Seen: false,
|
||||
Author: actor.Id,
|
||||
}
|
||||
return util.SendNotification(app, notification, trailAuthor)
|
||||
|
||||
}
|
||||
194
db/federation/undo.go
Normal file
194
db/federation/undo.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
// create outgoing follow activity
|
||||
func CreateUnfollowActivity(app core.App, follow *core.Record) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
follower := follow.GetString("follower")
|
||||
followee := follow.GetString("followee")
|
||||
|
||||
followerActor, err := app.FindRecordById("activitypub_actors", follower)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
followeeActor, err := app.FindRecordById("activitypub_actors", followee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find the original follow activity
|
||||
followActivityRecord, err := app.FindFirstRecordByFilter("activitypub_activities", "actor={:actor}&&object={:object}&&type={:type}", dbx.Params{"actor": followerActor.GetString("iri"), "object": followeeActor.GetString("iri"), "type": string(pub.FollowType)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
followActivity := pub.FollowNew(pub.IRI(followActivityRecord.GetString("iri")), pub.IRI(followeeActor.GetString("iri")))
|
||||
followActivity.Actor = pub.IRI(followActivityRecord.GetString("actor"))
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.UndoType))
|
||||
record.Set("object", followActivity)
|
||||
record.Set("actor", followerActor.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activity := pub.UndoNew(pub.IRI(id), followActivity)
|
||||
activity.Actor = pub.IRI(followerActor.GetString("iri"))
|
||||
|
||||
return PostActivity(app, followerActor, activity, []string{followeeActor.GetString("inbox")})
|
||||
}
|
||||
|
||||
// create outgoing unlike activity
|
||||
func CreateUnlikeActivity(app core.App, like *core.Record) error {
|
||||
origin := os.Getenv("ORIGIN")
|
||||
if origin == "" {
|
||||
return fmt.Errorf("ORIGIN not set")
|
||||
}
|
||||
|
||||
actor, err := app.FindRecordById("activitypub_actors", like.GetString("actor"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trail, err := app.FindRecordById("trails", like.GetString("trail"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trailAuthor, err := app.FindRecordById("activitypub_actors", trail.GetString("author"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
object := trail.GetString("iri")
|
||||
if object == "" {
|
||||
// trail is local
|
||||
object = fmt.Sprintf("%s/api/v1/trail/%s", origin, trail.Id)
|
||||
}
|
||||
|
||||
// find the original follow activity
|
||||
likeActivityRecord, err := app.FindFirstRecordByFilter("activitypub_activities", "actor={:actor}&&object={:object}&&type={:type}", dbx.Params{"actor": actor.GetString("iri"), "object": object, "type": string(pub.LikeType)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
likeActivity := pub.LikeNew(pub.IRI(likeActivityRecord.GetString("iri")), pub.IRI(object))
|
||||
likeActivity.Actor = pub.IRI(likeActivityRecord.GetString("actor"))
|
||||
|
||||
collection, err := app.FindCollectionByNameOrId("activitypub_activities")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordId := security.RandomStringWithAlphabet(core.DefaultIdLength, core.DefaultIdAlphabet)
|
||||
|
||||
id := fmt.Sprintf("%s/api/v1/activitypub/activity/%s", origin, recordId)
|
||||
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("id", recordId)
|
||||
record.Set("iri", id)
|
||||
record.Set("type", string(pub.UndoType))
|
||||
record.Set("object", likeActivity)
|
||||
record.Set("actor", actor.GetString("iri"))
|
||||
record.Set("published", time.Now())
|
||||
|
||||
err = app.Save(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activity := pub.UndoNew(pub.IRI(id), likeActivity)
|
||||
activity.Actor = pub.IRI(actor.GetString("iri"))
|
||||
|
||||
return PostActivity(app, actor, activity, []string{trailAuthor.GetString("inbox")})
|
||||
}
|
||||
|
||||
func ProcessUndoActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
|
||||
if activity.Object.GetType() == pub.FollowType {
|
||||
return processUnfollowActivity(app, actor, activity)
|
||||
} else if activity.Object.GetType() == pub.LikeType {
|
||||
return processUnlikeActivity(app, actor, activity)
|
||||
} else {
|
||||
return fmt.Errorf("unknown undo activity object type")
|
||||
}
|
||||
}
|
||||
|
||||
func processUnfollowActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
// this was a local follow
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
followActivity := activity.Object.(*pub.Activity)
|
||||
|
||||
followee, err := app.FindFirstRecordByData("activitypub_actors", "iri", followActivity.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
follow, err := app.FindFirstRecordByFilter("follows", "follower={:follower} && followee={:followee}", dbx.Params{"follower": actor.Id, "followee": followee.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = app.Delete(follow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processUnlikeActivity(app core.App, actor *core.Record, activity pub.Activity) error {
|
||||
if actor.GetBool("isLocal") {
|
||||
return nil
|
||||
}
|
||||
|
||||
likeActivity := activity.Object.(*pub.Activity)
|
||||
|
||||
trailId := path.Base(likeActivity.Object.GetID().String())
|
||||
trail, err := app.FindRecordById("trails", trailId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
like, err := app.FindFirstRecordByFilter("trail_like", "actor={:actor} && trail={:trail}", dbx.Params{"actor": actor.Id, "trail": trail.Id})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = app.Delete(like)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user