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/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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue