Activity Pub Implementation #58

Merged
h4kor merged 13 commits from activity_pub into main 2024-05-18 14:31:11 +00:00
6 changed files with 196 additions and 2 deletions
Showing only changes of commit 741ccfac73 - Show all commits

15
announce.json Normal file
View File

@ -0,0 +1,15 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://chaos.social/users/h4kor/statuses/112458343910963038/activity",
"type": "Announce",
"actor": "https://chaos.social/users/h4kor",
"published": "2024-05-17T20:35:48Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://blog.libove.org",
"https://chaos.social/users/h4kor/followers"
],
"object": "https://blog.libove.org/posts/073d4f64-fd9c-4b32-bdb7-4cfb1922b9d0/"
}

View File

@ -18,6 +18,9 @@ import (
"owl-blogs/interactions" "owl-blogs/interactions"
"owl-blogs/render" "owl-blogs/render"
"reflect" "reflect"
"regexp"
"strconv"
"strings"
"time" "time"
vocab "github.com/go-ap/activitypub" vocab "github.com/go-ap/activitypub"
@ -143,6 +146,15 @@ func (svc *ActivityPubService) FollowersUrl() string {
cfg, _ := svc.siteConfigServcie.GetSiteConfig() cfg, _ := svc.siteConfigServcie.GetSiteConfig()
return cfg.FullUrl + "/activitypub/followers" return cfg.FullUrl + "/activitypub/followers"
} }
func (svc *ActivityPubService) AcccepId() string {
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
return cfg.FullUrl + "#accept-" + strconv.FormatInt(time.Now().UnixNano(), 16)
}
func (svc *ActivityPubService) HashtagId(hashtag string) string {
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
return cfg.FullUrl + "/tags/" + strings.ReplaceAll(hashtag, "#", "")
}
func (s *ActivityPubService) AddFollower(follower string) error { func (s *ActivityPubService) AddFollower(follower string) error {
return s.followersRepo.Add(follower) return s.followersRepo.Add(follower)
@ -269,7 +281,7 @@ func (s *ActivityPubService) Accept(act *vocab.Activity) error {
return err return err
} }
accept := vocab.AcceptNew(vocab.IRI("TODO"), act) accept := vocab.AcceptNew(vocab.IRI(s.AcccepId()), act)
data, err := jsonld.WithContext( data, err := jsonld.WithContext(
jsonld.IRI(vocab.ActivityBaseURI), jsonld.IRI(vocab.ActivityBaseURI),
).Marshal(accept) ).Marshal(accept)
@ -327,6 +339,51 @@ func (s *ActivityPubService) RemoveLike(id string) error {
return s.interactionRepository.Delete(interaction) return s.interactionRepository.Delete(interaction)
} }
func (s *ActivityPubService) AddRepost(sender string, reposted string, respostId string) error {
entry, err := s.entryService.FindByUrl(reposted)
if err != nil {
return err
}
actor, err := s.GetActor(sender)
if err != nil {
return err
}
var repost *interactions.Repost
interaction, err := s.interactionRepository.FindById(respostId)
if err != nil {
interaction = &interactions.Repost{}
}
repost, ok := interaction.(*interactions.Repost)
if !ok {
return errors.New("existing interaction with same id is not a like")
}
existing := repost.ID() != ""
repostMeta := interactions.RepostMetaData{
SenderUrl: sender,
SenderName: actor.Name.String(),
}
repost.SetID(respostId)
repost.SetMetaData(&repostMeta)
repost.SetEntryID(entry.ID())
repost.SetCreatedAt(time.Now())
if !existing {
return s.interactionRepository.Create(repost)
} else {
return s.interactionRepository.Update(repost)
}
}
func (s *ActivityPubService) RemoveRepost(id string) error {
interaction, err := s.interactionRepository.FindById(id)
if err != nil {
interaction = &interactions.Repost{}
}
return s.interactionRepository.Delete(interaction)
}
func (s *ActivityPubService) sendObject(to vocab.Actor, data []byte) error { func (s *ActivityPubService) sendObject(to vocab.Actor, data []byte) error {
siteConfig := model.SiteConfig{} siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{} apConfig := ActivityPubConfig{}
@ -391,6 +448,18 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
slog.Error("Cannot retrieve followers") slog.Error("Cannot retrieve followers")
} }
content := noteEntry.Content()
r := regexp.MustCompile("#[a-z0-9_]+")
matches := r.FindAllString(string(content), -1)
tags := vocab.ItemCollection{}
for _, hashtag := range matches {
tags.Append(vocab.Object{
ID: vocab.ID(svc.HashtagId(hashtag)),
Name: vocab.NaturalLanguageValues{{Value: vocab.Content(hashtag)}},
})
}
note := vocab.Note{ note := vocab.Note{
ID: vocab.ID(noteEntry.FullUrl(siteCfg)), ID: vocab.ID(noteEntry.FullUrl(siteCfg)),
Type: "Note", Type: "Note",
@ -401,8 +470,9 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
Published: *noteEntry.PublishedAt(), Published: *noteEntry.PublishedAt(),
AttributedTo: vocab.ID(svc.ActorUrl()), AttributedTo: vocab.ID(svc.ActorUrl()),
Content: vocab.NaturalLanguageValues{ Content: vocab.NaturalLanguageValues{
{Value: vocab.Content(noteEntry.Content())}, {Value: vocab.Content(content)},
}, },
Tag: tags,
} }
create := vocab.CreateNew(vocab.IRI(noteEntry.FullUrl(siteCfg)), note) create := vocab.CreateNew(vocab.IRI(noteEntry.FullUrl(siteCfg)), note)
@ -436,5 +506,42 @@ func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) {
} }
func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) { func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) {
slog.Info("Processing Entry Delete for ActivityPub")
// limit to notes for now
noteEntry, ok := entry.(*entrytypes.Note)
if !ok {
slog.Info("not a note")
return
}
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
followers, err := svc.AllFollowers()
if err != nil {
slog.Error("Cannot retrieve followers")
}
note := vocab.Note{
ID: vocab.ID(noteEntry.FullUrl(siteCfg)),
Type: "Note",
}
delete := vocab.DeleteNew(vocab.IRI(noteEntry.FullUrl(siteCfg)), note)
delete.Actor = note.AttributedTo
delete.To = note.To
delete.Published = time.Now()
data, err := jsonld.WithContext(
jsonld.IRI(vocab.ActivityBaseURI),
).Marshal(delete)
if err != nil {
slog.Error("marshalling error", "err", err)
}
for _, follower := range followers {
actor, err := svc.GetActor(follower)
if err != nil {
slog.Error("Unable to retrieve follower actor", "err", err)
}
svc.sendObject(actor, data)
}
} }

View File

@ -42,6 +42,7 @@ func App(db infra.Database) *web.WebApp {
interactionRegister := app.NewInteractionTypeRegistry() interactionRegister := app.NewInteractionTypeRegistry()
interactionRegister.Register(&interactions.Webmention{}) interactionRegister.Register(&interactions.Webmention{})
interactionRegister.Register(&interactions.Like{}) interactionRegister.Register(&interactions.Like{})
interactionRegister.Register(&interactions.Repost{})
configRegister := app.NewConfigRegister() configRegister := app.NewConfigRegister()

33
interactions/repost.go Normal file
View File

@ -0,0 +1,33 @@
package interactions
import (
"fmt"
"owl-blogs/domain/model"
"owl-blogs/render"
)
type Repost struct {
model.InteractionBase
meta RepostMetaData
}
type RepostMetaData struct {
SenderUrl string
SenderName string
}
func (i *Repost) Content() model.InteractionContent {
str, err := render.RenderTemplateToString("interaction/Repost", i)
if err != nil {
fmt.Println(err)
}
return model.InteractionContent(str)
}
func (i *Repost) MetaData() interface{} {
return &i.meta
}
func (i *Repost) SetMetaData(metaData interface{}) {
i.meta = *metaData.(*RepostMetaData)
}

View File

@ -0,0 +1,3 @@
Reposted by <a href="{{.MetaData.SenderUrl}}">
{{.MetaData.SenderName}}
</a>

View File

@ -179,6 +179,9 @@ func (s *ActivityPubServer) processUndo(r *http.Request, act *vocab.Activity) er
if o.Type == vocab.LikeType { if o.Type == vocab.LikeType {
return s.apService.RemoveLike(o.ID.String()) return s.apService.RemoveLike(o.ID.String())
} }
if o.Type == vocab.AnnounceType {
return s.apService.RemoveRepost(o.ID.String())
}
slog.Warn("unsupporeted object type for undo", "object", o) slog.Warn("unsupporeted object type for undo", "object", o)
return errors.New("unsupporeted object type") return errors.New("unsupporeted object type")
}) })
@ -204,6 +207,32 @@ func (s *ActivityPubServer) processLike(r *http.Request, act *vocab.Activity) er
return nil return nil
} }
func (s *ActivityPubServer) processAnnounce(r *http.Request, act *vocab.Activity) error {
sender := act.Actor.GetID().String()
liked := act.Object.GetID().String()
err := s.apService.VerifySignature(r, sender)
if err != nil {
slog.Error("wrong signature", "err", err)
return err
}
err = s.apService.AddRepost(sender, liked, act.ID.String())
if err != nil {
slog.Error("error saving like", "err", err)
return err
}
go s.apService.Accept(act)
return nil
}
func (s *ActivityPubServer) processDelete(r *http.Request, act *vocab.Activity) error {
return vocab.OnObject(act.Object, func(o *vocab.Object) error {
slog.Warn("Not processing delete", "action", act, "object", o)
return nil
})
}
func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error { func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
body := ctx.Request().Body() body := ctx.Request().Body()
data, err := vocab.UnmarshalJSON(body) data, err := vocab.UnmarshalJSON(body)
@ -226,9 +255,15 @@ func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
if act.Type == vocab.UndoType { if act.Type == vocab.UndoType {
return s.processUndo(r, act) return s.processUndo(r, act)
} }
if act.Type == vocab.DeleteType {
return s.processDelete(r, act)
}
if act.Type == vocab.LikeType { if act.Type == vocab.LikeType {
return s.processLike(r, act) return s.processLike(r, act)
} }
if act.Type == vocab.AnnounceType {
return s.processAnnounce(r, act)
}
slog.Warn("Unsupported action", "body", body) slog.Warn("Unsupported action", "body", body)