Skip to content

Commit

Permalink
Uses the twitter 1.1 API to upload media when attachments are supplie…
Browse files Browse the repository at this point in the history
…d instead of appending the URL to the tweet
  • Loading branch information
bakatz committed Sep 29, 2023
1 parent 472e6f5 commit a07cc51
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 38 deletions.
65 changes: 49 additions & 16 deletions cmd/lambda/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
"log/slog"
"net/http"
"os"
"strconv"
"strings"
"time"

twitter11 "github.com/ChimeraCoder/anaconda"
"github.com/aws/aws-lambda-go/lambda"
lib_wip "github.com/bakatz/wip-to-twitter-bridge/lib/wip"
"github.com/dghubble/oauth1"
twitter "github.com/g8rswimmer/go-twitter/v2"
twitter2 "github.com/g8rswimmer/go-twitter/v2"
"github.com/joho/godotenv"
"go.uber.org/zap"
)

type Response struct {
Expand Down Expand Up @@ -56,15 +59,14 @@ type authorize struct{}

func (a authorize) Add(req *http.Request) {}

func makeAndLogErrorResponse(message string, code string, logger *zap.Logger) Response {
func makeAndLogErrorResponse(message string, code string, logger *slog.Logger) Response {
response := Response{Message: message, Code: code}
logger.Sugar().Error("Response: ", response)
logger.Error("Returning an error response", "response", response)
return response
}

func Handler(ctx context.Context) (Response, error) {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

// Get all the secrets we need
wipAPIKey := os.Getenv("WIP_API_KEY")
Expand Down Expand Up @@ -108,7 +110,8 @@ func Handler(ctx context.Context) (Response, error) {
TokenSecret: twitterAccessTokenSecret,
})
twitterHttpClient.Timeout = CONNECTION_TIMEOUT_DURATION
twitterClient := &twitter.Client{
twitter11Client := twitter11.NewTwitterApiWithCredentials(twitterAccessToken, twitterAccessTokenSecret, twitterAPIKey, twitterAPIKeySecret)
twitter2Client := &twitter2.Client{
Authorizer: authorize{},
Client: twitterHttpClient,
Host: "https://api.twitter.com",
Expand All @@ -128,16 +131,29 @@ func Handler(ctx context.Context) (Response, error) {
if todo.CompletedAt.Before(startOfLookbackWindow) || strings.Contains(todo.Body, PRIVATE_ENTITY_IDENTIFIER) {
continue
}
tweetMessage := "✅ " + todo.Body
if len(todo.Attachments) > 0 {
tweetMessage += " " + todo.Attachments[0].URL + " #buildinpublic" // Just use the first attachment for now
} else {
tweetMessage += " #buildinpublic"
tweetMessage := "✅ " + todo.Body + " #buildinpublic"
mediaIDs := []string{}

for _, attachment := range todo.Attachments {
mediaID, err := uploadAttachmentFromTodo(attachment, logger, twitter11Client)
if err != nil {
return makeAndLogErrorResponse("Error uploading attachment", "upload_attachment_error", logger), err
}
mediaIDs = append(mediaIDs, mediaID)
}
logger.Info("About to tweet this message: " + tweetMessage)
_, err := twitterClient.CreateTweet(context.Background(), twitter.CreateTweetRequest{

logger.Info("About to tweet this message", "message", tweetMessage)

createTweetRequest := &twitter2.CreateTweetRequest{
Text: tweetMessage,
})
}

if len(mediaIDs) > 0 {
createTweetRequest.Media = &twitter2.CreateTweetMedia{
IDs: mediaIDs,
}
}
_, err := twitter2Client.CreateTweet(context.Background(), *createTweetRequest)
if err != nil {
return makeAndLogErrorResponse("Error creating a tweet", "twitter_create_tweet_error", logger), err
}
Expand All @@ -147,10 +163,27 @@ func Handler(ctx context.Context) (Response, error) {
}

// Return a success message
logger.Info(SUCCESS_MESSAGE, zap.Int("num_todos_tweeted", numTodosTweeted))
logger.Info(SUCCESS_MESSAGE, "num_todos_tweeted", numTodosTweeted)
return Response{Message: SUCCESS_MESSAGE, NumTodosTweeted: numTodosTweeted}, nil
}

func uploadAttachmentFromTodo(attachment lib_wip.Attachment, logger *slog.Logger, twitter11Client *twitter11.TwitterApi) (string, error) {
resp, err := http.Get(attachment.URL)
if err != nil {
return "", err
}

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
media, err := twitter11Client.UploadMedia(base64.StdEncoding.EncodeToString(respBytes))
if err != nil {
return "", err
}
return strconv.FormatInt(media.MediaID, 10), err
}

func main() {
godotenv.Load("../../.env")
if os.Getenv("RUN_WITHOUT_LAMBDA") == "true" {
Expand Down
19 changes: 11 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
module github.com/bakatz/wip-to-twitter-bridge

go 1.20

require go.uber.org/zap v1.24.0

require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
)
go 1.21

require (
github.com/ChimeraCoder/anaconda v2.0.0+incompatible
github.com/aws/aws-lambda-go v1.41.0
github.com/dghubble/oauth1 v0.7.2
github.com/g8rswimmer/go-twitter/v2 v2.1.5
github.com/joho/godotenv v1.5.1
)

require (
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 // indirect
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 // indirect
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
golang.org/x/net v0.15.0 // indirect
)
27 changes: 18 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
github.com/ChimeraCoder/anaconda v2.0.0+incompatible h1:F0eD7CHXieZ+VLboCD5UAqCeAzJZxcr90zSCcuJopJs=
github.com/ChimeraCoder/anaconda v2.0.0+incompatible/go.mod h1:TCt3MijIq3Qqo9SBtuW/rrM4x7rDfWqYWHj8T7hLcLg=
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7 h1:r+EmXjfPosKO4wfiMLe1XQictsIlhErTufbWUsjOTZs=
github.com/ChimeraCoder/tokenbucket v0.0.0-20131201223612-c5a927568de7/go.mod h1:b2EuEMLSG9q3bZ95ql1+8oVqzzrTNSiOQqSXWFBzxeI=
github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y=
github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 h1:ekDALXAVvY/Ub1UtNta3inKQwZ/jMB/zpOtD8rAYh78=
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330/go.mod h1:nH+k0SvAt3HeiYyOlJpLLv1HG1p7KWP7qU9QPp2/pCo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dghubble/oauth1 v0.7.2 h1:pwcinOZy8z6XkNxvPmUDY52M7RDPxt0Xw1zgZ6Cl5JA=
github.com/dghubble/oauth1 v0.7.2/go.mod h1:9erQdIhqhOHG/7K9s/tgh9Ks/AfoyrO5mW/43Lu2+kE=
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI=
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA=
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad/go.mod h1:mPKfmRa823oBIgl2r20LeMSpTAteW5j7FLkc0vjmzyQ=
github.com/g8rswimmer/go-twitter/v2 v2.1.5 h1:Uj9Yuof2UducrP4Xva7irnUJfB9354/VyUXKmc2D5gg=
github.com/g8rswimmer/go-twitter/v2 v2.1.5/go.mod h1:/55xWb313KQs25X7oZrNSEwLQNkYHhPsDwFstc45vhc=
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE=
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
10 changes: 5 additions & 5 deletions lib/wip/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import "time"
type WIPAPIResponse struct {
Data Data `json:"data"`
}
type Attachments struct {
type Attachment struct {
URL string `json:"url"`
}
type Todo struct {
ID string `json:"id"`
Body string `json:"body"`
CompletedAt time.Time `json:"completed_at"`
Attachments []Attachments `json:"attachments"`
ID string `json:"id"`
Body string `json:"body"`
CompletedAt time.Time `json:"completed_at"`
Attachments []Attachment `json:"attachments"`
}
type Project struct {
ID string `json:"id"`
Expand Down

0 comments on commit a07cc51

Please sign in to comment.