2016/11/15


数年前 Twitter の API はベーシック認証、かつ Public な API だったので Twitter Bot なんて楽ちんで作れたのですが、最近は認証は OAuth、Public な API も無くなってしまい Bot を作るのはとても難しくなりました。

やりたい事はそんなに大した事ではないのに、認証のおかげでコードも大きくなりがちで、気付けば本体コードよりも認証用のコードの方が多くなってしまう事もあります。Bot を作りたいのに敷居が高くて手を出せない、なんて思っている方も多いかもしれません。

でももし Twitter Bot がシェルで書けたら、簡単だと思いませんか?実は twty というツールを使うと簡単に Twitter 連携アプリケーションが作れてしまうのです。

GitHub - mattn/twty: command-line twitter client written in golang

README.md twty A command-line twitter client Install Install golang environment. see: http://golang....

https://github.com/mattn/twty

twty は端末で Twitter をする為のコンソールアプリなのですが、実はストリーミングを表示する -S オプション、出力を JSON にする -json オプションがあります。ですので出力を grep したり jq で加工したり出来るのです。

twty のインストールは以下の手順で行います。
go get github.com/mattn/twty

先に twty を1回起動しておいて下さい。もし既にログイン済みで、通常アカウントと異なるユーザで Bot を実行したいならログアウトし直して PIN コードを入力して下さい。

また通常 twty を使うユーザと Bot ユーザを分けたい場合は、twty に -account xxx フラグ(xxx は人気)を付けて起動して下さい。以降のソースコードでも変更が必要です。

まずは以下のソースコードを見て下さい。

#!/bin/sh

twty -json -S | while read -r LINE; do
  TEXT=`echo "$LINE" | jq -r .text`
  ID=`echo "$LINE" | jq -r .id_str`
  SCREENNAME=`echo "$LINE" | jq -r .user.screen_name`
  if echo "LINE" | grep -q '^RT @'; then
    continue
  fi
  if echo "$LINE" | grep -q 'ぬるぽ'; then
    twty -i $ID "@$SCREENNAME ガッ"
  fi
done

このコードは、twty から JSON ストリーミングを出力させ、1行毎に while ループが実行されます。ループ内では TEXT と ID、SCREENNAME を jq を使って取得し、grep で RT を除け、「ぬるぽ」という発言を検知したら twty を使って「ガッ」を返信します。その際 -i オプションでリプライID(in-reply-to)を指定しているのでちゃんと返信として発言します。

たったこれだけのソースですが、「ぬるぽ」の発言に「ガッ」を応答する Bot が出来ました。簡単ですね。

アイデアさえあれば色んな物が作れるはずです。

また発言に対するアクションが既に外部コマンドとして存在し、返信する必要もない場合には jsonargs が使えます。

GitHub - mattn/jsonargs: xargs for JSON stream

README.md jsonargs xargs for JSON stream Usage input data { "name": "foo1", "value": "bar1" } { "nam...

https://github.com/mattn/jsonargs

xargs の json ストリーミング版と思って下さい。例えば Twitter のタイムラインストリーミングを加工して外部コマンドを起動するのであれば以下の様に実行します。

twty -S -json | jsonargs my-command {{.user.screen_name}} {{.text}}

my-command というコマンドの第一引数に発言者の名前が、第二引数に発言内容が渡されます。

引数は golang の text/template のフォーマットで記述します。

template - The Go Programming Language

Package template implements data-driven templates for generating textual output.

https://golang.org/pkg/text/template/

応用例として、Twitter のタイムラインをデスクトップの画面に通知するコードを書いてみます。Mac であれば gntp-send の代わりに growl-notify を使うといいかもしれません。(Windows の場合は Growl for Windows が必要です)

twty -S -json | jsonargs gntp-send {{.user.screen_name}} {{.text}} {{.user.profile_image_url}}
twtyをgrowl通知

今あるツールを組み合わせて簡単に Twitter と連携できる様になりました。認証が難しいから Twitter 連携ツールが作れない、そんな風に思っておられたならぜひ twty を使ってみて下さい。

入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界 入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界
ブルース・ブリン, Bruce Blinn
ソフトバンククリエイティブ / ¥ 3,456 (2003-02)
 
発送可能時間:在庫あり。


2016/11/06


golang 1.8 では database/sql に幾らかの新機能が追加されます。
  • キャンセル可能なクエリ
  • データベースの型の可視化
  • 複数の結果セット
  • サーバへのping
  • トランザクション分離レベル
  • 名前付きパラメータ
database/sql changes - Google ドキュメント
https://docs.google.com/document/d/1F778e7ZSNiSmbju3jsEWzShcb8lIO4kDyfKDNm4PNd8/edit#

本記事では Golang 1.8 で追加される database/sql の変更内容と、go-sqlite3 での対応状況、利用する上での注意点等を書いていきます。

キャンセル可能なクエリ

実行が長いクエリがキャンセルできるようになります。各 API に Context のサフィックスが付いた物が提供されます。具体的には Query であれば QueryContext、Exec であれば ExecContext が追加されます。第一引数に context.Context が渡され、外部からクエリのキャンセルが行えます。context.WithCancel でキャンセル用の関数を得て、キャンセルしたいタイミングで呼び出します。単純にタイムアウト目的であれば context.WithTimeout が使えます。

ctx, cancel := context.WithCancel(context.Background())
go func() {
    // 1秒待ってからキャンセル
    time.Sleep(1 * time.Second)
    cancel()
}()

stmt, err := db.PrepareContext(ctx, "SELECT name FROM test where id = ?")
if err != nil {
    panic(err)
}
defer stmt.Close()

rows, err := stmt.QueryContext(ctx, id)
if err != nil {
    log.Fatal(err)
}

ただしこの動作はデータベースのドライバにより異なります。MySQL の場合はクエリ毎にキャンセルできる仕組みがデータベースに備わっていますが、SQLite3 はクエリ毎ではありません。おそらく MySQL 向けのドライバはクエリ毎にキャンセル出来る実装を作ってくると思いますが、SQLite3 ドライバではデータベース全体でのキャンセルになります。もし個別にキャンセルしたいのであれば接続ごと作り直し別のコネクションでクエリを発行して下さい。(この動作は現状、暫定的です)

データベースの型の可視化

これまでは結果のカラム名を得る事は出来ましたが、型を得る事が出来ませんでした。しかし go1.8 からはドライバ側から型を得られる様になります。以下の情報が得られる様になります。
  • データベース上の型名
  • reflect.Type で表す型
  • 長さ
  • 厳密な数値のスケール
  • Null 可否

これまでも interface{} のポインタを使って Scan を呼び出す事でダイナミックな値をスキャンする事が出来ましたが、reflect.Type を使う事で、データベース側から予め型を提供できる様になる為、よりシームレスな値の取得が出来る様になります。また Null 可否が得られる様になるので例えばデータベースオーサリングツールを作る際には便利だと思います。実際には、Scan を使う限りそれほど便利にはなりません。しかしこれは今後、golang の database/sql として Blob をサポートしたり Blob を io.Reader/io.Writer で読み書き出来る様にする為の布石だと僕は思っています。

go-sqlite3 は上記の API を全てサポート済みです。

複数の結果セット

幾らかのデータベースでは複数の結果セットをサポートしています。複数のクエリを一度に発行し、結果セットを一度に得ます。

for {
    for rows.Next() {
        if err := rows.Scan(...); err != nil {
            log.Fatal(err)
        }
    }
    if !rows.NextResultSet() {
        break
    }
}

go-sqlite3 もサポートする予定でしたが、sqlite3 の複数クエリ実行は golang の複数結果セットが期待する物と異なる為、現状は実装を見送りました。

サーバへのPing

データベースが接続可能かどうかは、実際に接続してみないと分からない仕様であった為、今ある Ping の実装も実際に Open を呼び出しているだけでした。golang 1.8 ではこの実装をドライバ側に委ねられる様になりました。もちろん context.Context が引き渡されるため、データベースがサポートしているのであれば Ping の中断も行えます。go-sqlite3 でもサポートしています。ただし SQLite3 の場合はコネクションが閉じたかどうかでしか判断していませんが。

トランザクション分離レベル

トランザクション分離レベルをユーザ側から指定できる様になります。具体的には以下の種別を BeginContext で与えた context.Context に IsolationContext でレベル指定できる様になります。
  • LevelDefault
  • LevelReadUncommitted
  • LevelReadCommitted
  • LevelWriteCommitted
  • LevelRepeatableRead
  • LevelSnapshot
  • LevelSerializable
  • LevelLinearizable

ただしこれらはデータベースドライバによりサポート状況が異なるはずです。例えば SQLite3 であればデータベース全体でのロックしか粒度が設定できません。これについては現在実装を検討中です。それまではデフォルト以外の IsolationLevel は未サポートとなります。

名前付きパラメータ

これが今回、ユーザにとっては一番大きな変更点だと思います。これまでは Exec や Query 時に引数で値を受け渡す API であった為、名前付き引数が実現できませんでした。例えば PostgreSQL の場合はプレースホルダが $1$2 という指定であった為、引数の順番をプレースホルダ側から指定できるのですがこの形式を取らないデータベースでは引数の順番だけでプレースホルダの位置が決められません。今回の変更では以下の様に名前付き引数を値として渡せる様になりました。

row := db.QueryRow(`
    select id, extra from foo where id = :id and extra = :extra
`, sql.Param(":id"1), sql.Param(":extra""foo"))

SQL 内で値を使いまわせるのでとても便利になります。go-sqlite3 で対応済みです。

各ドライバの対応状況

現状僕が知る限り go-sqlite3 以外のドライバ全てではまだ実装が進んでいません(というか始まってすらいないかも)。go-sqlite3 がこの仕様に乗っかり人柱になろうという目論見です。名前付きパラメータはおそらく問題なく使えるかと思いますが、IsolateLevel やクエリキャンセルについては今後動作を変えるかもしれません。


2016/11/01


いい記事に感化されて僕も何か書きたくなった。

GolangでAPI Clientを実装する | SOTA

GolangでAPI Clientを実装する 特定のAPIを利用するコマンドラインツールやサービスを書く場合はClientパッケージ(SDKと呼ばれることも多いが本記事ではClientと呼ぶ)を使うこ...

http://deeeet.com/writing/2016/11/01/go-api-client/

この先、JSON REST API のエンドポイントに対して Golang の struct を用意していく訳だけど、ここが一番かったるい作業で一番手を抜きたい所だと思います。そこで便利なのが JSON-to-Go です。

JSON-to-Go: Convert JSON to Go instantly

JSON-to-Go Convert JSON to Go struct This tool instantly converts JSON into a Go type definition. Pa...

https://mholt.github.io/json-to-go/ JSON-to-Go

このサイトの左側ペインに JSON を与えると、Golang の struct が一気に生成できます。例えば GitHub API でユーザ情報を取得する API の JSON は以下の様になります。

{
  "login": "octocat",
  "id": 1,
  "avatar_url": "https://github.com/images/error/octocat_happy.gif",
  "gravatar_id": "",
  "url": "https://api.github.com/users/octocat",
  "html_url": "https://github.com/octocat",
  "followers_url": "https://api.github.com/users/octocat/followers",
  "following_url": "https://api.github.com/users/octocat/following{/other_user}",
  "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
  "organizations_url": "https://api.github.com/users/octocat/orgs",
  "repos_url": "https://api.github.com/users/octocat/repos",
  "events_url": "https://api.github.com/users/octocat/events{/privacy}",
  "received_events_url": "https://api.github.com/users/octocat/received_events",
  "type": "User",
  "site_admin": false,
  "name": "monalisa octocat",
  "company": "GitHub",
  "blog": "https://github.com/blog",
  "location": "San Francisco",
  "email": "octocat@github.com",
  "hireable": false,
  "bio": "There once was...",
  "public_repos": 2,
  "public_gists": 1,
  "followers": 20,
  "following": 0,
  "created_at": "2008-01-14T04:33:35Z",
  "updated_at": "2008-01-14T04:33:35Z"
}

これを JSON-to-Go に食わせると

type AutoGenerated struct {
    Login string `json:"login"`
    ID int `json:"id"`
    AvatarURL string `json:"avatar_url"`
    GravatarID string `json:"gravatar_id"`
    URL string `json:"url"`
    HTMLURL string `json:"html_url"`
    FollowersURL string `json:"followers_url"`
    FollowingURL string `json:"following_url"`
    GistsURL string `json:"gists_url"`
    StarredURL string `json:"starred_url"`
    SubscriptionsURL string `json:"subscriptions_url"`
    OrganizationsURL string `json:"organizations_url"`
    ReposURL string `json:"repos_url"`
    EventsURL string `json:"events_url"`
    ReceivedEventsURL string `json:"received_events_url"`
    Type string `json:"type"`
    SiteAdmin bool `json:"site_admin"`
    Name string `json:"name"`
    Company string `json:"company"`
    Blog string `json:"blog"`
    Location string `json:"location"`
    Email string `json:"email"`
    Hireable bool `json:"hireable"`
    Bio string `json:"bio"`
    PublicRepos int `json:"public_repos"`
    PublicGists int `json:"public_gists"`
    Followers int `json:"followers"`
    Following int `json:"following"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

Golang の struct が一瞬で出来上がります。struct 名は AutoGenerated になっているので目的の名前で置き換えて下さい。このサイト、javascript だけで実装されているので JSON はどこにも送信されません。もしどうしても外部のサイトに JSON を食わしたくないというのであれば以下のリポジトリを clone して自前で実行すれば良いです。

GitHub - mholt/json-to-go: Translates JSON into a Go type in your browser instantly

README.md Translates JSON into a Go type definition. Check it out! This is a sister tool to curl-to-...

https://github.com/mholt/json-to-go

尚、いちいちサーバ立ち上げるのめんどくさいという事であれば、コマンドラインで実行するためのパッチもあります。

Publish CLI to npm? · Issue #17 · mholt/json-to-go · GitHub
https://github.com/mholt/json-to-go/issues/17#issuecomment-224544937

便利ですね。