Basics of interactions for pixelfed

This commit is contained in:
JP Hastings-Spital 2023-11-12 10:55:33 +00:00
parent f0499ba2ed
commit 50278343bc
10 changed files with 204 additions and 43 deletions

View file

@ -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"

View file

@ -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"}]

View file

@ -0,0 +1 @@
[{"emoji":"⭐️","author":{"name":"Rui Almeida","url":"https://pixelfed.social/RJCA_PT"},"timestamp":"2022-12-03T15:13:46Z"}]

View file

@ -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)
}

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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) {

View file

@ -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:"-"`
}