Twitter Media Upload with go-twitter

On the previous post, I said that I was doing the web service migration in the company, porting the code from Scala to Go. One of the task was integrating the web service with Twitter: getting the data, posting the status, and uploading the media.

By the way, I am using go-twitter to do the integration. Everything ran smoothly until we want to upload the image to the twitter. There is no method related to the media in the library. After reading some issues, we found this pull request. We can see that there is a method for uploading any media inside the commit log, but we don’t know why the owner hasn’t merged them.

Because of that matter, we have 2 options: Use the forked version, or use the original repository and find out the way to upload the media. After reading the function and the documentation in the original repository, finally we choose the second option, because I think it is quite simple. I will explain it step by step.

Using go-twitter library

This is the sample to use the user-auth method with the app and user keys (Consumer Key, Consumer Secret, Access Key, and Access Secret).

config := oauth1.NewConfig("consumerKey", "consumerSecret")
token := oauth1.NewToken("accessToken", "accessSecret")
httpClient := config.Client(oauth1.NoContext, token)
client := twitter.NewClient(httpClient)

The client variable is a wrapper function that already prepared with functions to do anything on the twitter. We can see the method in twitter.go

// NewClient returns a new Client.
func NewClient(httpClient *http.Client) *Client {
   base := sling.New().Client(httpClient).Base(twitterAPI)
   return &Client{
      sling:          base,
      Accounts:       newAccountService(base.New()),
      DirectMessages: newDirectMessageService(base.New()),
      Favorites:      newFavoriteService(base.New()),
      Followers:      newFollowerService(base.New()),
      Friends:        newFriendService(base.New()),
      Friendships:    newFriendshipService(base.New()),
      Lists:          newListService(base.New()),
      RateLimits:     newRateLimitService(base.New()),
      Search:         newSearchService(base.New()),
      PremiumSearch:  newPremiumSearchService(base.New()),
      Statuses:       newStatusService(base.New()),
      Streams:        newStreamService(httpClient, base.New()),
      Timelines:      newTimelineService(base.New()),
      Trends:         newTrendsService(base.New()),
      Users:          newUserService(base.New()),
   }
}

Using the signed HTTP Client

From the NewClient function, we find that it receives http.Client parameter. Then, we check this part.

httpClient := config.Client(oauth1.NoContext, token)

We take a deeper look at the Client function. We can find it in config.go.

// Client returns an HTTP client which uses the provided ctx and access Token.
func (c *Config) Client(ctx context.Context, t *Token) *http.Client {
return NewClient(ctx, c, t)
}

// NewClient returns a new http Client which signs requests via OAuth1.
func NewClient(ctx context.Context, config *Config, token *Token) *http.Client {
transport := &Transport{
Base: contextTransport(ctx),
source: StaticTokenSource(token),
auther: newAuther(config),
}
return &http.Client{Transport: transport}
}

We can see that actually the httpClient variable is an http client that already signed with oauth and we can use it directly with the Twitter endpoint. So, this is the code to upload the media and post the status using go-twitter http client.

import (
   "bytes"
   "encoding/json"
   "fmt"
   "github.com/dghubble/oauth1"
   "io"
   "io/ioutil"
   "mime/multipart"
   "net/url"
   "os"
   "strconv"
)

type MediaUpload struct {
   MediaId int `json:"media_id"`
}

func main() {
   // authenticate
   config := oauth1.NewConfig("consumerKey", "consumerSecret") 
   token := oauth1.NewToken("accessToken", "accessSecret") 
   httpClient := config.Client(oauth1.NoContext, token)

   // create body form
   b := &bytes.Buffer{}
   form := multipart.NewWriter(b)

   // create media paramater
   fw, err := form.CreateFormFile("media", "file.jpg")
   if err != nil {
      panic(err)
   }

   // open file
   opened, err := os.Open("/path/to/file.jpg")
   if err != nil {
      panic(err)
   }

   // copy to form
   _, err = io.Copy(fw, opened)
   if err != nil {
      panic(err)
   }

   // close form
   form.Close()

   // upload media
   resp, err := httpClient.Post("https://upload.twitter.com/1.1/media/upload.json?media_category=tweet_image", form.FormDataContentType(), bytes.NewReader(b.Bytes()))
   if err != nil {
      fmt.Printf("Error: %s\n", err)
   }
   defer resp.Body.Close()

   // decode response and get media id
   m := &MediaUpload{}
   _ = json.NewDecoder(resp.Body).Decode(m)
   mid := strconv.Itoa(m.MediaId)

   // post status with media id
   resp, err = httpClient.PostForm("https://api.twitter.com/1.1/statuses/update.json", url.Values{"status": {"Post the status!"}, "media_ids": {mid}})
   // parse response
   body, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      fmt.Printf("Error: %s\n", err)
   }

   fmt.Printf("Response: %s\n", body)
}
Continue Reading