From 9cfbf0b9b7e10deb6efc3197db1a754f02090118 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Fri, 17 May 2024 22:37:18 +0200 Subject: [PATCH] process likes from ActivityPub --- app/activity_pub_service.go | 67 +++++++++++++++++++++++--- app/entry_service.go | 25 ++++++++-- app/entry_service_test.go | 2 +- cmd/owl/main.go | 9 +++- interactions/like.go | 33 +++++++++++++ render/templates/interaction/Like.tmpl | 3 ++ web/activity_pub_handler.go | 47 ++++++++++++++---- 7 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 interactions/like.go create mode 100644 render/templates/interaction/Like.tmpl diff --git a/app/activity_pub_service.go b/app/activity_pub_service.go index 3a2fb10..5a804bb 100644 --- a/app/activity_pub_service.go +++ b/app/activity_pub_service.go @@ -15,6 +15,7 @@ import ( "owl-blogs/config" "owl-blogs/domain/model" entrytypes "owl-blogs/entry_types" + "owl-blogs/interactions" "owl-blogs/render" "reflect" "time" @@ -54,21 +55,27 @@ func (cfg *ActivityPubConfig) PrivateKey() *rsa.PrivateKey { } type ActivityPubService struct { - followersRepo repository.FollowerRepository - configRepo repository.ConfigRepository - siteConfigServcie *SiteConfigService + followersRepo repository.FollowerRepository + configRepo repository.ConfigRepository + interactionRepository repository.InteractionRepository + entryService *EntryService + siteConfigServcie *SiteConfigService } func NewActivityPubService( followersRepo repository.FollowerRepository, configRepo repository.ConfigRepository, + interactionRepository repository.InteractionRepository, + entryService *EntryService, siteConfigServcie *SiteConfigService, bus *EventBus, ) *ActivityPubService { service := &ActivityPubService{ - followersRepo: followersRepo, - configRepo: configRepo, - siteConfigServcie: siteConfigServcie, + followersRepo: followersRepo, + configRepo: configRepo, + interactionRepository: interactionRepository, + entryService: entryService, + siteConfigServcie: siteConfigServcie, } bus.Subscribe(service) @@ -275,6 +282,51 @@ func (s *ActivityPubService) Accept(act *vocab.Activity) error { return s.sendObject(actor, data) } +func (s *ActivityPubService) AddLike(sender string, liked string, likeId string) error { + entry, err := s.entryService.FindByUrl(liked) + if err != nil { + return err + } + + actor, err := s.GetActor(sender) + if err != nil { + return err + } + + var like *interactions.Like + interaction, err := s.interactionRepository.FindById(likeId) + if err != nil { + interaction = &interactions.Like{} + } + like, ok := interaction.(*interactions.Like) + if !ok { + return errors.New("existing interaction with same id is not a like") + } + existing := like.ID() != "" + + likeMeta := interactions.LikeMetaData{ + SenderUrl: sender, + SenderName: actor.Name.String(), + } + like.SetID(likeId) + like.SetMetaData(&likeMeta) + like.SetEntryID(entry.ID()) + like.SetCreatedAt(time.Now()) + if !existing { + return s.interactionRepository.Create(like) + } else { + return s.interactionRepository.Update(like) + } +} + +func (s *ActivityPubService) RemoveLike(id string) error { + interaction, err := s.interactionRepository.FindById(id) + if err != nil { + interaction = &interactions.Like{} + } + return s.interactionRepository.Delete(interaction) +} + func (s *ActivityPubService) sendObject(to vocab.Actor, data []byte) error { siteConfig := model.SiteConfig{} apConfig := ActivityPubConfig{} @@ -325,10 +377,11 @@ func (s *ActivityPubService) sendObject(to vocab.Actor, data []byte) error { */ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) { + slog.Info("Processing Entry Create for ActivityPub") // limit to notes for now noteEntry, ok := entry.(*entrytypes.Note) if !ok { - slog.Info("not an image") + slog.Info("not a note") return } diff --git a/app/entry_service.go b/app/entry_service.go index 491e6c9..26a2053 100644 --- a/app/entry_service.go +++ b/app/entry_service.go @@ -1,6 +1,7 @@ package app import ( + "errors" "fmt" "owl-blogs/app/repository" "owl-blogs/domain/model" @@ -9,17 +10,20 @@ import ( ) type EntryService struct { - EntryRepository repository.EntryRepository - Bus *EventBus + EntryRepository repository.EntryRepository + siteConfigServcie *SiteConfigService + Bus *EventBus } func NewEntryService( entryRepository repository.EntryRepository, + siteConfigServcie *SiteConfigService, bus *EventBus, ) *EntryService { return &EntryService{ - EntryRepository: entryRepository, - Bus: bus, + EntryRepository: entryRepository, + siteConfigServcie: siteConfigServcie, + Bus: bus, } } @@ -70,6 +74,19 @@ func (s *EntryService) FindById(id string) (model.Entry, error) { return s.EntryRepository.FindById(id) } +func (s *EntryService) FindByUrl(url string) (model.Entry, error) { + cfg, _ := s.siteConfigServcie.GetSiteConfig() + if !strings.HasPrefix(url, cfg.FullUrl) { + return nil, errors.New("url does not belong to blog") + } + if strings.HasSuffix(url, "/") { + url = url[:len(url)-1] + } + parts := strings.Split(url, "/") + id := parts[len(parts)-1] + return s.FindById(id) +} + func (s *EntryService) filterEntries(entries []model.Entry, published bool, drafts bool) []model.Entry { filteredEntries := make([]model.Entry, 0) for _, entry := range entries { diff --git a/app/entry_service_test.go b/app/entry_service_test.go index 88a4e4b..ec7cd78 100644 --- a/app/entry_service_test.go +++ b/app/entry_service_test.go @@ -14,7 +14,7 @@ func setupService() *app.EntryService { register := app.NewEntryTypeRegistry() register.Register(&test.MockEntry{}) repo := infra.NewEntryRepository(db, register) - service := app.NewEntryService(repo, app.NewEventBus()) + service := app.NewEntryService(repo, nil, app.NewEventBus()) return service } diff --git a/cmd/owl/main.go b/cmd/owl/main.go index 2324f07..f8c7923 100644 --- a/cmd/owl/main.go +++ b/cmd/owl/main.go @@ -41,6 +41,7 @@ func App(db infra.Database) *web.WebApp { interactionRegister := app.NewInteractionTypeRegistry() interactionRegister.Register(&interactions.Webmention{}) + interactionRegister.Register(&interactions.Like{}) configRegister := app.NewConfigRegister() @@ -60,13 +61,17 @@ func App(db infra.Database) *web.WebApp { // Create Services siteConfigService := app.NewSiteConfigService(configRepo) - entryService := app.NewEntryService(entryRepo, eventBus) + entryService := app.NewEntryService(entryRepo, siteConfigService, eventBus) binaryService := app.NewBinaryFileService(binRepo) authorService := app.NewAuthorService(authorRepo, siteConfigService) webmentionService := app.NewWebmentionService( siteConfigService, interactionRepo, entryRepo, httpClient, eventBus, ) - apService := app.NewActivityPubService(followersRepo, configRepo, siteConfigService, eventBus) + apService := app.NewActivityPubService( + followersRepo, configRepo, interactionRepo, + entryService, siteConfigService, + eventBus, + ) // setup render functions render.SiteConfigService = siteConfigService diff --git a/interactions/like.go b/interactions/like.go new file mode 100644 index 0000000..59c95fa --- /dev/null +++ b/interactions/like.go @@ -0,0 +1,33 @@ +package interactions + +import ( + "fmt" + "owl-blogs/domain/model" + "owl-blogs/render" +) + +type Like struct { + model.InteractionBase + meta LikeMetaData +} + +type LikeMetaData struct { + SenderUrl string + SenderName string +} + +func (i *Like) Content() model.InteractionContent { + str, err := render.RenderTemplateToString("interaction/Like", i) + if err != nil { + fmt.Println(err) + } + return model.InteractionContent(str) +} + +func (i *Like) MetaData() interface{} { + return &i.meta +} + +func (i *Like) SetMetaData(metaData interface{}) { + i.meta = *metaData.(*LikeMetaData) +} diff --git a/render/templates/interaction/Like.tmpl b/render/templates/interaction/Like.tmpl new file mode 100644 index 0000000..b0ec3f1 --- /dev/null +++ b/render/templates/interaction/Like.tmpl @@ -0,0 +1,3 @@ +Liked by + {{.MetaData.SenderName}} + \ No newline at end of file diff --git a/web/activity_pub_handler.go b/web/activity_pub_handler.go index d5ea1d7..b20365b 100644 --- a/web/activity_pub_handler.go +++ b/web/activity_pub_handler.go @@ -160,26 +160,51 @@ func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity) } func (s *ActivityPubServer) processUndo(r *http.Request, act *vocab.Activity) error { - follower := act.Actor.GetID().String() - err := s.apService.VerifySignature(r, follower) + sender := act.Actor.GetID().String() + err := s.apService.VerifySignature(r, sender) + + return vocab.OnObject(act.Object, func(o *vocab.Object) error { + if o.Type == vocab.FollowType { + if err != nil { + slog.Error("wrong signature", "err", err) + return err + } + err = s.apService.RemoveFollower(sender) + if err != nil { + return err + } + go s.apService.Accept(act) + return nil + } + if o.Type == vocab.LikeType { + return s.apService.RemoveLike(o.ID.String()) + } + slog.Warn("unsupporeted object type for undo", "object", o) + return errors.New("unsupporeted object type") + }) + +} + +func (s *ActivityPubServer) processLike(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.RemoveFollower(follower) + + err = s.apService.AddLike(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) HandleInbox(ctx *fiber.Ctx) error { - // siteConfig, _ := s.siteConfigService.GetSiteConfig() - // apConfig, _ := s.apService.GetApConfig() - body := ctx.Request().Body() data, err := vocab.UnmarshalJSON(body) if err != nil { @@ -198,11 +223,15 @@ func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error { if act.Type == vocab.FollowType { return s.processFollow(r, act) } - if act.Type == vocab.UndoType { - slog.Info("processing undo") return s.processUndo(r, act) } + if act.Type == vocab.LikeType { + return s.processLike(r, act) + } + + slog.Warn("Unsupported action", "body", body) + return errors.New("only follow and undo actions supported") }) return err