Neutral Scent

App developments & Gadgets

AsyncOAuthとWebAuthenticationBrokerで作るuniversal App用Twitter APIラッパークラス

世界同時WP&W8 Devハッカソンイベント//Publish/、がりがり書いてたアプリが結局仕様的に簡単には実現できなそうな袋小道に迷い込んでしまったため、結果出ませんでした、無念...。

とはいえ、会場で結構話題になっていた、universal App(特にWindows Phone版)でのWebAuthenticationBrokerの挙動をまるっとラップして、AsyncOAuthと合わせてモダンにさくっとTwitterアプリが作れるラッパークラスを書いてみたので、universalでTwitterアプリやってみたい人のご参考になれば幸いです。ベースが切り張なので突っ込みどころはあると思いますが。
HTTP周りはAsyncOAuthがまるっと包んでくれますし、OS標準のWebAuthenticationBrokerのおかげでWebBrowserコントロールを張ってごにょごにょする必要もなく、今なら最短に近い組み合わせではないかと思います。

public class TwitterHandler
{
    //neue cc - AsyncOAuth - C#用の全プラットフォーム対応の非同期OAuthライブラリ
    //http://neue.cc/2013/02/27_398.html
    //AsyncOAuth/AsyncOAuth.ConsoleApp/Twitter.cs at master · neuecc/AsyncOAuth
    //https://github.com/neuecc/AsyncOAuth/blob/master/AsyncOAuth.ConsoleApp/Twitter.cs
    internal const string API_CONSUMER_TOKEN = "";
    internal const string API_CONSUMER_SECRET = "";
    internal const string API_CONSUMER_CALLBACK_URL = "";

    private readonly OAuthAuthorizer authorizer;
    private static RequestToken requestToken { get; set; }
    private static AccessToken accessToken { get; set; }
        
    public TwitterHandler()
    {
        AsyncOAuth.OAuthUtility.ComputeHash = (key, buffer) =>
        {
            var crypt = Windows.Security.Cryptography.Core.MacAlgorithmProvider.OpenAlgorithm("HMAC_SHA1");
            var keyBuffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(key);
            var cryptKey = crypt.CreateKey(keyBuffer);

            var dataBuffer = Windows.Security.Cryptography.CryptographicBuffer.CreateFromByteArray(buffer);
            var signBuffer = Windows.Security.Cryptography.Core.CryptographicEngine.Sign(cryptKey, dataBuffer);

            byte[] value;
            Windows.Security.Cryptography.CryptographicBuffer.CopyToByteArray(signBuffer, out value);
            return value;
        };
        authorizer = new OAuthAuthorizer(API_CONSUMER_TOKEN, API_CONSUMER_SECRET);
    }

    public async Task Auth()
    {
        accessToken = null;
        var res = await authorizer.GetRequestToken("https://api.twitter.com/oauth/request_token");
        requestToken = res.Token;

        var uriAuthorize = new Uri(authorizer.BuildAuthorizeUrl("https://api.twitter.com/oauth/authorize", requestToken));
        var uriCallback = new Uri(API_CONSUMER_CALLBACK_URL);
#if WINDOWS_PHONE_APP
        WebAuthenticationBroker.AuthenticateAndContinue(uriAuthorize, uriCallback, null, WebAuthenticationOptions.None);
#else // !WINDOWS_PHONE_APP
        var webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.None, uriAuthorize, uriCallback);
        if (webAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success)
        {
            Debug.WriteLine(webAuthenticationResult.ResponseData.ToString());
            await GetAccessToken(webAuthenticationResult.ResponseData.ToString());
        }
        else if (webAuthenticationResult.ResponseStatus == WebAuthenticationStatus.ErrorHttp)
        {
            MessageDialog ErrMsg = new MessageDialog(string.Format("HTTP Error returned by AuthenticateAsync() : " + webAuthenticationResult.ResponseErrorDetail.ToString()), "Sorry");
            await ErrMsg.ShowAsync();
        }
        else
        {
            MessageDialog ErrMsg = new MessageDialog(string.Format("Error returned by AuthenticateAsync() : " + webAuthenticationResult.ResponseStatus.ToString()), "Sorry");
            await ErrMsg.ShowAsync();
        }
#endif // !WINDOWS_PHONE_APP
    }

#if WINDOWS_PHONE_APP
    //Windows Phone Web authentication broker sample in C#, C++, JavaScript for Visual Studio 2013
    //http://code.msdn.microsoft.com/wpapps/Web-Authentication-d0485122
    //How to continue your Windows Phone Store app after calling an AndContinue method (Windows)
    //http://msdn.microsoft.com/en-us/library/dn631755.aspx
    //How to Use the WebAuthenticationBroker for oAuth in a Windows Phone Runtime WP8.1 App | .NET Zone
    //http://dotnet.dzone.com/articles/how-use
    public async void ContinueWebAuthentication(WebAuthenticationBrokerContinuationEventArgs args)
    {
        WebAuthenticationResult result = args.WebAuthenticationResult;

        if (result.ResponseStatus == WebAuthenticationStatus.Success)
        {
            await GetAccessToken(result.ResponseData.ToString());
        }
        else if (result.ResponseStatus == WebAuthenticationStatus.ErrorHttp)
        {
            MessageDialog ErrMsg = new MessageDialog(string.Format("There was an error connecting to Twitter: \n {0}", result.ResponseErrorDetail.ToString()), "Sorry");
            await ErrMsg.ShowAsync();
        }
        else
        {
            MessageDialog ErrMsg = new MessageDialog(string.Format("Error returned: \n{0}", result.ResponseStatus.ToString()), "Sorry");
            await ErrMsg.ShowAsync();
        }
    }
#endif // WINDOWS_PHONE_APP

    private async Task GetAccessToken(string webAuthResultResponseData)
    {
        string responseData = webAuthResultResponseData.Substring(webAuthResultResponseData.IndexOf("oauth_token"));
        string oauthVerifier = null;
        String[] keyValPairs = responseData.Split('&');

        for (int i = 0; i < keyValPairs.Length; i++)
        {
            String[] splits = keyValPairs[i].Split('=');
            switch (splits[0])
            {
                case "oauth_verifier":
                    oauthVerifier = splits[1];
                    break;
            }
        }
        var accessTokenResponse = await authorizer.GetAccessToken("https://api.twitter.com/oauth/access_token", requestToken, oauthVerifier);

        accessToken = accessTokenResponse.Token;
    }

    public HttpClient CreateTwitterHttpClient()
    {
        if (!IsAuthrized)
        {
            return null;
        }
        return OAuthUtility.CreateOAuthClient(API_CONSUMER_TOKEN, API_CONSUMER_SECRET, new AccessToken(accessToken.Key, accessToken.Secret));
    }

    public bool IsAuthrized
    {
        get { return accessToken != null && !string.IsNullOrWhiteSpace(accessToken.Key) && !string.IsNullOrWhiteSpace(accessToken.Secret); }
    }

    // Example API Call Handlers
    // AsyncOAuth/AsyncOAuth.ConsoleApp/Twitter.cs at master &middot; neuecc/AsyncOAuth
    // https://github.com/neuecc/AsyncOAuth/blob/master/AsyncOAuth.ConsoleApp/Twitter.cs
    public async Task<string> GetTimeline(int count, int page)
    {
        var client = CreateTwitterHttpClient();

        var json = await client.GetStringAsync("https://api.twitter.com/1.1/statuses/home_timeline.json?count=" + count + "&page=" + page);
        return json;
    }

    // Your Twitter Application must have Read & Write permission in Twitter API for Post handlers
    public async Task<string> PostUpdate(string status)
    {
        var client = CreateTwitterHttpClient();

        var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("status", status) });

        var response = await client.PostAsync("https://api.twitter.com/1.1/statuses/update.json", content);
        var json = await response.Content.ReadAsStringAsync();
        return json;
    }
}

Windows Phone版では、認証を終わらせるために、アプリが再アクティブ化されるため、App.xaml.csに以下のようなコードを追加する必要があります。(ViewModel.TwitterHandler〜は個々の実装に合わせて変更)

    public sealed partial class App : Application
    {
        protected override void OnActivated(IActivatedEventArgs args)
        {
            base.OnActivated(args);
#if WINDOWS_PHONE_APP
            if (args is WebAuthenticationBrokerContinuationEventArgs)
                ViewModel.TwitterHandler.ContinueWebAuthentication(args as WebAuthenticationBrokerContinuationEventArgs);
#endif // WINDOWS_PHONE_APP
        }
    }

あとは、適当なところでTwitterHandlerのインスタンスを作ってAuth()を呼んで、認証終わったらAPIをたたいていくだけです。詳しくはコード冒頭のneuecc先生のリンク等を参照のこと。
ふつうのuniversal AppテンプレートにnugetでAsyncOAuthを入れて、上記のコードを組み込み、あとは細かいAPIを実装していくだけで、わりとシンプルにTwitterアプリを作成できるかと思います。
(現状、nugetでAsyncOAuthをuniversalのWP8.1アプリに組み込むと依存関係の不足で起動時にFileNotFoundが出るかもしれません。その場合、nugetでさらにBCL Async(Microsoft Async)を追加してみてください)