Files
Christian Beutel e8c7e7721a fixes feed
2025-07-11 17:13:30 +02:00

854 lines
24 KiB
Go

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, actor *core.Record, 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, actor, 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, actor *core.Record, 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, actor, 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, actor *core.Record, 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, actor, 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("preferred_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, recipient *core.Record, activity pub.Activity) error {
var err error
if strings.Contains(activity.Object.GetID().String(), "/api/v1/trail") {
err = processCreateOrUpdateTrailActivity(activity, app, actor, recipient)
} 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, recipient)
} else {
err = processCreateOrUpdateCommentActivity(activity, app, actor)
}
if err != nil {
return err
}
return nil
}
func processCreateOrUpdateTrailActivity(activity pub.Activity, app core.App, actor *core.Record, recipient *core.Record) error {
trail, err := util.TrailFromActivity(activity, app, actor)
if err != nil {
return err
}
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, trail.Id, util.TrailFeed)
if err != nil {
return err
}
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("preferred_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("preferred_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("preferred_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("preferred_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("preferred_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, recipient *core.Record) error {
list, err := util.ListFromActivity(activity, app, actor)
if err != nil {
return err
}
_, err = util.InsertIntoFeed(app, recipient.Id, actor.Id, list.Id, util.ListFeed)
if err != nil {
return err
}
return err
}
func ActorsFromMentions(app core.App, actor *core.Record, 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, actor, h, false)
if err != nil {
continue
}
actors = append(actors, actor)
}
return actors, handles, nil
}