「俺ツイッタークライアントを作ろう!」に参加してきた

去る4日に行われた第1回YMPAミーティング「俺ツイッタークライアントを作ろう!」(略称:俺ツイ)に参加させてもらった。
初回からいきなり遅刻してしまい、みなさんすみませんでした...。

タイトルの通り、それぞれ思い思いのクライアント(botでも可)を作ろうという会だった。
参加者は8名でそんなに多いわけではなかったけど、社会人、学生、職業プログラマ、趣味プログラマ、いろんなくくりの人がいらっしゃった。
開発言語や環境などはid:sakurako_s:20100705:1278329905を。
スケジュールは昼から8時間ほどのコーディングしてできあがったものを発表、というかなり大雑把な流れだった。

Webが絡むプログラミングはほとんどやったことがなかったので、他のWebサービスを連携させるのは面白そうだなーとか思いながら@tatsushiさんの発表をきいていた。
@takecofeさんのWord VBAというネタっぷりにも興味津津だった。最後はホントにWord上にTLが表示されてて面白かった。
@gunsou_911と僕は、開発環境だけでなくできあがったアプリもほとんど同じというwww 前もってDataGridView使うって分かっておきながら真似してごめんねw

  • 企画してくださった@meso_oishiさん
  • 買い出しに行って下さった@meso_oishiさん、@sakurako_sさん
  • 快適な会場を貸して下さった@azmitterさん(とその会社の方々)
  • ust中継してくださった@mikage014さん
  • 途中にぎやかにしてくれたちびっこ

に多謝!

...と、こんな感じで、全員の名前を挙げられるくらいアットホームな感じだった。楽しかった。次回は8月にあるそうなので、興味がある人は是非!http://ympa.meso.jp/

自分の進捗

今のところ、タイムラインの取得とツイートのみしかできない。(スクリーンショットはエントリの一番下)

ソースコードはこげつさんのとこ(http://d.hatena.ne.jp/nagakura_eil/20081121)からパクっ... ゲフンゲフン 参考にさせて頂いた。高階関数を多用されていて僕の頭ではすぐには理解できなかったので、戻り値としてツイートを列挙する形に改変させてもらった。こうすることでConnectorがXmlParserに依存してしまうけど...。この先不都合が出てきたら高階関数に戻す方向で。基本的にtwitterAPIC#のstaticクラスでラップしといて、GUIの皮からそいつを利用する形でやろうとしてる。
id:gunsou_911:20100705で言及されているとおり、僕もツイート時に417ステータスコードでエラーになってたけど、System.Net.ServicePointManager.Expect100Continueをfalseにしてやったらうまくいった。@gunsou_911はうまくいっていないようだけど、自分でもどこが違うのか分かってない。

僕はWebに関しては全くの門外漢で、APIというと.Net FrameworkやWin32APIやX11ライブラリなど、ある特定の言語から利用できるもの(ある言語向けの関数やらクラスやら)を想像してしまうけど、Webプログラミングではそれでは困るよなぁ。

  • WebサービスにおけるAPIとは具体的に何なのか
  • どうやって利用するのか

らへんを勉強する必要がありそうだ。

ソースコードは以下。

Connector.cs:サーバへの接続を担う静的クラス
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;

namespace TwitterClientCore
{
  /// <summary>サーバへの接続</summary>
  public class Connector
  {
    string _id = "";
    string _pass = "";
    string _clientName = "";

    public string Id
    {
      get { return _id; }
      set { _id = value; }
    }

    public string Pass
    {
      get { return _pass; }
      set { _pass = value; }
    }

    public Connector(string id, string pass, string clientName)
      : this(id, pass)
    {
      _clientName = clientName;
    }

    public Connector(string id, string pass)
    {
      ServicePointManager.Expect100Continue = false;
      _id = id;
      _pass = pass;
    }

    /// <summary>接続できるか確認する。</summary>
    /// <returns></returns>
    public bool CanConnect()
    {
      WebRequest req = CreateWebRequest("http://twitter.com/account/rate_limit_status.xml");
      req.Method = "GET";
      try
      {
        HttpWebResponse wr = (HttpWebResponse)req.GetResponse();
        HttpStatusCode ht = wr.StatusCode;
        wr.Close();
        return ht == HttpStatusCode.OK;
      }
      catch
      {
        return false;
      }
    }

    /// <summary></summary>
    /// <returns></returns>
    public IEnumerable<TweetEntry> GetPublicTimeline()
    {
      return Get("http://twitter.com/statuses/friends_timeline.xml");
    }

    /// <summary>アドレスにアクセスし、取得したデータに対して処理を行う</summary>
    /// <param name="uri">URI</param>
    /// <returns></returns>
    private IEnumerable<TweetEntry> Get(string uri)
    {
      WebRequest req = CreateWebRequest(uri);
      req.Method = "GET";

      WebResponse res = req.GetResponse();
      Stream stream = res.GetResponseStream();
      StreamReader reader = new StreamReader(stream);
      string result = reader.ReadToEnd();
      reader.Close();
      stream.Close();

      return XmlParser.ParseTimeLine(result);
    }

    /// <summary>発言する</summary>
    /// <param name="status">発言内容</param>
    /// <returns>結果</returns>
    /// <exception cref="WebException">認証のエラー</exception>
    public bool UpdateStatus(string status)
    {
      byte[] postValue = CreatePostValue(status);
      WebRequest req = CreateWebRequest("http://twitter.com/statuses/update.xml");
      req.Method = "POST";
      req.ContentLength = postValue.Length;
      using (Stream strm = req.GetRequestStream())
      {
        strm.Write(postValue, 0, postValue.Length);
      }
      HttpWebResponse result = (HttpWebResponse)req.GetResponse();
      return result.StatusCode == HttpStatusCode.OK;
    }

    /// <summary>urlからリクエストを作成する</summary>
    /// <param name="uri"></param>
    /// <returns></returns>
    private WebRequest CreateWebRequest(string uri)
    {
      WebRequest req = HttpWebRequest.Create(uri);
      req.ContentType = "application/x-www-form-urlencoded";
      req.Headers.Add(CreateAuthString());
      return req;
    }

    /// <summary>Basic認証用の文字列を作成する</summary>
    /// <returns></returns>
    private string CreateAuthString()
    {
      return "Authorization: Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(_id + ":" + _pass));
    }

    /// <summary>発言する内容を作成する</summary>
    /// <param name="status"></param>
    /// <returns></returns>
    private byte[] CreatePostValue(string status)
    {
      return Encoding.ASCII.GetBytes("source=" + Uri.EscapeUriString(_clientName) + "&status=" + Uri.EscapeUriString(status));
    }
  }
}
XMLParser.cs:取得したXMLを解釈する静的クラス
using System.IO;
using System.Xml;
using System.Collections.Generic;

namespace TwitterClientCore
{
  /// <summary>XMLのパーザ</summary>
  static class XmlParser
  {
    /// <summary>xmlを解析して、ツイートを1件ずつ列挙する</summary>
    public static IEnumerable<TweetEntry> ParseTimeLine(string s)
    {
      StringReader sr = new StringReader(s);
      XmlTextReader xtr = new XmlTextReader(sr);
      while (xtr.Read())
      {
        if (!IsStatusElement(xtr)) continue;
        TweetEntry entry = ReadStatusData(xtr);
        yield return entry;
      }
      xtr.Close();
      sr.Close();
      yield break;
    }

    /// <summary>一つの発言(&lt;status&gt;から&lt;/status&gt;まで)を解析する</summary>
    /// <param name="xtr"></param>
    /// <returns>解析結果の文字列配列</returns>
    private static TweetEntry ReadStatusData(XmlTextReader xtr)
    {
      //1つの発言を解析
      //ID = 0,
      //Name = 1,
      //ScreenName = 2,
      //PostData = 3,
      //CreateAt = 4
      //UserID = 5
      TweetEntry entry = new TweetEntry();
      while (xtr.Read())
      {
        if (xtr.NodeType == XmlNodeType.EndElement && xtr.Name == "status")
        {
          return entry;
        }
        if (xtr.NodeType != XmlNodeType.Element) { continue; }
        switch (xtr.Name)
        {
          case "created_at"://発言時間

            entry.CreatedAt = GetNextValue(xtr);
            break;
          case "id"://POST番号
            entry.ID = GetNextValue(xtr);
            break;
          case "text"://post
            entry.Text = GetNextValue(xtr);
            break;
          case "user":
            while (xtr.Read())
            {
              if (xtr.NodeType == XmlNodeType.EndElement && xtr.Name == "user")
              {
                break;
              }
              if (xtr.NodeType != XmlNodeType.Element) { continue; }
              switch (xtr.Name)
              {
                case "id"://userid
                  entry.UserID = GetNextValue(xtr);
                  break;
                case "name"://表示名
                  entry.Name = GetNextValue(xtr);
                  break;
                case "screen_name"://アカウント
                  entry.ScreenName = GetNextValue(xtr);
                  break;
              }
            }
            break;
        }
      }
      //ここにはこないはず
      return entry;
    }

    /// <summary>一つ読んで次の値を返す</summary>
    /// <param name="xtr"></param>
    /// <returns></returns>
    private static string GetNextValue(XmlTextReader xtr)
    {
      xtr.Read();
      return xtr.Value;
    }

    /// <summary>Statusエレメントか調査する</summary>
    /// <param name="xtr"></param>
    /// <returns></returns>
    private static bool IsStatusElement(XmlTextReader xtr)
    {
      return xtr.NodeType == XmlNodeType.Element && xtr.Name == "status";
    }
  }
}
TweetEntry.cs:ツイート一件分を表すクラス
namespace TwitterClientCore
{
  /// <summary>ツイート一件分</summary>
  public class TweetEntry
  {
    string _id;
    string _name;
    string _screenName;
    string _text;
    string _createdAt;
    string _userID;

    public TweetEntry()
      : this(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty)
    { }

    public TweetEntry(string id, string name, string screenName, string text, string createdAt, string userID)
    {
      _id = id;
      _name = name;
      _screenName = screenName;
      _text = text;
      _createdAt = createdAt;
      _userID = userID;
    }

    public string ID
    {
      get { return _id; }
      set { _id = value; }
    }

    public string Name
    {
      get { return _name; }
      set { _name = value; }
    }

    public string ScreenName
    {
      get { return _screenName; }
      set { _screenName = value; }
    }

    public string Text
    {
      get { return _text; }
      set { _text = value; }
    }

    public string CreatedAt
    {
      get { return _createdAt; }
      set { _createdAt = value; }
    }

    public string UserID
    {
      get { return _userID; }
      set { _userID = value; }
    }
  }
}
AppData.cs:アプリの環境設定などを保持するクラス(予定)
using System.Runtime.Serialization;
namespace TwitterClientCore
{
  /// <summary>アプリケーションの設定関係</summary>
  public class AppData : ISerializable
  {
    /// <summary>id</summary>
    string _id;

    /// <summary>パスワード</summary>
    string _pass;

    public AppData()
    {
      _id = string.Empty;
      _pass = string.Empty;
    }

    #region ISerializable関連

    /// <summary>コンストラクタ</summary>
    public AppData(SerializationInfo info, StreamingContext context)
    {
      // todo : デシリアライズのコードをここに
    }

    /// <summary>シリアライズ時に使用するメソッド</summary>
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
      // todo : シリアライズのコードをここに
    }

    #endregion

    public string ID
    {
      get { return _id; }
      set { _id = value; }
    }

    public string Password
    {
      get { return _pass; }
      set { _pass = value; }
    }

    /// <summary></summary>
    public bool CanAuth
    {
      get { return !string.IsNullOrEmpty(_id) && !string.IsNullOrEmpty(_pass); }
    }
  }
}
GUI部分の(抜粋)ソースコード
// アプリの初期化部分
AppData _data = 設定ファイルからデシリアライズ();
Connector _con = new Connector(_data.ID, _data.Password);

// タイムラインの取得
entries = _con.GetPublicTimeline();

// ツイート
_con.UpdateStatus(txtTweet.Text);