Posted by & filed under programming, windows.


ちょっと前から作り始めた POS システムの基本的な機能は、 GW 入る前に既にあらかた完成させてしまっていて、今はおまけ機能を気が向いたときに付加させている状態。

で、今回したいことは twitter への自動ポスト機能。
誰が何を買ったとか、何が入荷したとか、そんな感じなのを勝手につぶやいてくれたら良いかな―と。

以前作った twitter BOT は、環境が整ってる Python で書いたので、何の苦労もなしに出来てたけど、今回は勝手の知らない C++/CLI。

調べてみると、 C用のライブラリに liboauth、 .NET 用のライブラリに DotNetOpenAuth というものがあった。
が、使い方がいまいち分からないのでスルーw

ついでだし、 OAuth の勉強がてら実装してみることにした。

OAuth の仕組みについては、わかりやすいページがあったのでそこを読んだ。

ゼロから学ぶOAuth:第1回 OAuthとは?―OAuthの概念とOAuthでできること|gihyo.jp … 技術評論社

あと、OAuth 1.0の Document の日本語訳があったので併せて。

The OAuth 1.0 Protocol draft-hammer-oauth-10

今回作るのはBOTなので、認証は始めに1回やってしまえば良い。
実装する部分ではないので、ネットから適当にライブラリを取ってきてURLを生成。認証を済ませた。

肝心なのは、投稿部分。.NET も OAuth も初めてで分からないことだらけ。完全に手探り状態だったが、Twitter のAPI Documentを参考になんとか作った。

Authenticating Requests
POST statuses/update

TwitterStatus.h

#pragma once

#define CONSUMER_KEY "***************************"
#define CONSUMER_SECRET "****************************************************"
#define TWITTER_POST_URI "http://api.twitter.com/1/statuses/update.json"

using namespace System;
using namespace System::Net;
using namespace System::Text;
using namespace System::Web;
using namespace System::Collections;

ref class TwitterStatus
{
private:
	String^ signing_key;
	SortedList^ authArgs;
public:
	TwitterStatus(String^ token, String^ secret);
public:
	Void Post(String^ status);
};

TwitterStatus.cpp

#include "StdAfx.h"
#include "TwitterStatus.h"

TwitterStatus::TwitterStatus(String^ token, String^ secret)
{
	this->authArgs = gcnew SortedList;
	this->authArgs->Add("oauth_consumer_key", HttpUtility::UrlEncode(CONSUMER_KEY));
	this->authArgs->Add("oauth_signature_method", HttpUtility::UrlEncode("HMAC-SHA1"));
	this->authArgs->Add("oauth_token", HttpUtility::UrlEncode(token));
	this->authArgs->Add("oauth_version", HttpUtility::UrlEncode("1.0"));

	// Create composite signing key
	this->signing_key = HttpUtility::UrlEncode(CONSUMER_SECRET) + "&" + HttpUtility::UrlEncode(secret);
}

Void TwitterStatus::Post(String^ status)
{
	UTF8Encoding^ encoding = gcnew UTF8Encoding;

	// Get UNIX timestamp
	TimeSpan _TimeSpan = (DateTime::UtcNow - DateTime(1970, 1, 1, 0, 0, 0));
	this->authArgs->Add("oauth_timestamp", Convert::ToString(safe_cast<System::Int64>(_TimeSpan.TotalSeconds)));
	// Create random string
	String^ guid = Convert::ToString(System::Guid::NewGuid());
	guid = guid->Replace("-", String::Empty);
	this->authArgs->Add("oauth_nonce", HttpUtility::UrlEncode(guid));
	// Generate signature
	// Create signature base string
	StringBuilder^ signature_base_string = gcnew StringBuilder();
	signature_base_string->Append("POST&");
	signature_base_string->Append(HttpUtility::UrlEncode(TWITTER_POST_URI));
	signature_base_string->Append("&");
	for each(DictionaryEntry^ arg in this->authArgs) {
		signature_base_string->Append((String^)arg->Key);
		signature_base_string->Append("%3D");
		signature_base_string->Append((String^)arg->Value);
		signature_base_string->Append("%26");
	}
	signature_base_string->Append("status%3D");
	signature_base_string->Append(HttpUtility::UrlEncode(status, Encoding::UTF8));

	// Generage oauth_signature
	System::Security::Cryptography::HMACSHA1^ hmacsha1 = gcnew System::Security::Cryptography::HMACSHA1(encoding->GetBytes(signing_key));
	array<Byte>^ signature = hmacsha1->ComputeHash(encoding->GetBytes(signature_base_string->ToString()));
	this->authArgs->Add("oauth_signature", HttpUtility::UrlEncode(Convert::ToBase64String(signature)));

	// -- Create authorization text --
	String^ authStr = "OAuth ";
	for each(DictionaryEntry^ arg in this->authArgs) {
		authStr += (String^)arg->Key;
		authStr += "="" + (String^)arg->Value + """;
		authStr += ", ";
	}
	authStr = authStr->Substring(0, authStr->Length -2);

	// Convert status string
	array<Byte>^ bytes = encoding->GetBytes("status=" + status);

	// Setup web request
	HttpWebRequest^ request = dynamic_cast<HttpWebRequest^>(WebRequest::Create(TWITTER_POST_URI));
	request->Proxy = request->GetSystemWebProxy();
	request->Method = "POST";
	request->ServicePoint->Expect100Continue = false;

	request->Headers->Add(HttpRequestHeader::Authorization, authStr);

	request->ContentType = "application/x-www-form-urlencoded";
	request->ContentLength = bytes->Length;

	// post data
	IO::StreamWriter^ rqStream = gcnew IO::StreamWriter(request->GetRequestStream());
	rqStream->Write("status="+status);
	rqStream->Flush();
	rqStream->Close();

	this->authArgs->Remove("oauth_nonce");
	this->authArgs->Remove("oauth_timestamp");
	this->authArgs->Remove("oauth_signature");
}

できたけど、こんどはうまく動いてくれない。
見当違いなところを何度も修正したりと迷走しながら、最後の最後にパケットキャプチャを思いついてやってみると原因がすぐ判明。

{“request”:”/1/statuses/update.json”,”error”:”Incorrect signature”}

と api.twitter.com から帰ってきていた。
これで、 signature の生成をミスってるということはこれで分かったが、どこを修正したらいいのかがわからない。

今回はここまで。次回は投稿できるようにする…