From 5c05f48be34e1549b235ee620bf931ae26054736 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Sat, 18 May 2024 19:34:44 +0200 Subject: [PATCH] prevent publishing of drafts + support for recipes --- app/activity_pub_service.go | 98 +++++++++++++++++++++++++++++++++++-- app/entry_service.go | 19 ++++++- app/entry_service_test.go | 4 +- 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/app/activity_pub_service.go b/app/activity_pub_service.go index 1f06ec9..1bad4e0 100644 --- a/app/activity_pub_service.go +++ b/app/activity_pub_service.go @@ -498,7 +498,41 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) { } func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) { + slog.Info("Processing Entry Create for ActivityPub") + followers, err := svc.AllFollowers() + if err != nil { + slog.Error("Cannot retrieve followers") + } + object, err := svc.entryToObject(entry) + if err != nil { + slog.Error("Cannot convert object", "err", err) + } + + update := vocab.UpdateNew(object.ID, object) + update.Actor = object.AttributedTo + update.To = object.To + update.Published = object.Published + data, err := jsonld.WithContext( + jsonld.IRI(vocab.ActivityBaseURI), + jsonld.Context{ + jsonld.ContextElement{ + Term: "toot", + IRI: jsonld.IRI("http://joinmastodon.org/ns#"), + }, + }, + ).Marshal(update) + 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) + } } func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) { @@ -543,6 +577,13 @@ func (svc *ActivityPubService) entryToObject(entry model.Entry) (vocab.Object, e if imageEntry, ok := entry.(*entrytypes.Image); ok { return svc.imageToObject(imageEntry), nil } + if articleEntry, ok := entry.(*entrytypes.Article); ok { + return svc.articleToObject(articleEntry), nil + } + if recipeEntry, ok := entry.(*entrytypes.Recipe); ok { + return svc.recipeToObject(recipeEntry), nil + } + slog.Warn("entry type not yet supported for activity pub") return vocab.Object{}, errors.New("entry type not supported") } @@ -595,11 +636,14 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab Type: vocab.DocumentType, MediaType: vocab.MimeType(binaryFile.Mime()), URL: vocab.ID(fullImageUrl), + Name: vocab.NaturalLanguageValues{ + {Value: vocab.Content(content)}, + }, }) - image := vocab.Note{ + image := vocab.Image{ ID: vocab.ID(imageEntry.FullUrl(siteCfg)), - Type: "Note", + Type: "Image", To: vocab.ItemCollection{ vocab.PublicNS, vocab.IRI(svc.FollowersUrl()), @@ -610,7 +654,7 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab {Value: vocab.Content(imageEntry.Title())}, }, Content: vocab.NaturalLanguageValues{ - {Value: vocab.Content(content)}, + {Value: vocab.Content(imageEntry.Title() + "

" + string(content))}, }, Attachment: attachments, // Tag: tags, @@ -618,3 +662,51 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab return image } + +func (svc *ActivityPubService) articleToObject(articleEntry *entrytypes.Article) vocab.Object { + siteCfg, _ := svc.siteConfigServcie.GetSiteConfig() + content := articleEntry.Content() + + image := vocab.Article{ + ID: vocab.ID(articleEntry.FullUrl(siteCfg)), + Type: "Article", + To: vocab.ItemCollection{ + vocab.PublicNS, + vocab.IRI(svc.FollowersUrl()), + }, + Published: *articleEntry.PublishedAt(), + AttributedTo: vocab.ID(svc.ActorUrl()), + Name: vocab.NaturalLanguageValues{ + {Value: vocab.Content(articleEntry.Title())}, + }, + Content: vocab.NaturalLanguageValues{ + {Value: vocab.Content(string(content))}, + }, + } + return image + +} + +func (svc *ActivityPubService) recipeToObject(recipeEntry *entrytypes.Recipe) vocab.Object { + siteCfg, _ := svc.siteConfigServcie.GetSiteConfig() + content := recipeEntry.Content() + + image := vocab.Article{ + ID: vocab.ID(recipeEntry.FullUrl(siteCfg)), + Type: "Article", + To: vocab.ItemCollection{ + vocab.PublicNS, + vocab.IRI(svc.FollowersUrl()), + }, + Published: *recipeEntry.PublishedAt(), + AttributedTo: vocab.ID(svc.ActorUrl()), + Name: vocab.NaturalLanguageValues{ + {Value: vocab.Content(recipeEntry.Title())}, + }, + Content: vocab.NaturalLanguageValues{ + {Value: vocab.Content(string(content))}, + }, + } + return image + +} diff --git a/app/entry_service.go b/app/entry_service.go index 26a2053..7f3e87a 100644 --- a/app/entry_service.go +++ b/app/entry_service.go @@ -48,7 +48,13 @@ func (s *EntryService) Create(entry model.Entry) error { if err != nil { return err } - s.Bus.NotifyCreated(entry) + // only notify if the publishing date is set + // otherwise this is a draft. + // listeners might publish the entry to other services/platforms + // this should only happen for publshed content + if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() { + s.Bus.NotifyCreated(entry) + } return nil } @@ -57,7 +63,13 @@ func (s *EntryService) Update(entry model.Entry) error { if err != nil { return err } - s.Bus.NotifyUpdated(entry) + // only notify if the publishing date is set + // otherwise this is a draft. + // listeners might publish the entry to other services/platforms + // this should only happen for publshed content + if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() { + s.Bus.NotifyUpdated(entry) + } return nil } @@ -66,6 +78,9 @@ func (s *EntryService) Delete(entry model.Entry) error { if err != nil { return err } + // deletes should always be notfied + // a published entry might be converted to a draft before deletion + // omitting the deletion in this case would prevent deletion on other platforms s.Bus.NotifyDeleted(entry) return nil } diff --git a/app/entry_service_test.go b/app/entry_service_test.go index ec7cd78..41cafc4 100644 --- a/app/entry_service_test.go +++ b/app/entry_service_test.go @@ -14,7 +14,9 @@ func setupService() *app.EntryService { register := app.NewEntryTypeRegistry() register.Register(&test.MockEntry{}) repo := infra.NewEntryRepository(db, register) - service := app.NewEntryService(repo, nil, app.NewEventBus()) + cfgRepo := infra.NewConfigRepo(db) + cfgService := app.NewSiteConfigService(cfgRepo) + service := app.NewEntryService(repo, cfgService, app.NewEventBus()) return service }