2014年9月8日月曜日

LINQ to Twitter v3で遊ぶ

今回はLINQ to Twitterのネタです。

C#でTwitterやるんだったらLINQ to Twitterでしょう。TwitterのAPI変更にもしつこくついていってくれて、ユーザーとしてはとても助かります。

久しぶりにLINQ to Twitterをいじったらバージョンが2から3に上がってました。ほうほう。と思って、安易にアップデートしてみたらいろいろと破壊的仕様変更がされていて焦りました。 いろいろといじってみるとバージョン3になってから.NET4.5の非同期処理に対応したようでした。Asyncメソッドの嵐です。まあ、Windowsストアアプリとかに使われることも想定しているライブラリですから、当然といえば当然の流れですよね。

ということで、使い方を一通りまとめてみます。

認証


Twitterのアプリケーションを作成するにはConsumerKeyとConsumerSecretが必要なことは多くの人がご存知かと思います。https://apps.twitter.com/からアプリケーションを登録してこれらのキーを手に入れます。

さて、認証に関して、2通りの方法を紹介します。

すでにAccessToken、AccessTokenSecretを知っているとき

アプリケーションに対応するキーの他、そのアプリケーションでユーザーを認証するために使うキーが2つあります。これらのキーを取得する必要がありますが、これを別経路で知っている場合はこれを直接入力して使うことができます(最近の上記のTwitter開発者向けサイトではアプリケーションを登録した本人のそのアプリケーション用のAccessToken、AccessTokenSecretを教えてくれるようです)。もちろん、その場合は他人にそのアプリケーションを渡せなくなる(渡したら自分のアカウントで好き放題されてしまう)ので注意してください。

SingleUserAuthorizer auth = new SingleUserAuthorizer() {
    CredentialStore = new SingleUserInMemoryCredentialStore() {
        ConsumerKey = "****",
        ConsumerSecret = "****",
        AccessToken = "****",
        AccessTokenSecret = "****"
    }
};

このように書けばおkです。超簡単です。

PINコードで認証をするとき

さて、では実際に自分のアプリケーションを他人に配布できるようなものにするとき、上記のような方法では不十分ですね。PIN認証する必要があります。

PinAuthorizer auth = new PinAuthorizer() {
    CredentialStore = new InMemoryCredentialStore() {
        ConsumerKey = "****",
        ConsumerSecret = "****",
    }
};

auth.SupportsCompression = false;
auth.GoToTwitterAuthorization = (link) => {
    Process.Start(link);
    Console.WriteLine(link);
};
auth.GetPin = () => {
    Console.Write("Input PIN :");
    return Console.ReadLine();
};
auth.AuthorizeAsync().Wait();

ConsumerKeyとConsumerSecretを設定し、認証用リンクを受け取り適切に転送するデリゲート、入力されたPINコードを返すデリゲートを設定してやります。そして、AuthorizeAsync関数を呼び出せば非同期で認証を行ってくれます。非同期ですので、これを同期的に処理するためにWaitメソッドを呼び出して終了を待っています。こうすると、linkのところでブラウザを起動して、ReadLineでPINの入力を要求して入力が終わったら認証が完了します。もしも誤ったPINが入力されたらWaitメソッドが例外を吐くようなので、適切に処理してやれば良いでしょう。

各種データの取得

タイムラインを取得する

つづいてタイムラインを取得します。この辺りからLINQ to Twitterの威力が発揮されます。

var q = from p in context.Status 
        where p.Type == StatusType.Home && p.Count == 100
        orderby p.CreatedAt
        select p;

foreach(var s in q) {
    ShowStatus(s);
    Console.WriteLine();
}

LINQがわかってる人なら超簡単に取得できていることがわかると思います。個人的に、取得する個数の指定で「p.Count == 100」とかいうのはちょっと不満なんですが…。だって、個々の要素についてのことじゃないじゃんってね。

特定のツイートを取得する

今度は特定のIDのツイートを取得します。InReplyToからリプライ元を取得する時などに役に立ちます。

var q = from p in context.Status
        where p.Type == StatusType.Show && p.ID == 508907673895436288
        select p;

ShowStatus(q.Single());
Console.WriteLine();

とってもシンプルですね。ただ、単一のツイートってわかりきってるのに列挙可能型になるのは冗長な気はしますが。ちなみに、バージョン2まではp.IDはstring型でしたが、バージョン3ではulong型になってました。個人的には良改変だと思ってます。

ユーザー情報を取得する

今度は任意のユーザーのアカウント情報を取得してみましょう。

var q = from p in context.User
        where p.Type == UserType.Show && p.ScreenName == "EH500_Kintarou"
        select p;
User u = q.Single();

Console.WriteLine("UserID = " + u.UserIDResponse);
Console.WriteLine("UserScreenName = " + u.ScreenNameResponse);
Console.WriteLine();

はい、簡単です。
UserクラスのScreenNameはどうもこの検索時にユーザー名を設定するためのプロパティのようで、サーバーからの応答はScreenNameResponseに入るみたいですね。この辺はかなり直感的じゃなくて使いにくいところですが、改良はなかなかされてないですね…。

自分のアカウント情報を取得する

次は自分のアカウント情報を見てみます。特に、PINコードで認証した時なんかは自分のアカウント情報は自明ではありません。調べる必要があります。

var q = from p in context.Account
        where p.Type == AccountType.VerifyCredentials
        select p;
Account a = q.Single();

Console.WriteLine("UserID = " + a.User.UserIDResponse);
Console.WriteLine("UserScreenName = " + a.User.ScreenNameResponse);
Console.WriteLine();

context.Accountを使ってる点はユーザー情報の取得とは違いますが、あとは概ね同じです。
ていうか、この辺全部返ってくる情報は1つってわかってるのに列挙可能型(ry はい、Singleで決め打ちしちゃってます。

UserStreamを受信する

Twitterにはリアルタイムでタイムラインなどを取得する機能があります。さらに、API1.1の仕様変更のタイミングでタイムラインの取得を頻繁にやりすぎるとすぐにAPIが切れてしまう使用になりました。UserStreamは必須と言えるでしょう。

(from p in context.Streaming where p.Type == StreamingType.User select p).StartAsync(async s => {
    await Task.Run(() => {
        Status status = new Status(LitJson.JsonMapper.ToObject(s.Content));
        if(!string.IsNullOrEmpty(status.Text)) {
            ShowStatus(status);
            Console.WriteLine();
        }
    });
});

なんていうか、もはやUserStreamまでにLINQを使う必要なんてあるのかって気すらしてきてしまいますが、その辺はLINQのプライド?なんでしょうね。
若干の設定をしてStartAsyncを呼び出すと、その中のデリゲートにUserStreamを受信するたびにデータが飛んできます。そのデリゲートがasyncなので、中でawaitメソッドの呼び出しをするためにあえてTask.Runを挟んでいます。
ちなみに、受信するデータはStatusだけではないので、あえてシリアライズされたままのJsonの情報で飛んできますので、そのままではツイートは見られません。なので、 new Status(LitJson.JsonMapper.ToObject(s.Content))でStatusクラスを作ってあげています。ただ、これでは例えばフォローされたとかいったような情報が拾えなくなります。ツイートだけを選り分けるためにTextプロパティが空じゃないかを見ていますが、もしもフォローされたとかの情報をちゃんと取得したければ、Jsonの時点でパースをしてあげる必要があるでしょう。
LINQ to TwitterとしてはUserStreamsParser for LinqToTwitterというサードパーティ製のライブラリの使用を推奨しているっぽいれすが、これを昔導入してみたところ、LINQ to Twitterの仕様変更かなんかで上手く動いてくれなかったと記憶しています。このライブラリもアプデされてないっぽいので、まあ、動かないのでしょう。

ツイートする

普通にツイートをする

ツイートも非同期メソッドになっています。

context.TweetAsync("なんていうか、LINQ to Twitter v3で.Wait()の嵐になってる(((").Wait();

非同期メソッドを同期的に使うならば、.Wait()を付ける必要がありますね。

リプライをする

さて、問題はリプライです。というのも、バージョン2ではツイートするためのメソッドであるUpdateStatusのオーバーロードとしてリプライが実装されていました。しかし、同様のオーバーライドがTweetAsyncには見当たりません。
と思って見てみたら、TwitterContextにReplyAsyncという別のメソッドが用意されていました。

context.ReplyAsync(508969959364362242, "非同期メソッドばっかりだからな。").Wait();

これは自分自身へのリプライなので@を本文につける必要が無いパターンですね。

位置情報をツイートをする

位置情報はオーバーロードとして実装されています。

context.TweetAsync("皇居なう(´へωへ`*)", 35.6855323M, 139.7527346M).Wait();

緯度経度はdecimal型なのでMまたはmのサフィックスを付ける必要があります。

画像をツイートする

画像はTweetWithMediaAsyncというメソッドになります。

context.TweetWithMediaAsync("コンソールTL", false, System.IO.File.ReadAllBytes(@"ss001.png")).Wait();

そのまま画像ファイルのバイト列を突っ込めばいいっぽいです。

ツイートを表示する

最後にツイートの表示です。
先ほどのユーザー情報の表示でUserクラスのScreenNameプロパティはデータを読みだした時には使えないなどといった若干のクセがありますが、Statusクラスにも同様にクセがあります。というよりか、C#ユーザーに取って自然な形を目指すよりTwitter APIに忠実にクラスを設計したといった感じなんですかね。

static void ShowStatus(Status s)
{
    if(s.RetweetedStatus != null && !string.IsNullOrEmpty(s.RetweetedStatus.Text)) {
        ShowStatus(s.RetweetedStatus);
        Console.WriteLine("Retweeted by @{0}", s.User.ScreenNameResponse);
    } else {
        Console.WriteLine("@{0}/{1}", s.User.ScreenNameResponse, s.User.Name);
        Console.WriteLine(s.Text.Replace("&lt;", "<").Replace("&gt;", ">").Replace("&amp;", "&"));
        Console.WriteLine("via {0} at {1}", new Regex(@"\>(?<source>.*)\<").Match(s.Source).Groups["source"].Value, s.CreatedAt);
    }
}

まず、リツイートですが、bool型のRetweetedっていういかにもリツイートされたツイートかどうかを示すプロパティっぽいのがありますが、これでリツイートされたツイートかどうかは見分けられないようです結局、RetweetedStatusに中身があるかどうかを見て判断しています。
ツイートの本文は、Jsonのために<>&などといった記号がエスケープされた状態でそのまま格納されています。なので、これらはReplaceを用いて展開をしてやる必要があります。もしかしたらこれ以外にもエスケープされてる文字があるかもしれません。
最後に、viaを表すSourceプロパティですが、これはそのリンクを含めたHTMLのAタグがまるごと入っています。なので、今回は手抜きで>と<に囲まれた領域を正規表現で抜き出してあげています。ちゃんとやるならタグをパースしてあげたりすると良いでしょう。

結構面倒ですね。



とまあ、概ねこんな感じでしょうか…。 まだ検索とか隅々まで試したわけじゃないですが、いろいろ使えそうです。

今回は非同期メソッドを活かしたコードは書いていませんが、また機会があったら書いてみたいですねえ。
Windowsストアアプリ、XAML/C#で作れるけど、それ以外の敷居が高すぎるからなあ…。

0 件のコメント:

コメントを投稿