Twitter to Google Talk bridge

TwitterGmailのWebインターフェイス上のGoogle Talkで読み書きできるといいなあと思い,調べてみたところ,Twitterには昔はJabber(XMPP)インターフェイスがあったが,ある時点からなくなったらしい.ちょうどいいので,App Engineで作ってみた.

設計

Twitter からGoogle Talkへのブリッジには,cronを用いる.定期的に起動するサーブレットが,Twitterのfriends time lineを取得し,更新があれば,Google Talkxmppへ送信する.この際,前回送信した最後のメッセージIDを保存しておく必要がある.

Google TalkからTwitterへのブリッジは,XMPPハンドラで,送信元のXMPPアドレスから,フォワードするべきTwitterアカウントを判断し,そのアカウントにフォワードする.

データ構造

ユーザごとに,XMPPのアドレスと,Twitterのアカウント名,パスワード,最後に読み出したメッセージのIDを保持する必要がある.クラス定義は下記のようになる.

import javax.jdo.annotations.*;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class TwitterUser {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Long id;
  @Persistent private String xmppAddr;
  @Persistent private String username;
  @Persistent private String password;
  @Persistent private long   lastId;
    
  public TwitterUser(String xmppAddr, String username, String password,
      long lastId) {
    super();
    this.xmppAddr = xmppAddr;
    this.username = username;
    this.password = password;
    this.lastId = lastId;
  }
  // ... getters / setters 省略
}

TwitterからGoogle Talk

cronから起動されるサーブレット

  • データストアからTwitterUser Kindのオブジェクトをすべて取り出し,それぞれに対して,
  • friends timelineの更新を取得し,
  • 個々のメッセージをXMPPで送信する.

コードはこんな感じ.friends timeline は最大でも1ページ分しか取得していない.デフォルトでは1ページは20メッセージなので,このロジックでは,人によっては取りこぼすこともあるだろう.

@SuppressWarnings("serial")
public class TwitterToXMPPServlet extends HttpServlet {
  @Override
  public void doGet(HttpServletRequest req, 
      HttpServletResponse res)
      throws IOException {
    
    PersistenceManager pm = PMF.get().getPersistenceManager();
    Query query = pm.newQuery(TwitterUser.class); 
    List<TwitterUser> users = (List<TwitterUser>) query.execute();

    for (TwitterUser user: users) {
      String to =   user.getXmppAddr();
      String username = user.getUsername();
      String password = user.getPassword();
      long   lastId = user.getLastId();
      JID jid = new JID(to);
      
      Twitter twitter = new Twitter(username, password);
      List<Status> statuses = null;
      try {
         statuses = twitter.getFriendsTimeline(new Paging(1).sinceId(lastId)); 
      } catch (TwitterException e) {
        e.printStackTrace();
        System.err.println("failed to get status for user: " + username);
        continue;        
      }
      
      Collections.reverse(statuses);
      for (Status status: statuses){
        String mes = status.getUser().getName() + ":" +  status.getText();
        if (! sendXMPPMessage(jid, mes, System.err))
          break;
        lastId = status.getId();
      }
      user.setLastId(lastId); // update lastId
      try {
        pm.makePersistent(user);
      } finally {
        pm.close();
      }
    }
  }
...
}

送信するメソッドはこんな感じ.送信に失敗すると,falseを返す.falseが返った場合には,次回cronで起動された際に再送信が試みられる.

  private boolean sendXMPPMessage(JID jid, String msgBody, PrintStream writer) {
    boolean messageSent = false;
    Message msg = new MessageBuilder()
    .withRecipientJids(jid)
    .withBody(msgBody)
    .build();

    XMPPService xmpp = XMPPServiceFactory.getXMPPService();
    if (xmpp.getPresence(jid).isAvailable()) {
        SendResponse status = xmpp.sendMessage(msg);
      messageSent = (status.getStatusMap().get(jid) == 
        SendResponse.Status.SUCCESS);
      if (messageSent) {
        writer.println("<h2>Message Sent!</h2>");
        return true;
      } else{
        writer.println("<h2>Failed!</h2>");           
        return false;
      }
    } else {
      writer.println("<h2>the address " + jid.getId() + 
                                       " is not available</h2>");
      return false;
    }    
  }

cronをセットするには,war/WEB-INF/cron.xmlを書けばよい.

<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
  <cron>
    <url>/t2x</url>
    <schedule>every 5 minutes</schedule>
  </cron>
</cronentries>

/t2x に先のサーブレットを登録している.

Google TalkからTwitter

XMPPの受信サーブレットで次のことを行う.

  • 送信者のXMPPアドレスから,TwitterUserオブジェクトを検索
  • TwitterUser に送信

split("/")しているのは,Jid.getId()で返されるアドレスの後部に余分な文字(?)が付いているので.

public class XMPPToTwitterServlet extends HttpServlet {
  public void doPost(HttpServletRequest req, 
      HttpServletResponse res)
     throws IOException {
    XMPPService xmpp = XMPPServiceFactory.getXMPPService();
    Message message = xmpp.parseMessage(req);

    String xmppAddr = 
      message.getFromJid().getId().split("/")[0].toLowerCase();

    PersistenceManager pm = PMF.get().getPersistenceManager();
    Query query = pm.newQuery(TwitterUser.class); 
    query.setFilter("xmppAddr == ADDR");
    query.declareParameters("java.lang.String ADDR");
    List<TwitterUser> users = (List<TwitterUser>) query.execute(xmppAddr);

    if (users.isEmpty()) {
      System.err.println("no twitter user found for " + xmppAddr);      
      System.err.println("body: "+ message.getBody());
      return;
    }
    TwitterUser user = users.get(0);
    Twitter twitter = new Twitter(user.getUsername(), user.getPassword());
    try {
      twitter.updateStatus(message.getBody());
    } catch (TwitterException e) {
      e.printStackTrace();
    }
  }
}

もちろん,このサーブレットをweb.xmlで,/_ah/xmpp/message/chat/にマップする必要がある.

<servlet>
  <servlet-name>xmpp2twitter</servlet-name>
  <servlet-class>XMPPToTwitterServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>xmpp2twitter</servlet-name>
  <url-pattern>/_ah/xmpp/message/chat/</url-pattern>
</servlet-mapping>

これで,Google Talkで書くだけで,Twitterにポストすることができる.

問題点

実は,今のところ,Google TalkからTwitterに送信するほうは,マルチバイトコードで動かない.これはApp EngineのXMPP受信機能の既知のバグで1.2.6で直る,らしい.まあ,まだ新しい機能なので仕方がないか.

所感

XMPPとApp Engineの組み合わせは強力で,いろいろと使い道がありそうだ.それだけに,このバグは残念.早く直らないかな...

追記 (10/14)

1.2.6でマルチバイトが通るようになった.すばらしや.