自動でTweetするBotを作ろうと思って、TwitterのREST APIを使ってみたのだけど、アルファベット以外の文字をTweetするとOAuthに失敗してハマったのでメモしておきます。
TwitterのBotを作る記事はたくさんネット上に転がってるので、ここでは特に説明しないですが、僕が参考にしたのはこの記事。
https://blogs.msdn.microsoft.com/henrikn/2012/02/16/extending-httpclient-with-oauth-to-access-twitter/
で、この中で使っているOAuthBase.csはリンク切れだったのでGitHubにあったのを使ってます。
https://gist.github.com/tsupo/112124
ただ、このOAuthBase.csだとAuthenticationHeaderを作ってくれないのでそこは自分で作成。
| 1 2 3 4 5 6 7 8 | 	string authHeader = "oauth_consumer_key=\"" + _consumerKey + "\"," 		+ "oauth_nonce=\"" + nonce + "\"," 		+ "oauth_signature=\"" + System.Web.HttpUtility.UrlEncode(signature) + "\"," 		+ "oauth_signature_method=\"HMAC-SHA1\"," 		+ "oauth_timestamp=\"" + timestamp + "\"," 		+ "oauth_token=\"" + _token + "\"," 		+ "oauth_version=\"1.0\""; 	request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("OAuth", authHeader); | 
TweetするコードはHttpClientを使ってこんな感じ。
| 1 2 3 4 5 | 	string message = "Test"; // Tweetするメッセージ. 	HttpClient httpClient = new HttpClient(new OAuthMessageHandler(new HttpClientHandler())); 	Uri uri = new Uri("https://api.twitter.com/1.1/statuses/update.json?status=" + message); 	Task<HttpResponseMessage> tweetTask = httpClient.PostAsync(uri, null); 	tweetTask.Wait(); | 
これで「Test」みたいなアルファベットだけのメッセージならTweetできたのだけど「a=b」みたいなのをTweetするとOAuthに失敗する(401が返る)。UriのコンストラクタはmessageをUrlEncodeしてくれるみたいで、こんな風に明示的にUrlEncodeする必要はないし、このように変えてもやはりOAuthに失敗する。
| 1 | 	Uri uri = new Uri("https://api.twitter.com/1.1/statuses/update.json?status=" + System.Web.HttpUtility.UrlEncode(message), true); | 
原因は、.NET FrameworkのUrlEncodeがOAuthが要求しているRFC 3986に従っていないため。何がいけないかというと、例えば「=」をRFC 3986に従ってUrl Encodeすると「%3D」とならなければいけないのだけど、.NETのは「%3d」と小文字の「d」を使ってしまうのであった。当然これでシグネチャを作ると違う結果になってOAuthに失敗するというわけだ。
というわけで自分で作ったUrlEncodeがこちら。OAuthBase.csにもUrlEncodeがあるんだけど、UTF-8に変換してないからこっちの方がよいと思う。(OAuthBase.csでは文字列は一度UrlEncodeされているという前提だから問題ない)
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 	static string UrlEncode(string src) 	{ 		byte[] data = System.Text.Encoding.UTF8.GetBytes(src); 		System.Text.StringBuilder dst = new System.Text.StringBuilder(); 		foreach (byte c in data) 		{ 			if ((0x30 <= c && c <= 0x39) // 0 - 9 				|| (0x41 <= c && c <= 0x5A) // A - Z 				|| (0x61 <= c && c <= 0x7A) // a - z 				|| c == 0x2D || c == 0x2E   // -, . 				|| c == 0x5F || c == 0x7E)  // _, ~ 			{ 				dst.Append((char)c); 			} 			else 			{ 				dst.Append('%'); 				dst.Append(String.Format("{0:X2}", (int)c)); 			} 		} 		return dst.ToString(); 	} | 
同じようにハマった人の助けになれば幸いです。

