mirror of
https://github.com/by-jp/www.byjp.me.git
synced 2025-08-10 19:05:41 +01:00
Basics of interactions for pixelfed
This commit is contained in:
parent
f0499ba2ed
commit
50278343bc
10 changed files with 204 additions and 43 deletions
11
.syndicate
11
.syndicate
|
@ -3,13 +3,14 @@ publish_url = "https://www.byjp.me/"
|
||||||
feeds = ["index.xml"]
|
feeds = ["index.xml"]
|
||||||
content_glob = "content/**/*.md"
|
content_glob = "content/**/*.md"
|
||||||
interactions_dir = "data/interactions/"
|
interactions_dir = "data/interactions/"
|
||||||
|
interaction_pfps_dir = "static/interaction-pfps/"
|
||||||
|
|
||||||
[sites]
|
[sites]
|
||||||
[sites.instagram]
|
# [sites.instagram]
|
||||||
type = "instagram"
|
# type = "instagram"
|
||||||
username = "jphastings"
|
# username = "jphastings"
|
||||||
password_envvar = "INSTAGRAM_PASSWORD"
|
# password_envvar = "INSTAGRAM_PASSWORD"
|
||||||
totp_envvar = "INSTAGRAM_TOTP"
|
# totp_envvar = "INSTAGRAM_TOTP"
|
||||||
|
|
||||||
# [sites.bluesky]
|
# [sites.bluesky]
|
||||||
# type = "bluesky"
|
# type = "bluesky"
|
||||||
|
|
1
data/interactions/photos/mums-garden/interactions.json
Normal file
1
data/interactions/photos/mums-garden/interactions.json
Normal 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"}]
|
1
data/interactions/photos/sunny-brunch/interactions.json
Normal file
1
data/interactions/photos/sunny-brunch/interactions.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"emoji":"⭐️","author":{"name":"Rui Almeida","url":"https://pixelfed.social/RJCA_PT"},"timestamp":"2022-12-03T15:13:46Z"}]
|
|
@ -1,17 +1,69 @@
|
||||||
package backfeeder
|
package backfeeder
|
||||||
|
|
||||||
import (
|
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/services"
|
||||||
|
"github.com/by-jp/www.byjp.me/tools/syndicate/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type backfeeder struct {
|
type BackfeedRef struct {
|
||||||
services *services.List
|
Source string
|
||||||
done map[string]struct{}
|
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{
|
return &backfeeder{
|
||||||
services: services,
|
services: services,
|
||||||
done: make(map[string]struct{}),
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -30,13 +30,13 @@ type fileConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
feeds []string
|
feeds []string
|
||||||
services *services.List
|
services *services.List
|
||||||
interactionsDir string
|
content []string
|
||||||
content []string
|
tagMatcher *regexp.Regexp
|
||||||
tagMatcher *regexp.Regexp
|
syndicationMatchers map[string]*regexp.Regexp
|
||||||
syndicationMatchers map[string]*regexp.Regexp
|
urlToPublicPath func(string) string
|
||||||
urlToPath func(string) string
|
urlToInteractionsPath func(string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseConfig(cfgPath string) (*config, error) {
|
func parseConfig(cfgPath string) (*config, error) {
|
||||||
|
@ -48,11 +48,14 @@ func parseConfig(cfgPath string) (*config, error) {
|
||||||
|
|
||||||
cfg := &config{
|
cfg := &config{
|
||||||
feeds: []string{},
|
feeds: []string{},
|
||||||
|
services: services.New(),
|
||||||
syndicationMatchers: make(map[string]*regexp.Regexp),
|
syndicationMatchers: make(map[string]*regexp.Regexp),
|
||||||
interactionsDir: cfgData.InteractionsDir,
|
urlToPublicPath: func(url string) string {
|
||||||
urlToPath: func(url string) string {
|
|
||||||
return path.Join(cfgData.PublishRoot, strings.TrimPrefix(url, cfgData.PublishURL))
|
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)
|
cfg.content, err = doublestar.Glob(os.DirFS("."), cfgData.ContentGlob)
|
||||||
|
|
|
@ -6,14 +6,13 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"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/poster"
|
||||||
"github.com/by-jp/www.byjp.me/tools/syndicate/shared"
|
"github.com/by-jp/www.byjp.me/tools/syndicate/shared"
|
||||||
"github.com/mmcdole/gofeed"
|
"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, backfeeder.ToBackfeedList, error) {
|
||||||
|
|
||||||
func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher *regexp.Regexp, syndicationMatchers map[string]*regexp.Regexp) ([]string, poster.ToPostList, toBackfeedMap, error) {
|
|
||||||
fp := gofeed.NewParser()
|
fp := gofeed.NewParser()
|
||||||
feed, err := fp.Parse(feedReader)
|
feed, err := fp.Parse(feedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -21,7 +20,7 @@ func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher *
|
||||||
}
|
}
|
||||||
|
|
||||||
toPost := make(poster.ToPostList)
|
toPost := make(poster.ToPostList)
|
||||||
toBackfeed := make(toBackfeedMap)
|
toBackfeed := make(backfeeder.ToBackfeedList)
|
||||||
services := make(map[string]struct{})
|
services := make(map[string]struct{})
|
||||||
|
|
||||||
for _, item := range feed.Items {
|
for _, item := range feed.Items {
|
||||||
|
@ -46,7 +45,10 @@ func parseFeed(urlToPath func(string) string, feedReader io.Reader, tagMatcher *
|
||||||
|
|
||||||
for sName, bf := range syndicationMatchers {
|
for sName, bf := range syndicationMatchers {
|
||||||
if bf.MatchString(ext.Value) {
|
if bf.MatchString(ext.Value) {
|
||||||
toBackfeed[ext.Value] = item.Link
|
toBackfeed[ext.Value] = backfeeder.BackfeedRef{
|
||||||
|
Source: sName,
|
||||||
|
LocalURL: item.Link,
|
||||||
|
}
|
||||||
services[sName] = struct{}{}
|
services[sName] = struct{}{}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,13 @@ func main() {
|
||||||
check(err)
|
check(err)
|
||||||
|
|
||||||
pstr := poster.New(cfg.services)
|
pstr := poster.New(cfg.services)
|
||||||
bkfd := backfeeder.New(cfg.services)
|
bkfd := backfeeder.New(cfg.services, cfg.urlToInteractionsPath)
|
||||||
|
|
||||||
for _, feed := range cfg.feeds {
|
for _, feed := range cfg.feeds {
|
||||||
f, err := os.Open(feed)
|
f, err := os.Open(feed)
|
||||||
check(err)
|
check(err)
|
||||||
defer f.Close()
|
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)
|
check(err)
|
||||||
|
|
||||||
if len(services) > 0 {
|
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)
|
fmt.Fprintf(os.Stderr, "Found %d new syndications to post in %s\n", len(toPost), feed)
|
||||||
posted, err := pstr.PostAll(toPost)
|
if _, err := pstr.PostAll(toPost); err != nil {
|
||||||
_ = posted
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Couldn't post syndications: %v\n", err)
|
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)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,103 @@
|
||||||
package mastodon
|
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) {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ type List struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *List {
|
func New() *List {
|
||||||
return &List{}
|
return &List{
|
||||||
|
available: make(map[string]shared.Service),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *List) Load(name string, siteConfig any) (shared.Service, error) {
|
func (l *List) Load(name string, siteConfig any) (shared.Service, error) {
|
||||||
|
|
|
@ -26,16 +26,19 @@ type Post struct {
|
||||||
|
|
||||||
type Interaction struct {
|
type Interaction struct {
|
||||||
// eg. Repost is 🔁, Facebook is 👍, Instagram is ♥️, Mastodon is ⭐️, Medium is 👏
|
// eg. Repost is 🔁, Facebook is 👍, Instagram is ♥️, Mastodon is ⭐️, Medium is 👏
|
||||||
Emoji string
|
Emoji string `json:"emoji,omitempty"`
|
||||||
// The URL of the original interaction
|
// The URL of the original interaction
|
||||||
URL string
|
URL string `json:"url,omitempty"`
|
||||||
// If there's a comment associated with the interaction
|
// If there's a comment associated with the interaction
|
||||||
Comment string
|
Comment string `json:"comment,omitempty"`
|
||||||
// Details of the author
|
// Details of the author
|
||||||
Author struct {
|
Author Author `json:"author"`
|
||||||
Name string
|
Timestamp time.Time `json:"timestamp"`
|
||||||
URL string
|
}
|
||||||
Icon []byte
|
|
||||||
}
|
type Author struct {
|
||||||
Timestamp time.Time
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Avatar []byte `json:"-"`
|
||||||
|
AvatarURL string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue