Activity Pub Implementation #58
|
@ -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/"
|
||||
}
|
|
@ -18,6 +18,9 @@ import (
|
|||
"owl-blogs/interactions"
|
||||
"owl-blogs/render"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
vocab "github.com/go-ap/activitypub"
|
||||
|
@ -143,6 +146,15 @@ func (svc *ActivityPubService) FollowersUrl() string {
|
|||
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||
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 {
|
||||
return s.followersRepo.Add(follower)
|
||||
|
@ -269,7 +281,7 @@ func (s *ActivityPubService) Accept(act *vocab.Activity) error {
|
|||
return err
|
||||
}
|
||||
|
||||
accept := vocab.AcceptNew(vocab.IRI("TODO"), act)
|
||||
accept := vocab.AcceptNew(vocab.IRI(s.AcccepId()), act)
|
||||
data, err := jsonld.WithContext(
|
||||
jsonld.IRI(vocab.ActivityBaseURI),
|
||||
).Marshal(accept)
|
||||
|
@ -327,6 +339,51 @@ func (s *ActivityPubService) RemoveLike(id string) error {
|
|||
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 {
|
||||
siteConfig := model.SiteConfig{}
|
||||
apConfig := ActivityPubConfig{}
|
||||
|
@ -391,6 +448,18 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
|
|||
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{
|
||||
ID: vocab.ID(noteEntry.FullUrl(siteCfg)),
|
||||
Type: "Note",
|
||||
|
@ -401,8 +470,9 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
|
|||
Published: *noteEntry.PublishedAt(),
|
||||
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||
Content: vocab.NaturalLanguageValues{
|
||||
{Value: vocab.Content(noteEntry.Content())},
|
||||
{Value: vocab.Content(content)},
|
||||
},
|
||||
Tag: tags,
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ func App(db infra.Database) *web.WebApp {
|
|||
interactionRegister := app.NewInteractionTypeRegistry()
|
||||
interactionRegister.Register(&interactions.Webmention{})
|
||||
interactionRegister.Register(&interactions.Like{})
|
||||
interactionRegister.Register(&interactions.Repost{})
|
||||
|
||||
configRegister := app.NewConfigRegister()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
Reposted by <a href="{{.MetaData.SenderUrl}}">
|
||||
{{.MetaData.SenderName}}
|
||||
</a>
|
|
@ -179,6 +179,9 @@ func (s *ActivityPubServer) processUndo(r *http.Request, act *vocab.Activity) er
|
|||
if o.Type == vocab.LikeType {
|
||||
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)
|
||||
return errors.New("unsupporeted object type")
|
||||
})
|
||||
|
@ -204,6 +207,32 @@ func (s *ActivityPubServer) processLike(r *http.Request, act *vocab.Activity) er
|
|||
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 {
|
||||
body := ctx.Request().Body()
|
||||
data, err := vocab.UnmarshalJSON(body)
|
||||
|
@ -226,9 +255,15 @@ func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
|
|||
if act.Type == vocab.UndoType {
|
||||
return s.processUndo(r, act)
|
||||
}
|
||||
if act.Type == vocab.DeleteType {
|
||||
return s.processDelete(r, act)
|
||||
}
|
||||
if act.Type == vocab.LikeType {
|
||||
return s.processLike(r, act)
|
||||
}
|
||||
if act.Type == vocab.AnnounceType {
|
||||
return s.processAnnounce(r, act)
|
||||
}
|
||||
|
||||
slog.Warn("Unsupported action", "body", body)
|
||||
|
||||
|
|
Loading…
Reference in New Issue