Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions bot/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,28 @@ import (
"log"
"os"
"os/signal"
"time"

"github.com/togdon/reply-bot/bot/pkg/environment"
"github.com/togdon/reply-bot/bot/pkg/mastodon"
"github.com/togdon/reply-bot/bot/pkg/gsheets"
"github.com/togdon/reply-bot/bot/pkg/mastodon"
)



func main() {
cfg, err := environment.New()

if err != nil {
log.Fatalf("Error loading .env or ENV: %v", err)
}

log.Printf("Successfully read the env\n")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

writeChan := make(chan interface{})
gsheetClient, err := gsheets.NewGSheetsClient(gsheets.CREDS_FILE, gsheets.SHEET_ID, gsheets.SHEET_NAME)
gsheetClient, err := gsheets.NewClient(ctx)
if err != nil {
log.Fatalf("Unable to create gsheets client: %v", err)
}
mastodonClient, err := mastodon.NewClient(
writeChan,
gsheetClient,
mastodon.WithConfig(*cfg),
)
if err != nil {
log.Fatal(err)
}

errs := make(chan error, 1)

Expand All @@ -49,15 +39,24 @@ func main() {
cancel()
}()

go mastodonClient.Run(ctx, cancel, errs)
go mastodonClient.Write(ctx)
mastodonClient, err := mastodon.NewClient(gsheetClient, mastodon.WithConfig(*cfg))
if err != nil {
log.Fatal(err)
}

go mastodonClient.Run(ctx, errs)

for {
select {
case err := <-errs:
fmt.Println(err)
case <-ctx.Done():
fmt.Println("Context cancelled, waiting 5 seconds for services to shut down...")

time.Sleep(5 * time.Second)

fmt.Println("Shutting down...")

return
}
}
Expand Down
33 changes: 16 additions & 17 deletions bot/pkg/bsky/bsky.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

const (
bskyFeedUrl = "https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:ltradugkwaw6yfotr7boceaj/app.bsky.feed.generator/aaapztniwbk46"
bskyFeedURL = "https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:ltradugkwaw6yfotr7boceaj/app.bsky.feed.generator/aaapztniwbk46"
)

type Post struct {
Expand All @@ -23,6 +23,7 @@ type Post struct {
RepostCount int `json:"reposeCount"`
QuoteCount int `json:"quoteCount"`
}

type FeedItem struct {
Post Post `json:"post"`
}
Expand All @@ -31,61 +32,59 @@ type SearchResponse struct {
Feed []FeedItem `json:"feed"`
}

func FetchPosts(client *gsheets.Client) {

resp, err := http.Get(bskyFeedUrl)

func FetchPosts(client *gsheets.Client) error {
resp, err := http.Get(bskyFeedURL)
if err != nil {
panic(err)
return err
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)

if err != nil {
panic(err)
return err
}

var search_hits SearchResponse
var searchHits SearchResponse

if err := json.Unmarshal(body, &search_hits); err != nil {
panic(err)
if err := json.Unmarshal(body, &searchHits); err != nil {
return err
}

for _, item := range search_hits.Feed {
for _, item := range searchHits.Feed {
//TODO more specific logic to filter bots / low engagement posts / low follower authors?

url, err := generateBskyUrl(item.Post)
if err != nil {
fmt.Printf("error generating bsky url for uri %v", err)

continue
}

fmt.Printf("Associated URL: %s\n", url)

}

//TODO write to the google sheet where responses can be generated?

return nil
}

func generateBskyUrl(post Post) (string, error) {
uri := post.URI
handle, ok := post.Author["handle"]
if !ok {
return "", fmt.Errorf("author handle invalid")
}

rkey, err := extractRKey(uri)
rkey, err := extractRKey(post.URI)
if err != nil {
return "", fmt.Errorf("failed to extract rkey for post: %w", err)
}

return fmt.Sprintf("https://bsky.app/profile/%s/post/%s", handle, rkey), nil

}

func extractRKey(uri string) (string, error) {
parts := strings.Split(uri, "/")

if len(parts) < 2 {
return "", fmt.Errorf("invalid uri format")
}
Expand Down
6 changes: 3 additions & 3 deletions bot/pkg/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ type BlueSky struct {

func New() (*Config, error) {
var cfg Config
err := env.Parse(&cfg)
if err != nil {

if err := env.Parse(&cfg); err != nil {
return nil, err
}
return &cfg, nil

return &cfg, nil
}
56 changes: 26 additions & 30 deletions bot/pkg/gsheets/gsheets.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,51 @@ import (
)

const (
SHEET_ID = "1wD8zsIcn9vUPmL749MFAreXx8cfaYeqRfFoGuSnJ2Lk"
SHEET_NAME = "replies"
CREDS_FILE = "credentials.json"
sheetID = "1wD8zsIcn9vUPmL749MFAreXx8cfaYeqRfFoGuSnJ2Lk"
sheetName = "replies"
credsFile = "credentials.json"
)

// GSheetsClient encapsulates the Sheets service and sheet configuration.
// Client encapsulates the Sheets service and sheet configuration.
type Client struct {
Service *sheets.Service
SheetID string
SheetName string
service *sheets.Service
sheetID string
sheetName string
}

// NewGSheetsClient initializes a Google Sheets API client and returns a GSheetsClient instance.
func NewGSheetsClient(credentialsFile, sheetID, sheetName string) (*Client, error) {
ctx := context.Background()
service, err := sheets.NewService(ctx, option.WithCredentialsFile(credentialsFile))
// NewClient initializes a Google Sheets API client and returns a Client instance.
func NewClient(ctx context.Context) (*Client, error) {
service, err := sheets.NewService(ctx, option.WithCredentialsFile(credsFile))
if err != nil {
return nil, fmt.Errorf("unable to create Sheets client: %v", err)
}

return &Client{
Service: service,
SheetID: sheetID,
SheetName: sheetName,
service: service,
sheetID: sheetID,
sheetName: sheetName,
}, nil
}

// AppendRow adds a new entry to the Google Sheet, formatted with URL, Post Type, and Responded checkbox.
func (c *Client) AppendRow(post post.Post) error {
rowData := []interface{}{
post.ID,
post.URI,
post.Type,
post.Content,
post.Source,
false,
}

writeRange := fmt.Sprintf("%s!A:F", c.SheetName) // Columns A to F

// Append data to the specified range in the sheet
_, err := c.Service.Spreadsheets.Values.Append(c.SheetID, writeRange, &sheets.ValueRange{
Values: [][]interface{}{rowData},
}).ValueInputOption("USER_ENTERED").Do()

if err != nil {
if _, err := c.service.Spreadsheets.Values.Append(c.sheetID, fmt.Sprintf("%s!A:F", c.sheetName), &sheets.ValueRange{
Values: [][]interface{}{
{
post.ID,
post.URI,
post.Type,
post.Content,
post.Source,
false,
},
},
}).ValueInputOption("USER_ENTERED").Do(); err != nil {
return fmt.Errorf("unable to append data to sheet: %v", err)
}

log.Println("Row successfully appended.")

return nil
}
25 changes: 18 additions & 7 deletions bot/pkg/mastodon/mastodon.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func WithConfig(cfg environment.Config) Option {
}
}

func NewClient(ch chan interface{}, gsheetsClient *gsheets.Client, options ...Option) (*Client, error) {
func NewClient(gsheetsClient *gsheets.Client, options ...Option) (*Client, error) {
var cfg config

for _, opt := range options {
Expand All @@ -61,11 +61,20 @@ func NewClient(ch chan interface{}, gsheetsClient *gsheets.Client, options ...Op
AccessToken: cfg.accessToken,
}),
gsheetsClient: gsheetsClient,
writeChannel: ch,
writeChannel: make(chan interface{}),
}, nil
}

func (c *Client) Run(ctx context.Context, cancel context.CancelFunc, errs chan error) {
func (c *Client) Run(ctx context.Context, errs chan error) {
go c.read(ctx, errs)
go c.write(ctx, errs)

<-ctx.Done()

fmt.Println("Context cancelled, shutting down Mastodon client...")
}

func (c *Client) read(ctx context.Context, errs chan<- error) {
events, err := c.mastodonClient.StreamingPublic(ctx, false)
if err != nil {
errs <- err
Expand Down Expand Up @@ -103,14 +112,12 @@ func (c *Client) Run(ctx context.Context, cancel context.CancelFunc, errs chan e
// How should we handle this?
}
case <-ctx.Done():
fmt.Println("Context cancelled, shutting down Mastodon client...")
return
}
}
}

func (c *Client) Write(ctx context.Context) {

func (c *Client) write(ctx context.Context, errs chan<- error) {
for {
select {
case event := <-c.writeChannel:
Expand All @@ -125,7 +132,6 @@ func (c *Client) Write(ctx context.Context) {
// How should we handle this?
}
case <-ctx.Done():
fmt.Println("Context cancelled, shutting down Mastodon client...")
return
}
}
Expand All @@ -135,13 +141,15 @@ func createPost(URI string, content string, postType post.NYTContentType) (*post
if URI == "" || content == "" {
return nil, fmt.Errorf("empty content or uri. Content: %s, URI: %s", URI, content)
}

post := post.Post{
ID: URI,
URI: URI,
Content: content,
Type: postType,
Source: post.Mastodon,
}

return &post, nil
}

Expand Down Expand Up @@ -190,6 +198,7 @@ func findURLs(s string) string {
if a.Key == "href" {
url = a.Val
}

if a.Key == "class" {
class = a.Val
}
Expand All @@ -210,6 +219,7 @@ func findURLs(s string) string {
}

extractURL(doc, &buf)

return buf.String()
}

Expand All @@ -226,6 +236,7 @@ func parseURLs(urls string) bool {
}

newsRE := regexp.MustCompile(`(?i)nytimes\.com`)

if newsRE.MatchString(u) {
return true
}
Expand Down