diff --git a/.syndicate b/.syndicate index 40a920c5..d4a1f210 100644 --- a/.syndicate +++ b/.syndicate @@ -3,13 +3,14 @@ publish_url = "https://www.byjp.me/" feeds = ["index.xml"] content_glob = "content/**/*.md" interactions_dir = "data/interactions/" +interaction_pfps_dir = "static/interaction-pfps/" [sites] - [sites.instagram] - type = "instagram" - username = "jphastings" - password_envvar = "INSTAGRAM_PASSWORD" - totp_envvar = "INSTAGRAM_TOTP" + # [sites.instagram] + # type = "instagram" + # username = "jphastings" + # password_envvar = "INSTAGRAM_PASSWORD" + # totp_envvar = "INSTAGRAM_TOTP" # [sites.bluesky] # type = "bluesky" diff --git a/data/interactions/photos/mums-garden/interactions.json b/data/interactions/photos/mums-garden/interactions.json new file mode 100644 index 00000000..77c76dca --- /dev/null +++ b/data/interactions/photos/mums-garden/interactions.json @@ -0,0 +1 @@ +[{"emoji":"⭐️","author":{"name":"JP","url":"https://hachyderm.io/users/byjp"},"timestamp":"2022-12-14T11:16:00Z"},{"emoji":"⭐️","author":{"name":"Ben!","url":"https://pixelfed.social/bencord0"},"timestamp":"2019-09-14T08:43:33Z"},{"emoji":"⭐️","author":{"name":"Thomas Zimmermann","url":"https://pixelfed.social/curlingtom"},"timestamp":"2022-10-30T14:12:09Z"},{"emoji":"⭐️","author":{"name":"Ivan Jurišić","url":"https://pixelfed.social/ijurisic"},"timestamp":"2019-09-27T22:53:45Z"}] diff --git a/data/interactions/photos/sunny-brunch/interactions.json b/data/interactions/photos/sunny-brunch/interactions.json new file mode 100644 index 00000000..227df95d --- /dev/null +++ b/data/interactions/photos/sunny-brunch/interactions.json @@ -0,0 +1 @@ +[{"emoji":"⭐️","author":{"name":"Rui Almeida","url":"https://pixelfed.social/RJCA_PT"},"timestamp":"2022-12-03T15:13:46Z"}] diff --git a/tools/syndicate/backfeeder/backfeeder.go b/tools/syndicate/backfeeder/backfeeder.go index 1aefcb45..8a136442 100644 --- a/tools/syndicate/backfeeder/backfeeder.go +++ b/tools/syndicate/backfeeder/backfeeder.go @@ -1,17 +1,69 @@ package backfeeder import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "github.com/by-jp/www.byjp.me/tools/syndicate/services" + "github.com/by-jp/www.byjp.me/tools/syndicate/shared" ) -type backfeeder struct { - services *services.List - done map[string]struct{} +type BackfeedRef struct { + Source string + LocalURL string } -func New(services *services.List) *backfeeder { +type ToBackfeedList map[string]BackfeedRef + +type backfeeder struct { + services *services.List + done map[string]struct{} + urlToPath func(string) string +} + +func New(services *services.List, urlToPath func(string) string) *backfeeder { return &backfeeder{ - services: services, - done: make(map[string]struct{}), + services: services, + done: make(map[string]struct{}), + urlToPath: urlToPath, } } + +func (b *backfeeder) BackfeedAll(toBackfeed ToBackfeedList) error { + allIAs := make(map[string][]shared.Interaction) + + for remoteURL, ref := range toBackfeed { + ias, err := b.services.Service(ref.Source).Interactions(remoteURL) + if err != nil { + return err + } + + path := b.urlToPath(ref.LocalURL) + allIAs[path] = append(allIAs[path], ias...) + } + + for postDir, ias := range allIAs { + if err := writeInteractions(postDir, ias); err != nil { + return fmt.Errorf("couldn't write interactions into %s: %w", postDir, err) + } + } + return nil +} + +func writeInteractions(dir string, ias []shared.Interaction) error { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + path := filepath.Join(dir, "interactions.json") + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer f.Close() + + enc := json.NewEncoder(f) + return enc.Encode(ias) +} diff --git a/tools/syndicate/config.go b/tools/syndicate/config.go index 4f460dd6..6fd18f5a 100644 --- a/tools/syndicate/config.go +++ b/tools/syndicate/config.go @@ -30,13 +30,13 @@ type fileConfig struct { } type config struct { - feeds []string - services *services.List - interactionsDir string - content []string - tagMatcher *regexp.Regexp - syndicationMatchers map[string]*regexp.Regexp - urlToPath func(string) string + feeds []string + services *services.List + content []string + tagMatcher *regexp.Regexp + syndicationMatchers map[string]*regexp.Regexp + urlToPublicPath func(string) string + urlToInteractionsPath func(string) string } func parseConfig(cfgPath string) (*config, error) { @@ -48,11 +48,14 @@ func parseConfig(cfgPath string) (*config, error) { cfg := &config{ feeds: []string{}, + services: services.New(), syndicationMatchers: make(map[string]*regexp.Regexp), - interactionsDir: cfgData.InteractionsDir, - urlToPath: func(url string) string { + urlToPublicPath: func(url string) string { return path.Join(cfgData.PublishRoot, strings.TrimPrefix(url, cfgData.PublishURL)) }, + urlToInteractionsPath: func(url string) string { + return path.Join(cfgData.InteractionsDir, strings.TrimPrefix(url, cfgData.PublishURL)) + }, } cfg.content, err = doublestar.Glob(os.DirFS("."), cfgData.ContentGlob) diff --git a/tools/syndicate/feeds.go b/tools/syndicate/feeds.go index 18e9944e..7e61f0b2 100644 --- a/tools/syndicate/feeds.go +++ b/tools/syndicate/feeds.go @@ -6,14 +6,13 @@ import ( "regexp" "strings" + "github.com/by-jp/www.byjp.me/tools/syndicate/backfeeder" "github.com/by-jp/www.byjp.me/tools/syndicate/poster" "github.com/by-jp/www.byjp.me/tools/syndicate/shared" "github.com/mmcdole/gofeed" ) -type toBackfeedMap map[string]string - -func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher *regexp.Regexp, syndicationMatchers map[string]*regexp.Regexp) ([]string, poster.ToPostList, toBackfeedMap, error) { +func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher *regexp.Regexp, syndicationMatchers map[string]*regexp.Regexp) ([]string, poster.ToPostList, backfeeder.ToBackfeedList, error) { fp := gofeed.NewParser() feed, err := fp.Parse(feedReader) if err != nil { @@ -21,7 +20,7 @@ func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher * } toPost := make(poster.ToPostList) - toBackfeed := make(toBackfeedMap) + toBackfeed := make(backfeeder.ToBackfeedList) services := make(map[string]struct{}) for _, item := range feed.Items { @@ -46,7 +45,10 @@ func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher * for sName, bf := range syndicationMatchers { if bf.MatchString(ext.Value) { - toBackfeed[ext.Value] = item.Link + toBackfeed[ext.Value] = backfeeder.BackfeedRef{ + Source: sName, + LocalURL: item.Link, + } services[sName] = struct{}{} break } diff --git a/tools/syndicate/main.go b/tools/syndicate/main.go index 921cfbf6..2e276eb1 100644 --- a/tools/syndicate/main.go +++ b/tools/syndicate/main.go @@ -24,13 +24,13 @@ func main() { check(err) pstr := poster.New(cfg.services) - bkfd := backfeeder.New(cfg.services) + bkfd := backfeeder.New(cfg.services, cfg.urlToInteractionsPath) for _, feed := range cfg.feeds { f, err := os.Open(feed) check(err) defer f.Close() - services, toPost, toBackfeed, err := parseFeed(cfg.urlToPath, f, cfg.tagMatcher, cfg.syndicationMatchers) + services, toPost, toBackfeed, err := parseFeed(cfg.urlToPublicPath, f, cfg.tagMatcher, cfg.syndicationMatchers) check(err) if len(services) > 0 { @@ -43,9 +43,7 @@ func main() { } fmt.Fprintf(os.Stderr, "Found %d new syndications to post in %s\n", len(toPost), feed) - posted, err := pstr.PostAll(toPost) - _ = posted - if err != nil { + if _, err := pstr.PostAll(toPost); err != nil { fmt.Fprintf(os.Stderr, "Couldn't post syndications: %v\n", err) } @@ -56,6 +54,8 @@ func main() { } fmt.Fprintf(os.Stderr, "Found %d existing syndications to backfeed from %s\n", len(toBackfeed), feed) - _ = bkfd + if err := bkfd.BackfeedAll(toBackfeed); err != nil { + fmt.Fprintf(os.Stderr, "Couldn't backfeed syndications: %v\n", err) + } } } diff --git a/tools/syndicate/services/mastodon/interactions.go b/tools/syndicate/services/mastodon/interactions.go index 4480bfd1..39002f4b 100644 --- a/tools/syndicate/services/mastodon/interactions.go +++ b/tools/syndicate/services/mastodon/interactions.go @@ -1,7 +1,103 @@ package mastodon -import "github.com/by-jp/www.byjp.me/tools/syndicate/shared" +import ( + "context" + "fmt" + + "github.com/by-jp/www.byjp.me/tools/syndicate/shared" + "github.com/mattn/go-mastodon" +) func (s *service) Interactions(url string) ([]shared.Interaction, error) { - return nil, nil + id, err := s.postID(url) + if err != nil { + return nil, err + } + + var ias []shared.Interaction + + ctx := context.Background() + favs, err := s.masto.GetFavouritedBy(ctx, id, nil) + if err != nil { + return nil, fmt.Errorf("unable to get favourites for %s: %w", url, err) + } + + for _, acc := range favs { + ias = append(ias, shared.Interaction{ + Emoji: "⭐️", + Author: shared.Author{ + Name: acc.DisplayName, + URL: acc.URL, + AvatarURL: acc.Avatar, + }, + Timestamp: acc.CreatedAt, + }) + } + + reblogs, err := s.masto.GetRebloggedBy(ctx, id, nil) + if err != nil { + return nil, fmt.Errorf("unable to get boosts for %s: %w", url, err) + } + + for _, acc := range reblogs { + ias = append(ias, shared.Interaction{ + Emoji: "🔁", + Author: shared.Author{ + Name: acc.DisplayName, + URL: acc.URL, + AvatarURL: acc.Avatar, + }, + Timestamp: acc.CreatedAt, + }) + } + + postCtx, err := s.masto.GetStatusContext(ctx, id) + if err != nil { + return nil, fmt.Errorf("unable to get replies for %s: %w", url, err) + } + + for _, reply := range postCtx.Descendants { + if reply.Visibility != mastodon.VisibilityPublic { + continue + } + + ias = append(ias, shared.Interaction{ + URL: reply.URL, + Comment: reply.Content, + Author: shared.Author{ + Name: reply.Account.DisplayName, + URL: reply.Account.URL, + AvatarURL: reply.Account.Avatar, + }, + Timestamp: reply.CreatedAt, + }) + } + + return ias, nil +} + +func (s *service) postID(url string) (mastodon.ID, error) { + re, err := s.BackfeedMatcher() + if err != nil { + return "", fmt.Errorf("couldn't create the backfeed matcher: %w", err) + } + + m := re.FindStringSubmatch(url) + if len(m) == 0 { + return "", fmt.Errorf("couldn't extract the post ID from the URL") + } + + var id string + for i, name := range re.SubexpNames() { + if name == "id" { + id = m[i] + break + } + } + + if id == "" { + return "", fmt.Errorf("couldn't extract the post ID from the URL") + } + + return mastodon.ID(id), nil } diff --git a/tools/syndicate/services/tracker.go b/tools/syndicate/services/tracker.go index 26c4cff6..0c22d5e9 100644 --- a/tools/syndicate/services/tracker.go +++ b/tools/syndicate/services/tracker.go @@ -11,7 +11,9 @@ type List struct { } func New() *List { - return &List{} + return &List{ + available: make(map[string]shared.Service), + } } func (l *List) Load(name string, siteConfig any) (shared.Service, error) { diff --git a/tools/syndicate/shared/types.go b/tools/syndicate/shared/types.go index c92bf8d0..a1952d53 100644 --- a/tools/syndicate/shared/types.go +++ b/tools/syndicate/shared/types.go @@ -26,16 +26,19 @@ type Post struct { type Interaction struct { // eg. Repost is 🔁, Facebook is 👍, Instagram is ♥️, Mastodon is ⭐️, Medium is 👏 - Emoji string + Emoji string `json:"emoji,omitempty"` // The URL of the original interaction - URL string + URL string `json:"url,omitempty"` // If there's a comment associated with the interaction - Comment string + Comment string `json:"comment,omitempty"` // Details of the author - Author struct { - Name string - URL string - Icon []byte - } - Timestamp time.Time + Author Author `json:"author"` + Timestamp time.Time `json:"timestamp"` +} + +type Author struct { + Name string `json:"name"` + URL string `json:"url"` + Avatar []byte `json:"-"` + AvatarURL string `json:"-"` }