ReactiveOAuth ver.0.4 - Twitpic(OAuth Echo)対応

ver.0.4になりました。少し前に0.3.0.1をこっそり出していたので、それを含めて0.3からの差分は、「対象Rxのバージョンが現在最新の1.0.10605(Stable)」に、というのと「Realmが含まれていると認証が正しく生成出来なかったバグの修正」と、「TwitpicClientサンプルの追加」になります。バグのほうは本当にすみません……。Twitterでしかテストしてない&TwitterはRealm使わないため、全然気づいていなくて。ダメですねホント。

OAuth Echo

TwitpicはOAuth Echoという仕組みでTwitterと連携した認証をして、画像を投稿できます。詳しくはUsing OAuth Echo | dev.twitter.comTwitPic Developers - API Documentation - API v2 » uploadにありますが、よくわかりませんね!Twitpicに画像を投稿、というわけでTwitpicのAPIにアクセスするわけですが、その際のヘッダにTwitterに認証するためのOAuthのヘッダを付けておくと、Twitpic側がTwitterに問い合せて認証を行う。という仕組みです、大雑把に言って。

ただのOAuthとはちょっと違うので、今までのReactiveOAuthのOAuthClientクラスは使えない。けれど、認証用ヘッダの生成は同じように作る。というわけで、ここはReactiveOAuthにひっそり用意されているOAuthBaseクラスを継承して、Twitpic専用のTwitpicClientクラスを作りましょう。

が、作るのもまた少し面倒なので Sample/TwitpicClient/TwitpicClient.cs に作成したのを置いておきました。ファイルごとコピペってご自由にお使いください。.NET 4 Client Profile, Silverlight 4, Windows Phone 7の全てに対応しています。

Windows Phone 7でのカメラ撮影+投稿のサンプル

TwitpicClient.cs の解説は後でやりますが、その前に利用例を。WP7でカメラ撮影+投稿をしてみます。CameraCaptureTaskの利用法に関しては CameraCaptureTaskを使ってカメラで静止画撮影を行う – CH3COOH(酢酸)の実験室 を参考にさせて頂きました。TwitterのAccessTokenの取得に関しては、ここでは解説しませんので neue cc - ReactiveOAuth - Windows Phone 7対応のOAuthライブラリ を参照ください。

// CameraCaptureTaskのCompletedイベント
void camera_Completed(object sender, PhotoResult e)
{
    if (e.TaskResult == TaskResult.OK)
    {
        // 撮影画像(Stream)をバイト配列に格納
        var stream = e.ChosenPhoto;
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
 
        // key, secret, tokenは別に設定・取得しておいてね
        new TwitpicClient(ConsumerKey, ConsumerSecret, accessToken)
            .UploadPicture(e.OriginalFileName, "from WP7!", buffer)
            .ObserveOnDispatcher()
            .Catch((WebException ex) =>
            {
                MessageBox.Show(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd());
                return Observable.Empty<string>();
            })
            .Subscribe(s => MessageBox.Show(s), ex => MessageBox.Show(ex.ToString()));
    }
}

new TwitpicClient(キー, シークレット, アクセストークン).UploadPicture(ファイル名, メッセージ, 画像) といった風に使います。戻り値はIObservable<string>で結果(投稿後のURLとか)が返ってくるので、あとは好きなように。投稿に失敗した場合は、WebExceptionが投げられるので、それを捉えてエラーメッセージを読み取ると開発には楽になれそうです。

TwitpicClient.cs

以下ソース。Sample/TwitpicClient/TwitpicClient.cs と同じですが、自由にコピペって使ってください。大事なことなので2回言いました。このコード自体はTwitpicに特化してありますが、認証部分のヘッダを少しと画像アップロードを変更する部分を弄れば、他のOAuth Echoサービスにも対応させることができると思います。

using System;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
 
#if WINDOWS_PHONE
using Microsoft.Phone.Reactive;
#else
using System.Reactive.Linq;
#endif
 
namespace Codeplex.OAuth
{
    public class TwitpicClient : OAuthBase
    {
        const string ApiKey = ""; // set your apikey
 
        readonly AccessToken accessToken;
 
        public TwitpicClient(string consumerKey, string consumerSecret, AccessToken accessToken)
            : base(consumerKey, consumerSecret)
        {
            this.accessToken = accessToken;
        }
 
        private WebRequest CreateRequest(string url)
        {
            const string ServiceProvider = "https://api.twitter.com/1/account/verify_credentials.json";
            const string Realm = "http://api.twitter.com/";
 
            var req = WebRequest.Create(url);
 
            // generate oauth signature and parameters
            var parameters = ConstructBasicParameters(ServiceProvider, MethodType.Get, accessToken);
            // make auth header string
            var authHeader = BuildAuthorizationHeader(new[] { new Parameter("Realm", Realm) }.Concat(parameters));
 
            // set authenticate headers
            req.Headers["X-Verify-Credentials-Authorization"] = authHeader;
            req.Headers["X-Auth-Service-Provider"] = ServiceProvider;
 
            return req;
        }
 
        public IObservable<string> UploadPicture(string filename, string message, byte[] file)
        {
            var req = CreateRequest("http://api.twitpic.com/2/upload.xml"); // choose xml or json
            req.Method = "POST";
 
            var boundaryKey = Guid.NewGuid().ToString();
            var boundary = "--" + boundaryKey;
            req.ContentType = "multipart/form-data; boundary=" + boundaryKey;
 
            return Observable.Defer(() =>
                    Observable.FromAsyncPattern<Stream>(req.BeginGetRequestStream, req.EndGetRequestStream)())
                .Do(stream =>
                {
                    using (stream)
                    using (var sw = new StreamWriter(stream, new UTF8Encoding(false)))
                    {
                        sw.WriteLine(boundary);
                        sw.WriteLine("Content-Disposition: form-data; name=\"key\"");
                        sw.WriteLine();
                        sw.WriteLine(ApiKey);
 
                        sw.WriteLine(boundary);
                        sw.WriteLine("Content-Disposition: form-data; name=\"message\"");
                        sw.WriteLine();
                        sw.WriteLine(message);
 
                        sw.WriteLine(boundary);
                        sw.WriteLine("Content-Disposition: form-data; name=\"media\"; filename=\"" + filename + "\"");
                        sw.WriteLine("Content-Type: application/octet-stream");
                        sw.WriteLine("Content-Transfer-Encoding: binary");
                        sw.WriteLine();
                        sw.Flush();
 
                        stream.Write(file, 0, file.Length);
                        stream.Flush();
 
                        sw.WriteLine();
                        sw.WriteLine("--" + boundaryKey + "--");
                        sw.Flush();
                    }
                })
                .SelectMany(_ => Observable.FromAsyncPattern<WebResponse>(req.BeginGetResponse, req.EndGetResponse)())
                .Select(res =>
                {
                    using (res)
                    using (var stream = res.GetResponseStream())
                    using (var sr = new StreamReader(stream, Encoding.UTF8))
                    {
                        return sr.ReadToEnd();
                    }
                });
        }
    }
}

認証ヘッダ作成はConstructBasicParametersとBuildAuthorizationHeaderというprotectedメソッドで行います。わけわかんないよね…気持ち悪いよね…。使いにくいメソッドです、すみません、私もそう思います。そういうものだと思って、見ないふりしてもらえれば幸いです。

コードの大半を占めているのは画像を投稿するためのmultipart/form-dataのもので、これはもうOAuth Echo関係ない話、で、面倒ぃ。特にWP7での非同期だと涙が出る。POSTはBeginGetRequestStreamとBeginGetResponseの二つの非同期メソッドをセットで使う必要があるため、コードがごちゃごちゃするのです。

しかしReactive Extensionsを使えばあら不思議!でもないですが、ネストがなくなって完全に平らなので、結構普通に読めるのではないでしょうか?(ストリーム書き込みのコード量が多いのは、これは同期でやっても同じ話なので)。例外処理も利用例のところで見たように、Catchメソッドをくっつけるだけ。実に色々とスッキリします。

Rxがあれば非同期POSTも怖くない。

やっていることは単純で、FromAsyncPatternでBegin-Endを変換。StreamへのWriteは後続への射影はなく、対象(Stream)に対しての副作用(書き込み)のみなのでDo、RequestStream->Responseへの切り替えはSelectMany、Responseから結果のStringへの変換はSelect、と、お決まりの定型メソッドに置き換えていっただけです。この辺はパターンみたいなものなので、これやるにはこのメソッドね、というのを覚えてしまえばそれでお終いです。

Stream読み書きは非同期にしないの?

StreamにもBeginReadとかBeginWriteとかありますものね。しかし、しません(キリッ。理由は死ぬほど面倒だからです。やってみると分かりますが想像以上に大変で、おまけに何とか実現するためにはRxでのチェーンを大量に重ねる必要がありオーバーヘッドがバカにならない……。なので、わざわざやるメリットも全くありません。

一応、ReactiveOAuthのOAuthClientは、そこも非同期でやってますが、わざわざ頑張った意味があったかは、かなり微妙なところ。実装は Internal/AsynchronousExtensions.cs にあるので参照ください。それと、この AsynchronousExtensions.cs はReactive Extensionsで非同期処理を簡単にで言った「拡張メソッドのすゝめ」を実践したものでもあります。WebRequestはプリミティブすぎて扱い難いので、Rxに特化したうえで簡単に扱えるようにDownloadStringやUploadValueなどといったメソッドを拡張してあります。便利だと思いますので、こちらも TwitpicClient.cs と同様に、ファイルごと自由にコピペって使ってやってください。

まとめ

ReactiveOAuthを公開する目的に、「これが入り口になってRxの世界を知ってもらえると嬉しい」というのもあったのですが、WP7開発で利用してもらったりと、その目的は少しは達成出来たかもで、良かった良かった。ちょっと練りたりなかったり、未だにバグがあったり(本当にごめんなさい!)と至らない点も多いですが、今後も改善していきますのでよろしくお願いします。

Comment (9)

Iridium : (06/23 23:29)

前回の記事にコメントでXboxInfoTwitが起動しない旨を書きこみました、Iridiumです。
.NET3.5も、Windowsの機能、に入ってるので入ってるはずなんですが・・・
いまだ起動せず、です。

neuecc : (06/23 23:57)

すみません、全く関係ない記事のところにコメント頂くと厳しいのですが……。
XboxInfoTwitのページの「下記のBlog記事の最新のもの」が指しているのは
そのままページの下記、現在だと http://neue.cc/2011/04/29_319.html になります。

aetos : (06/24 12:16)

ということがあるので、ソフトを公開している人はblogだけでなく掲示板もあるといいよね、と思う

neuecc : (06/24 21:27)

うーん、一応ソフトのページ http://neue.cc/software/xboxinfotwit
の下にカテゴリに一致する記事を連結しているようにしていたし、
今までもそれで回っていたので……。

kazu : (11/10 12:07)

自作でWindowsPhoneアプリを作ってるものです。
Twitter関連の実装がなかなかうまくいかずに悩んでいたところ、とてもすばらしいサイトを発見しました。
早速使わせていただいたのですが、Twitpicに画像をアップする際に、分離ストレージ上のデータを使用したいと考えています。(PhotoCameraクラスを使って写真を撮っているため)
やり方を考えていて行き詰まってしまっています。
何か良い方法はありますでしょうか?

neuecc : (11/13 22:31)

どうもありがとうございます。
また、返信遅れてすみません(コメントに気づいていませんでして)

この例だとカメラからのキャプチャのStreamから画像データを取ってますが
var stream = e.ChosenPhoto;
その部分を、IsolatedStorageFileのOpenFileで、
分離ストレージからStreamを開けばいいと思います。
あとはサンプルコードと同じですね。

kazu : (11/14 21:44)

twitpicの件ではどうもありがとうございました。
わがまま言って申し訳ありませんが、もう一つだけ教えてください。

twitterにpostする機能を使ったところ、「return sr.ReadToEnd();」のところが「NotSupportedExceptionはハンドルされませんでした」とエラーが出てしまいました。
ダウンロードさせていただいたものをそのまま使用しているので、自分で変に改変してしまったということはありません。
ご教授いただけませんでしょうか?

kazu : (11/15 21:35)

11/14に投稿させていただいた件、自己解決しました。
ご迷惑をおかけしました・・・。

neuecc : (11/21 23:32)

いえいえー、解決したのなら何よりです。

Name
WebSite(option)
Comment

Trackback(1) | http://neue.cc/2011/06/23_330.html/trackback

.NET Clips : (06/29 00:52)

neue cc - ReactiveOAuth ver.0.4 - Twitpic(OAuth Echo)対応…

素敵なエントリーの登録ありがとうございます - .NET Clipsからのトラックバック…

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for .NET(C#)

April 2011
|
March 2017

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc