Go
Twitter
golang
cli
TwitterAPI

GolangでcliからTweetするツールを作った

結論

Golang云々よりもTwitterのOAuth周りに踊らされた。OAuth難しいので今後共出来る限りライブラリ使おう。
コード書く時にしょっちゅう気づいたこと呟くので、それをするのにcli-tweeterめっちゃ便利。

導入

去年のとある金曜日の夜に突如思い立って Golang の勉強に cli ツールを3つ作ることにした。
この記事は2つ目。

前記事(cliから英単語・日本語の単語を翻訳できるツールを作った)
=> https://qiita.com/ahaha0807_alg/items/83ebf6fec4790d7a6130

cliツールを作るのに便利な urfave/cli パッケージを使う。

やること

プログラミングでの発見とかを呟くときにcliからパッと呟けるようにする

目標

  • tweeter account でインタラクティブにTwitterのユーザー情報入力してもらい、Twitterアカウントを登録する
  • tweeter tweet "[ツイート内容]" で登録したアカウントでツイートする
  • tweeter list で登録したアカウントのIDリストを表示する
  • tweeter tweet [アカウントID] "[ツイート内容]" で指定のアカウントでツイートする

※改行は\nで入力する形にする => ""で内容を囲んで送れば自動で改行扱いになった

やらないこと

  • TLの表示
  • RT/ふぁぼの実装
  • パイプとかでコマンド繋いで呟けるようにする
  • 画像や動画を呟けるようにする

そのうちやろうと思ってること・作って気づいてやったこと

実装開始

ディレクトリ構造

$GOPATH/github.com/ahaha0807/cli-tweeter
├── Gopkg.lock
├── Gopkg.toml
├── account
│   └── user.go
├── bin/
├── cmd
│   └── tweeter
│       └── main.go
├── list
│   └── list.go
├── tweet
│   └── tweet.go
├── util
│   ├── filer.go
│   └── util.go
└── vendor/

まずはアカウント登録機能(ここでクソ詰まった)

呟くにはまずTwitter APIを叩くための認証通しておかないと行けないので、認証→認証情報を保存しておく機能を作る。

Twitter API の認証周りについて

Twitter APIはOAuth1とOAuth2の二種類の認証方法があり、どちらの認証を通すかによって出来ることが異なる。

OAuth1 を使って認証することでアクセストークン・アクセスシークレットが取得でき、投稿などのPOST系機能や、ユーザーに紐づく機能を利用できる。

その代わりに認証までのフローが結構めんどくさい。

OAuth2 を使って認証することでベアラートークンが取得でき、ユーザーに紐付かない機能(Twitterのページでログインしなくても読めるようなパブリックな情報)を利用できる。

今回はツイートする必要があるので、OAuth1を使ってアクセストークン・アクセスシークレットを取得する処理を書いていくことにした。

OAuth1認証を手で書こうとした…

今ではある程度理解が進んで、これだけ書けているが実際にコードを書いていた時はほとんどOAuthへの知識がなく、手探り状態だったため、めちゃめちゃ時間かかって詰まりまくった。

これに関しての詳しいことは別記事にして、供養している。

https://qiita.com/ahaha0807_alg/items/61ff69608491cc2f59c8

実際に作った関数

結局、Golangのmrjones/oauthというライブラリを使って関数を実装した。

元ソースコードはここ

account.go
func getTwitterToken() (token, secret, userID string) {
    consumer := oauth.NewConsumer(
        os.Getenv("TWITTER_CLI_CONSUMER_KEY"),
        os.Getenv("TWITTER_CLI_CONSUMER_SECRET_KEY"),
        oauth.ServiceProvider{
            RequestTokenUrl:   "https://api.twitter.com/oauth/request_token",
            AuthorizeTokenUrl: "https://api.twitter.com/oauth/authorize",
            AccessTokenUrl:    "https://api.twitter.com/oauth/access_token",
        })

    requestToken, url, err := consumer.GetRequestTokenAndUrl("oob")
    util.Check(err)

    open.Run(url)

    fmt.Println("「連携アプリを認証」ボタンを押した後、ブラウザに表示されたPINコードを入力してください。")
    fmt.Println("(Please push a button for authentication. After input visualized PIN.)")

    verificationCode := ""
    fmt.Scanln(&verificationCode)

    accessToken, err := consumer.AuthorizeToken(requestToken, verificationCode)
    if err != nil {
        fmt.Println("認証失敗")
        fmt.Println("(Authenticate faild.)")
        log.Fatal(err)
    }

    userID = accessToken.AdditionalData["screen_name"]
    token = accessToken.Token
    secret = accessToken.Secret

    return token, secret, userID
}

Twitterには PINベース認証というCLIなどに特化した認証方法が用意されていて、それを使っている。

ブラウザに表示されるPINを入力する必要があるので、自動でブラウザを開いてくれるskratchdot/open-golangも使用。

取得した情報の扱い

このアプリでは上記の関数で取得したアカウントのID/アクセストークン/アクセスシークレットをcsvに書き出して保存している。

JSONを使用するか迷ったが情報量的にはcsvで十分と判断し、ファイルを指定の場所に保存するようにした。

セキュリティ面は…個人用のアプリだし、OSにログイン出来ない限りファイルにアクセスできないし、このアプリで考える必要はないんじゃないかなと…w

登録したアカウントのリスト表示機能

この機能は単純明快。

上記のデータが保存されてるファイルを引っ張ってきて、フォーマットをあわせて表示するだけ。

list.go
func List(_ *cli.Context) error {

    userInfoList := util.GetUserInfoList()

    for _, element := range userInfoList {
        fmt.Println(element["userId"])
    }

    return nil
}

ツイートする機能

処理の流れ

ツイートするアカウントのIDを入力してもらう(デフォは最初に登録したアカウント)

ファイルから認証情報を取ってくる

ChimeraCoder/anaconda にアカウントの認証情報(アクセストークン/アクセスシークレット)与える

ツイート内容をPOSTする

ファイルから情報を取ってくる

元ソースコードはここ

/* ~tweet関数内~ */
    selectedAccountIndex := 0
    if context.String("account") != "" {
        selectedAccountIndex = searchAccount(context.String("account"))
    }

    userInfoList := util.GetUserInfoList()
    tweetAccount := userInfoList[selectedAccountIndex]
/* ~~ */

func searchAccount(accountId string) int {
    userInfoList := util.GetUserInfoList()

    selectedUserIndex := 0
    for index, element := range userInfoList {
        if element["userId"] == accountId {
            selectedUserIndex = index
        }
    }
    return selectedUserIndex
}

anacondaにデータを渡し、POSTする

元ソースコードはここ

/* ~tweet関数内~ */
    err := doTweet(tweetAccount, context.Args().Get(0))
    util.Check(err)
/* ~~ */

func doTweet(tweetAccount map[string]string, tweetContents string) error {
    anaconda.SetConsumerKey(os.Getenv("TWITTER_CLI_CONSUMER_KEY"))
    anaconda.SetConsumerSecret(os.Getenv("TWITTER_CLI_CONSUMER_SECRET_KEY"))

    api := anaconda.NewTwitterApi(tweetAccount["accessToken"], tweetAccount["accessSecret"])

    tweet, err := api.PostTweet(tweetContents, nil)
    if err == nil {
        fmt.Print("[Tweet by " + tweetAccount["userId"] + " Successed] ")
        fmt.Println(tweet.Text)
        return nil
    } else {
        return err
    }
}

まとめ

以上の機能を一通り実装したところで一旦の完成とした。
他にもアカウント削除機能の実装やファイル周りの処理だけをまとめたファイルを作ったりもしたが、この記事に書くほどでもないので、気になればGitHubのリポジトリを見て欲しい。

このツールを作った事によってGolangで環境変数にアクセス、ファイルの処理周りを勉強することができた。
これでやっと、一番作りたいと思っていたCLIアプリを実装するのに必要な要件が揃った。

また出来上がったら記事にして公開するので、その時はぜひいいねよろしくお願いします!

そして、まだだいぶと不便なところがあったりもするが、よければcli-tweeter使ってみてください!

読んでくださってありがとうございました!

P.S. あと、Golandがめっちゃ便利ってことに改めて気が付かされた…。
ぜひGolang初学者はGoland使って触るのがおすすめ。