2010/07/08

Twitter4J 2.1.2を改造してGoogle AppEngineからTwitPicにOAuthEchoで画像を投稿する。

Twitter4J2.1.2では(執筆時はまだ開発中)、画像の投稿機能が提供されます。現在実装されているのは、YFrogへの投稿機能(OAuthEcho, Basic認証)とTwitPicへの投稿機能(Basic認証のみ)です。
pos2witではTwitPicへのOAuthEchoによる投稿を行いたかったので、できるように改造してみました。

まず、Twitter4Jのサイトから、バージョン2.1.2をダウンロードして展開して下さい。この中に含まれる、twitter-core内のソースを改造しますので、このソースが改造できる環境を整えて下さい(僕はEclipse上にプロジェクトを作成し、ソースをコピーしました。)。環境を整えてコンパイルすると、twitter4j.internal.logging内のいくつかのソースでコンパイルエラーが起こります。参照しているクラスがないためで、ここは何も考えずにCommonsLogging, Log4J, SLF4J関連のソースを削除します(それぞれLoggerとLoggerFactoryがあるのでそれらを削除)。

次にgithubのtwitter4j.utilから、ImageUpload.javaをダウンロードし、twitter4j.util内にコピーします。このファイル内で、画像投稿機能が実装されています。
コピーするとコンパイルエラーが出るので、まずはこれを潰します。

twitter4j.http.OAuthAuthorizationに、次のメソッドを追加します(これはgit内のソースには実装されています)。

public List<HttpParameter> generateOAuthSignatureHttpParams (String method, String url) {
long timestamp = System.currentTimeMillis() / 1000;
long nonce = timestamp + RAND.nextInt();

List<HttpParameter> oauthHeaderParams = new ArrayList<HttpParameter>(5);
oauthHeaderParams.add(new HttpParameter("oauth_consumer_key", consumerKey));
oauthHeaderParams.add(OAUTH_SIGNATURE_METHOD);
oauthHeaderParams.add(new HttpParameter("oauth_timestamp", timestamp));
oauthHeaderParams.add(new HttpParameter("oauth_nonce", nonce));
oauthHeaderParams.add(new HttpParameter("oauth_version", "1.0"));
if (null != oauthToken) {
oauthHeaderParams.add(new HttpParameter("oauth_token", oauthToken.getToken()));
}

List<HttpParameter> signatureBaseParams = new ArrayList<HttpParameter> (oauthHeaderParams.size());
signatureBaseParams.addAll(oauthHeaderParams);
parseGetParameters (url, signatureBaseParams);

StringBuffer base = new StringBuffer (method).append("&")
.append(encode(constructRequestURL(url))).append("&");
base.append(encode (normalizeRequestParameters(signatureBaseParams)));

String oauthBaseString = base.toString();
String signature = generateSignature (oauthBaseString, oauthToken);

oauthHeaderParams.add (new HttpParameter("oauth_signature", signature));

return oauthHeaderParams;
}


次に、ImageUpload.java内にTwitPic用のOAuth Echo対応ImageUploadを実装します。
同ファイル内に、下記のソースをコピーします。下記ソースは、同ファイル内のYFrogOAuthUploaderをコピーしてTwitPic用に修正したものです。


// import部分
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationContext;
import twitter4j.internal.org.json.JSONException;
import twitter4j.internal.org.json.JSONObject;

// TwitPicOAuthUploader
public static class TwitPicOAuthUploader extends ImageUpload {
private String apiKey;
private OAuthAuthorization auth;

// uses the secure upload URL, not the one specified in the YFrog FAQ
private static final String TWITPIC_UPLOAD_URL = "http://api.twitpic.com/2/upload.json";
private static final String TWITTER_VERIFY_CREDENTIALS = "https://api.twitter.com/1/account/verify_credentials.json";

public TwitPicOAuthUploader(String apiKey, OAuthAuthorization auth) {
this.apiKey = apiKey;
this.auth = auth;
}

public String upload(File image) throws TwitterException{
throw new UnsupportedOperationException(); // AppEngineではFileInputStreamが使えない。
}

public String upload(String message, String fileName, InputStream fileBody) throws TwitterException {
// step 1 - generate verification URL
// String signedVerifyCredentialsURL = generateSignedVerifyCredentialsURL();

// step 2 - generate HTTP parameters
HttpParameter[] params = {
new HttpParameter("key", apiKey)
, new HttpParameter("message", message, true)
, new HttpParameter("media", fileName, fileBody)
};

// step 3 - upload the file
Configuration config = ConfigurationContext.getInstance();
config.getRequestHeaders().put("X-Auth-Service-Provider", TWITTER_VERIFY_CREDENTIALS);
StringBuilder b = new StringBuilder();
b.append("OAuth realm=\"http://api.twitter.com/\"");
for(HttpParameter p : auth.generateOAuthSignatureHttpParams("GET", TWITTER_VERIFY_CREDENTIALS)){
if(p.getName().equals("oauth_signature")){
try {
b.append(", ").append(p.getName()).append("=\"").append(
URLEncoder.encode(p.getValue(), "ISO8859_1")
).append("\"");
} catch (UnsupportedEncodingException e) {
throw new TwitterException(e);
}
} else{
b.append(", ").append(p.getName()).append("=\"").append(p.getValue()).append("\"");
}
}
config.getRequestHeaders().put("X-Verify-Credentials-Authorization", b.toString());
System.out.println("X-Auth-Service-Provider: " + TWITTER_VERIFY_CREDENTIALS);
System.out.println("X-Verify-Credentials-Authorization: " + b.toString());
HttpClientWrapper client = new HttpClientWrapper(config);
HttpResponse httpResponse = client.post(TWITPIC_UPLOAD_URL, params);

// step 4 - check the response
int statusCode = httpResponse.getStatusCode();
if (statusCode != 200) {
throw new TwitterException("Twitpic image upload returned invalid status code", httpResponse);
}

try{
String response = httpResponse.asString();
return new JSONObject(response).getString("url");
} catch(JSONException e){
throw new TwitterException("Unknown Twitpic response", httpResponse);
}
}
}


上記のソースをペーストすると、twitter4j.internal.http.HttpParameterに必要なメソッドが無いためにコンパイルエラーが発生します。下記のフィールド及びメソッドを、HttpParameterクラスに追加して下さい。


// フィールドを追加
private boolean noUrlEncode = false;
private InputStream fileBody = null;
...

// コンストラクタを追加
public HttpParameter(String name, String value, boolean noUrlEncode) {
this.name = name;
this.value = value;
this.noUrlEncode = noUrlEncode;
}

public HttpParameter(String name, String fileName, InputStream fileBody) {
this.name = name;
this.file = new File(fileName);
this.fileBody = fileBody;
}
...

// メソッドを追加
public boolean isUrlEncodeUnnecessary(){
return this.urlEncodeUnnecessary;
}

public boolean hasFileBody(){
return null != fileBody;
}

public InputStream getFileBody(){
return fileBody;
}


これでコンパイルエラー自体は消えますが、追加されたパラメータに対応してリクエスト作成機能を修正する必要があります。リクエスト作成機能はtwitter4j.internal.http.HttpClientImpl.java内にあるので、下記のように改造します(赤字が改造部分)。

                                // 241行目あたり
if(param.isFile()){
write(out, boundary + "\r\n");
write(out, "Content-Disposition: form-data; name=\"" + param.getName() + "\"; filename=\"" + param.getFile().getName() + "\"\r\n");
write(out, "Content-Type: " + param.getContentType() + "\r\n\r\n");
BufferedInputStream in = new BufferedInputStream(
param.hasFileBody() ? param.getFileBody() : new FileInputStream(param.getFile())
);
int buff = 0;
...

// 258行目あたり
logger.debug(param.getValue());
if(param.isUrlEncodeUnnecessary()){
out.write(param.getValue().getBytes("UTF-8"));
} else{
out.write(encode(param.getValue()).getBytes("UTF-8"));
}

write(out, "\r\n");
...


この修正が必要なのは、
  • オリジナルのコードではFileInputStreamを使っているが、これはAppEngineでは使えない。
  • オリジナルのコードでは常にパラメータをURLEncodeしているが、メッセージをURLEncodeするとTwitPicで復元されない(URLEncodeされた状態で記録される)

という問題に対処するためです。

これでTwitPicに画像を投稿できるようになります。サンプルコードはこんな感じ。投稿にはTwitPicのAPIKEYが必要なので、ここからアプリケーションを登録し、APIKEYを入手して下さい。
 public void test() throws Exception{
String apiKey = TwitPicのAPI KEY;
Twitter t = new TwitterFactory().getOAuthAuthorizedInstance(new AccessToken(
Twitterユーザのaccess_token
, Twitterユーザのaccess_token_secret
));
OAuthAuthorization auth = (OAuthAuthorization)t.getAuthorization();
String message = "投稿するメッセージ。";
String url = new ImageUpload.TwitpicOAuthUploader(apiKey, auth).upload(
message, "duke.jpg", getClass().getResourceAsStream("duke-on-gae.jpg")
);
// twitterには、別途投稿する必要がある(uploadでは写真がTwitPicに投稿されるのみ)。
t.updateStatus(message + " " + url);
}


だいぶ汚い改造になってしまいましたが、とりあえず投稿することはできるようになりました。もう少し整理して、Twitter4Jにうまく取り込んでもらえるよう働きかける予定です。

2 件のコメント:

twj さんのコメント...

Twitter4Jのハック、お疲れ様です。
pull request、楽しみにしていますね!

Takao Nakaguchi さんのコメント...

コメントありがとうございます!
調べてみると、影響する範囲が大きく、うまく取り込めるかわかりませんが、とりあえずTwitter4J Jに投稿しましたので、後はそちらで議論させていただければ。