TwitterのAPI がv1.1になるそうで

Twitterで定期的に投稿するのも面倒なんで、こちらで済ませてしまおうかなとかなんとか。

Twitterクライアント&サービス API1.1対応状況リスト

これを張っておきたかっただけです。

なんでこんなリストを用意したのか、という背景をちょっと書いておきます。
TwitterAPIについてのお話なんですが、開発関係者だけでなく、一般ユーザーにも影響のあるお話なので軽く目を通して頂ければ幸い。

これまでのおさらい

Twitterが提供しているAPIにはバージョンがあります。これは、TwitterがとあるAPIの機能・呼び出し方・応答内容などを変更してしまうと、その瞬間からそのAPIを使っているサービスやクライアント(以下クライアントでまとめます)がモロに影響を受けて正しく動作しなくなってしまうためです。そこで、新しいバージョンのAPIを設けて、各クライアントが新しいAPIに対応するまで新旧両方のAPIを提供することで時間に余裕をもたせていたわけです。というのは建前で、これまではバージョン変えずに応答内容の変更とかよくあったわけですが、まぁ、それは置いといて。
で、現在APIには2つのバージョンがあります。1.0と1.1です。数年間APIを提供していたわりにバージョンが少ないのは、初期のAPIはバージョニングされておらず、現在それらのAPIは無くなっているためです。

現在

現在使えるAPIは1.0と1.1の2つのバージョンです。つまり、今がまさに移行のための猶予期間なわけです。昨年の10月頃だったか、1.1がお目見えし、Twitterから開発者へ「さっさと移行しろ」とアナウンスが出されました。期限付きで。
旧来の1.0系APIは2013年3月5日を持って使えなくなります。公開から約半年の猶予期間ですね。

追記 2013/06/12 1.0系APIの停止は当初予定日から延期されていましたが、本日2013/06/12未明に停止されました。

今後

既に残り2ヶ月を切っており、各クライアントは対応作業を始めています。ただ、すべてのクライアントが対応するわけではなく、これを節目と開発をやめるクライアントも出てきています。すなわち1.1に対応せず、3月5日を以って使えなくなるクライアントもあるわけです。
現在は様々なTwitterクライアントが使われているわけですが、「自分の使っているクライアントは今後も使い続けられるのか?使い続けられないなら他にどんなクライアントが選択肢としてあるのか?」ってのが分かると便利だろうと思い、件のリストを作ったわけです。作ったといっても、私はひな形しか用意してないんですけどね。
クライアントはTwitter公式のものもありますし、Webも使えるので最終的にはどうとでもなるでしょう。Twitter自体がなくなるわけじゃないので。

件のリスト、誰でも編集可能なので、ご自分の使っているサービスやクライアントが載っていなければ追記してみて下さい。また、対応状況や開発状況など情報が有りましたら更新していただければ、みんなが幸せになれるかな、と思います。

ここまで書いてきてなんですが、結構多くのクライアントが1.1対応予定あるみたいで一安心です。

Twitterは過剰なブームを二山ぐらい超えて、アレヤコレヤと問題も出てきたりして落ち着いたんだか下火になったんだか微妙なところに差し掛かっているなーと思う今日このごろ。このまま消えるのか一線を保って定着するのか分かりませんが、乗りかかった船なんでボチボチ追従していくつもりです。

でわでわ

InstallShield 2011 Limited Edition

.NET FrameworkベースのWindows フォーム アプリケーションのインストーラーを作った時の話。


まずは環境から。


OS: Windows7 Pro (32bit)
IDE: Visual Studio 2010 Pro
フレームワーク: .NET Framework 4.0 Full
開発言語: Visual Basic
インストーラー: InstallShield 2011 Limited Edition

InstallShieldのLimited Editionは、VS2010のPro以上なら無料で使えるので助かります。今回初めて使ったのですが、パッと見分かりやすくてホホホィっとインストーラーが作れちゃいます。


http://www.atmarkit.co.jp/fdotnet/chushin/introwinform_12/introwinform_12_02.html


が、普通にアシスタントに沿って進めてコンパイルすると、こんな警告が出ます。

  • 6245: One or more of the project's components contain .NET properties that require the .NET Framework. It is recommended that the release include the .NET Framework.

.NET Frameworkに依存しているファイルをインストールするから、セットアップにも.NET Framework含めるのオススメ」って事なんだけど、警告だけなので無視しても問題はない。けど、今回はまさに、これをするためにインストーラーに手を出したわけでして。

で、この警告をダブルクリックして出てくる解決手順を試そうとしたけど、ドキュメントが古いのか手順中の該当項目が存在しない。Webでアレコレ検索してもヒットしない。もしかして、Limited Editionじゃ.NET Frameworkを含めることが出来ないのかしら、などと想像は膨らむ一方。


で、セットアッププロジェクトを隅から隅まで舐め回したところ、ようやく見つけました。ソリューションエクスプローラーで、


2 Specify Application Data
└Redistributables


です。
分かりにくい。アシスタント内に含めるかどうか質問してくれても良さそうだけど。


これで該当項目にチェックを付ければ解決、かと思いきや、また別の問題が。選択した再頒布パッケージがない場合は、自動でダウンロードしてくれる親切設計なのですが、そのダウンロードが出来ないと言われてしまう。「Administratorでやってるかい?」と書かれてたので、Adminでやってみたり、通信設定見直したりしたけどダメ。ググってみたところ、ダウンロード先のフォルダに書き込み権限が無いのが原因とのこと。良く見たらProgram Files下だし。なぜUAC出さないし・・・あまりお行儀は良くないけど、一般ユーザーに書き込み権限与えたところ、正常にダウンロードしてくれました。


C:\Program Files\InstallShield\2011LE\SetupPrerequisites


念のため、ISLEのオプションで指定されている2つのフォルダにも同じ対処をしておきました。私の環境では、これらのフォルダ。


C:\Program Files\InstallShield\2011LE\Objects
C:\Program Files\Common Files\Merge Modules


余談:オプションでPrerequisitesフォルダを変更できるけど、元のフォルダのファイルを全部移動する必要があるっぽい?(未確認)

めでたしめでたし。


2012/06/07追記
どうにも取り回しが悪いので、現在はVSのセットアッププロジェクトを使うことで落ち着いています。私の用途ではこちらで十分でした。

OAuth(xAuth)のVB.net実装サンプル

OAuthのVB.netサンプルです。お好きにどうぞ。
PIN入力用のメソッドもついていますが、こちらはおまけということで。
オリジナルはこちら

はじめは備忘録として言葉でゴチャっと書いて済まそうと思ってたんですが、見てみたい、という意見を見かけたのでアップしてみた次第。おかげで見落としてた依存関係とか消せてすっきりしました。

使い方サンプル(xAuth)

初期化〜認証まで
'HttpConnectionの初期化(HttpConnection.InitializeConnection呼び出し)
'プロキシ設定と通信タイムアウトの設定です。はじめに一回だけ呼べばOK。
HttpConnection.InitializeConnection(20, ProxyType.IE, "", 0, 0, "", "")

'HttpConnectionOAuthのインスタンス化
Dim httpCon as New HttpConnectionOAuth

'初期化(コンシューマーキーの設定など)
httpCon.Initialize("Your consumerKey", "Your consumerSecret", "", "", "screen_name") 'screen_nameはtwitter用

'認証
Dim result As Boolean = httpCon.AuthenticateXAuth("https://api.twitter.com/oauth/access_token", "Your username", "Your password")
If result Then
    MessageBox.Show("認証成功")
Else
    MessageBox.Show("認証失敗")
End If
通信

取得はこんな感じ。

Dim param As New Dictionary(Of String, String)
param.Add("count", 20)

Dim content As String = ""
Dim statusCode As HttpStatusCode = _
    httpCon.GetContent("GET", _
                            New Uri("http://api.twitter.com/1/statuses/home_timeline.xml"), _
                            param, _
                            content, _
                            Nothing)

更新も同じ。

Dim param As New Dictionary(Of String, String)
param.Add("status", "更新しちゃうよ!")

Dim content As String = ""
Dim statusCode As HttpStatusCode = _
    httpCon.GetContent("POST", _
                            New Uri("http://api.twitter.com/1/statuses/update.xml"), _
                            param, _
                            content, _
                            Nothing)

ソース

HTTP通信の大元。ストリームとか画像取得とかのメソッドは省略してあります。

Imports System.Net
Imports System.IO
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.Text

'''<summary>
'''HttpWebRequest,HttpWebResponseを使用した基本的な通信機能を提供する
'''</summary>
'''<remarks>
'''プロキシ情報などを設定するため、使用前に静的メソッドInitializeConnectionを呼び出すこと。
'''通信方式によって必要になるHTTPヘッダの付加などは、派生クラスで行う。
'''</remarks>
Public Class HttpConnection
    '''<summary>
    '''プロキシ
    '''</summary>
    Private Shared proxy As WebProxy = Nothing

    '''<summary>
    '''ユーザーが選択したプロキシの方式
    '''</summary>
    Private Shared proxyKind As ProxyType = proxyType.IE

    '''<summary>
    '''クッキー保存用コンテナ
    '''</summary>
    Private Shared cookieContainer As New CookieContainer

    '''<summary>
    '''初期化済みフラグ
    '''</summary>
    Private Shared isInitialize As Boolean = False

    Public Enum ProxyType
        None
        IE
        Specified
    End Enum

    '''<summary>
    '''HttpWebRequestオブジェクトを取得する。パラメータはGET/HEAD/DELETEではクエリに、POST/PUTではエンティティボディに変換される。
    '''</summary>
    '''<remarks>
    '''追加で必要となるHTTPヘッダや通信オプションは呼び出し元で付加すること
    '''(Timeout,AutomaticDecompression,AllowAutoRedirect,UserAgent,ContentType,Accept,HttpRequestHeader.Authorization,カスタムヘッダ)
    '''POST/PUTでクエリが必要な場合は、requestUriに含めること。
    '''</remarks>
    '''<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
    '''<param name="requestUri">通信先URI</param>
    '''<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
    '''<param name="withCookie">通信にcookieを使用するか</param>
    '''<returns>引数で指定された内容を反映したHttpWebRequestオブジェクト</returns>
    Protected Function CreateRequest(ByVal method As String, _
                                            ByVal requestUri As Uri, _
                                            ByVal param As Dictionary(Of String, String), _
                                            ByVal withCookie As Boolean _
                                        ) As HttpWebRequest
        If Not isInitialize Then Throw New Exception("Sequence error.(not initialized)")

        'GETメソッドの場合はクエリとurlを結合
        Dim ub As New UriBuilder(requestUri.AbsoluteUri)
        If method = "GET" OrElse method = "DELETE" OrElse method = "HEAD" Then
            ub.Query = CreateQueryString(param)
        End If

        Dim webReq As HttpWebRequest = DirectCast(WebRequest.Create(ub.Uri), HttpWebRequest)

        'プロキシ設定
        If proxyKind <> ProxyType.IE Then webReq.Proxy = proxy

        webReq.Method = method
        If method = "POST" OrElse method = "PUT" Then
            webReq.ContentType = "application/x-www-form-urlencoded"
            'POST/PUTメソッドの場合は、ボディデータとしてクエリ構成して書き込み
            Using writer As New StreamWriter(webReq.GetRequestStream)
                writer.Write(CreateQueryString(param))
            End Using
        End If
        'cookie設定
        If withCookie Then webReq.CookieContainer = cookieContainer
        'タイムアウト設定
        webReq.Timeout = DefaultTimeout

        Return webReq
    End Function

    '''<summary>
    '''HTTPの応答を処理し、応答ボディデータをテキストとして返却する
    '''</summary>
    '''<remarks>
    '''リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
    '''WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
    '''テキストの文字コードはUTF-8を前提として、エンコードはしていません
    '''</remarks>
    '''<param name="webRequest">HTTP通信リクエストオブジェクト</param>
    '''<param name="contentText">[OUT]HTTP応答のボディデータ</param>
    '''<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
    '''<param name="withCookie">通信にcookieを使用する</param>
    '''<returns>HTTP応答のステータスコード</returns>
    Protected Function GetResponse(ByVal webRequest As HttpWebRequest, _
                                        ByRef contentText As String, _
                                        ByVal headerInfo As Dictionary(Of String, String), _
                                        ByVal withCookie As Boolean _
                                    ) As HttpStatusCode
        Try
            Using webRes As HttpWebResponse = CType(webRequest.GetResponse(), HttpWebResponse)
                Dim statusCode As HttpStatusCode = webRes.StatusCode
                'cookie保持
                If withCookie Then SaveCookie(webRes.Cookies)
                'リダイレクト応答の場合は、リダイレクト先を設定
                GetHeaderInfo(webRes, headerInfo)
                '応答のストリームをテキストに書き出し
                If contentText Is Nothing Then Throw New ArgumentNullException("contentText")
                If webRes.ContentLength > 0 Then
                    Using sr As StreamReader = New StreamReader(webRes.GetResponseStream)
                        contentText = sr.ReadToEnd()
                    End Using
                End If
                Return statusCode
            End Using
        Catch ex As WebException
            If ex.Status = WebExceptionStatus.ProtocolError Then
                Dim res As HttpWebResponse = DirectCast(ex.Response, HttpWebResponse)
                Return res.StatusCode
            End If
            Throw ex
        End Try
    End Function

    '''<summary>
    '''HTTPの応答を処理します。応答ボディデータが不要な用途向け。
    '''</summary>
    '''<remarks>
    '''リダイレクト応答の場合(AllowAutoRedirect=Falseの場合のみ)は、headerInfoインスタンスがあればLocationを追加してリダイレクト先を返却
    '''WebExceptionはハンドルしていないので、呼び出し元でキャッチすること
    '''</remarks>
    '''<param name="webRequest">HTTP通信リクエストオブジェクト</param>
    '''<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。ヘッダ名をキーにして空データのコレクションを渡すことで、該当ヘッダの値をデータに設定して戻す</param>
    '''<param name="withCookie">通信にcookieを使用する</param>
    '''<returns>HTTP応答のステータスコード</returns>
    Protected Function GetResponse(ByVal webRequest As HttpWebRequest, _
                                        ByVal headerInfo As Dictionary(Of String, String), _
                                        ByVal withCookie As Boolean _
                                    ) As HttpStatusCode
        Try
            Using webRes As HttpWebResponse = CType(webRequest.GetResponse(), HttpWebResponse)
                Dim statusCode As HttpStatusCode = webRes.StatusCode
                'cookie保持
                If withCookie Then SaveCookie(webRes.Cookies)
                'リダイレクト応答の場合は、リダイレクト先を設定
                GetHeaderInfo(webRes, headerInfo)
                Return statusCode
            End Using
        Catch ex As WebException
            If ex.Status = WebExceptionStatus.ProtocolError Then
                Dim res As HttpWebResponse = DirectCast(ex.Response, HttpWebResponse)
                Return res.StatusCode
            End If
            Throw ex
        End Try
    End Function

    '''<summary>
    '''クッキーを保存。ホスト名なしのドメインの場合、ドメイン名から先頭のドットを除去して追加しないと再利用されないため
    '''</summary>
    Private Sub SaveCookie(ByVal cookieCollection As CookieCollection)
        For Each ck As Cookie In cookieCollection
            If ck.Domain.StartsWith(".") Then
                ck.Domain = ck.Domain.Substring(1, ck.Domain.Length - 1)
                cookieContainer.Add(ck)
            End If
        Next
    End Sub

    '''<summary>
    '''headerInfoのキー情報で指定されたHTTPヘッダ情報を取得・格納する。redirect応答時はLocationヘッダの内容を追記する
    '''</summary>
    '''<param name="webResponse">HTTP応答</param>
    '''<param name="headerInfo">[IN/OUT]キーにヘッダ名を指定したデータ空のコレクション。取得した値をデータにセットして戻す</param>
    Private Sub GetHeaderInfo(ByVal webResponse As HttpWebResponse, _
                                    ByVal headerInfo As Dictionary(Of String, String))

        If headerInfo Is Nothing Then Exit Sub

        If headerInfo.Count > 0 Then
            Dim keys(headerInfo.Count - 1) As String
            headerInfo.Keys.CopyTo(keys, 0)
            For Each key As String In keys
                If Array.IndexOf(webResponse.Headers.AllKeys, key) > -1 Then
                    headerInfo.Item(key) = webResponse.Headers.Item(key)
                Else
                    headerInfo.Item(key) = ""
                End If
            Next
        End If

        Dim statusCode As HttpStatusCode = webResponse.StatusCode
        If statusCode = HttpStatusCode.MovedPermanently OrElse _
           statusCode = HttpStatusCode.Found OrElse _
           statusCode = HttpStatusCode.SeeOther OrElse _
           statusCode = HttpStatusCode.TemporaryRedirect Then
            If headerInfo.ContainsKey("Location") Then
                headerInfo.Item("Location") = webResponse.Headers.Item("Location")
            Else
                headerInfo.Add("Location", webResponse.Headers.Item("Location"))
            End If
        End If
    End Sub

    '''<summary>
    '''クエリコレクションをkey=value形式の文字列に構成して戻す
    '''</summary>
    '''<param name="param">クエリ、またはポストデータとなるkey-valueコレクション</param>
    Protected Function CreateQueryString(ByVal param As IDictionary(Of String, String)) As String
        If param Is Nothing OrElse param.Count = 0 Then Return String.Empty

        Dim query As New StringBuilder
        For Each key As String In param.Keys
            query.AppendFormat("{0}={1}&", UrlEncode(key), UrlEncode(param(key)))
        Next
        Return query.ToString(0, query.Length - 1)
    End Function

    '''<summary>
    '''クエリ形式(key1=value1&key2=value2&...)の文字列をkey-valueコレクションに詰め直し
    '''</summary>
    '''<param name="queryString">クエリ文字列</param>
    '''<returns>key-valueのコレクション</returns>
    Protected Function ParseQueryString(ByVal queryString As String) As NameValueCollection
        Dim query As New NameValueCollection
        Dim parts() As String = queryString.Split("&"c)
        For Each part As String In parts
            Dim index As Integer = part.IndexOf("="c)
            If index = -1 Then
                query.Add(Uri.UnescapeDataString(part), "")
            Else
                query.Add(Uri.UnescapeDataString(part.Substring(0, index)), Uri.UnescapeDataString(part.Substring(index + 1)))
            End If
        Next
        Return query
    End Function

    '''<summary>
    '''2バイト文字も考慮したUrlエンコード
    '''</summary>
    '''<param name="str">エンコードする文字列</param>
    '''<returns>エンコード結果文字列</returns>
    Protected Function UrlEncode(ByVal stringToEncode As String) As String
        Const UnreservedChars As String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"
        Dim sb As New StringBuilder
        Dim bytes As Byte() = Encoding.UTF8.GetBytes(stringToEncode)

        For Each b As Byte In bytes
            If UnreservedChars.IndexOf(Chr(b)) <> -1 Then
                sb.Append(Chr(b))
            Else
                sb.AppendFormat("%{0:X2}", b)
            End If
        Next
        Return sb.ToString()
    End Function

#Region "DefaultTimeout"
    '''<summary>
    '''通信タイムアウト時間(ms)
    '''</summary>
    Private Shared timeout As Integer = 20000

    '''<summary>
    '''通信タイムアウト時間(ms)。10〜120秒の範囲で指定。範囲外は20秒とする
    '''</summary>
    Protected Shared Property DefaultTimeout() As Integer
        Get
            Return timeout
        End Get
        Set(ByVal value As Integer)
            Const TimeoutMinValue As Integer = 10000
            Const TimeoutMaxValue As Integer = 120000
            Const TimeoutDefaultValue As Integer = 20000
            If value < TimeoutMinValue OrElse value > TimeoutMaxValue Then
                ' 範囲外ならデフォルト値設定
                timeout = TimeoutDefaultValue
            Else
                timeout = value
            End If
        End Set
    End Property
#End Region

    '''<summary>
    '''通信クラスの初期化処理。タイムアウト値とプロキシを設定する
    '''</summary>
    '''<remarks>
    '''通信開始前に最低一度呼び出すこと
    '''</remarks>
    '''<param name="timeout">タイムアウト値(秒)</param>
    '''<param name="proxyType">なし・指定・IEデフォルト</param>
    '''<param name="proxyAddress">プロキシのホスト名orIPアドレス</param>
    '''<param name="proxyPort">プロキシのポート番号</param>
    '''<param name="proxyUser">プロキシ認証が必要な場合のユーザ名。不要なら空文字</param>
    '''<param name="proxyPassword">プロキシ認証が必要な場合のパスワード。不要なら空文字</param>
    Public Shared Sub InitializeConnection( _
            ByVal timeout As Integer, _
            ByVal proxyType As ProxyType, _
            ByVal proxyAddress As String, _
            ByVal proxyPort As Integer, _
            ByVal proxyUser As String, _
            ByVal proxyPassword As String)
        isInitialize = True
        ServicePointManager.Expect100Continue = False
        DefaultTimeout = timeout * 1000     's -> ms
        Select Case proxyType
            Case proxyType.None
                proxy = Nothing
            Case proxyType.Specified
                proxy = New WebProxy("http://" + proxyAddress + ":" + proxyPort.ToString)
                If Not String.IsNullOrEmpty(proxyUser) OrElse Not String.IsNullOrEmpty(proxyPassword) Then
                    proxy.Credentials = New NetworkCredential(proxyUser, proxyPassword)
                End If
            Case proxyType.IE
                'IE設定(システム設定)はデフォルト値なので処理しない
        End Select
        proxyType = proxyType
    End Sub

End Class

それを使ったOAuth。

Imports System.Net
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.IO
Imports System.Text
Imports System.Security

'''<summary>
'''OAuth認証を使用するHTTP通信。HMAC-SHA1固定
'''</summary>
'''<remarks>
'''使用前に認証情報を設定する。認証確認を伴う場合はAuthenticate系のメソッドを、認証不要な場合はInitializeを呼ぶこと。
'''</remarks>
Public Class HttpConnectionOAuth
    Inherits HttpConnection
    Implements IHttpConnection

    '''<summary>
    '''OAuth署名のoauth_timestamp算出用基準日付(1970/1/1 00:00:00)
    '''</summary>
    Private Shared ReadOnly UnixEpoch As New DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified)

    '''<summary>
    '''OAuth署名のoauth_nonce算出用乱数クラス
    '''</summary>
    Private Shared ReadOnly NonceRandom As New Random

    '''<summary>
    '''OAuthのアクセストークン。永続化可能(ユーザー取り消しの可能性はある)。
    '''</summary>
    Private token As String = ""

    '''<summary>
    '''OAuthの署名作成用秘密アクセストークン。永続化可能(ユーザー取り消しの可能性はある)。
    '''</summary>
    Private tokenSecret As String = ""

    '''<summary>
    '''OAuthのコンシューマー鍵
    '''</summary>
    Private consumerKey As String

    '''<summary>
    '''OAuthの署名作成用秘密コンシューマーデータ
    '''</summary>
    Private consumerSecret As String

    '''<summary>
    '''認証成功時の応答でユーザー情報を取得する場合のキー。設定しない場合は、AuthUsernameもブランクのままとなる
    '''</summary>
    Private userIdentKey As String

    '''<summary>
    '''OAuthの署名作成用秘密コンシューマーデータ
    '''</summary>
    Private authorizedUsername As String

    '''<summary>
    '''OAuth認証で指定のURLとHTTP通信を行い、結果を返す
    '''</summary>
    '''<param name="method">HTTP通信メソッド(GET/HEAD/POST/PUT/DELETE)</param>
    '''<param name="requestUri">通信先URI</param>
    '''<param name="param">GET時のクエリ、またはPOST時のエンティティボディ</param>
    '''<param name="content">[OUT]HTTP応答のボディデータ</param>
    '''<param name="headerInfo">[IN/OUT]HTTP応答のヘッダ情報。必要なヘッダ名を事前に設定しておくこと</param>
    '''<returns>HTTP応答のステータスコード</returns>
    Public Function GetContent(ByVal method As String, _
            ByVal requestUri As Uri, _
            ByVal param As Dictionary(Of String, String), _
            ByRef content As String, _
            ByVal headerInfo As Dictionary(Of String, String)) As HttpStatusCode Implements IHttpConnection.GetContent
        '認証済かチェック
        If String.IsNullOrEmpty(token) Then Throw New Exception("Sequence error. (Token is blank.)")

        Dim webReq As HttpWebRequest = CreateRequest(method, _
                                                    requestUri, _
                                                    param, _
                                                    False)
        'OAuth認証ヘッダを付加
        AppendOAuthInfo(webReq, param, token, tokenSecret)

        If content Is Nothing Then
            Return GetResponse(webReq, headerInfo, False)
        Else
            Return GetResponse(webReq, content, headerInfo, False)
        End If
    End Function

#Region "認証処理"
    '''<summary>
    '''OAuth認証の開始要求(リクエストトークン取得)。PIN入力用の前段
    '''</summary>
    '''<remarks>
    '''呼び出し元では戻されたurlをブラウザで開き、認証完了後PIN入力を受け付けて、リクエストトークンと共にAuthenticatePinFlowを呼び出す
    '''</remarks>
    '''<param name="requestTokenUrl">リクエストトークンの取得先URL</param>
    '''<param name="requestUri">ブラウザで開く認証用URLのベース</param>
    '''<param name="requestToken">[OUT]認証要求で戻されるリクエストトークン。使い捨て</param>
    '''<param name="authUri">[OUT]requestUriを元に生成された認証用URL。通常はリクエストトークンをクエリとして付加したUri</param>
    '''<returns>取得結果真偽値</returns>
    Public Function AuthenticatePinFlowRequest(ByVal requestTokenUrl As String, _
                                        ByVal authorizeUrl As String, _
                                        ByRef requestToken As String, _
                                        ByRef authUri As Uri) As Boolean
        'PIN-based flow
        authUri = GetAuthenticatePageUri(requestTokenUrl, authorizeUrl, requestToken)
        If authUri Is Nothing Then Return False
        Return True
    End Function

    '''<summary>
    '''OAuth認証のアクセストークン取得。PIN入力用の後段
    '''</summary>
    '''<remarks>
    '''事前にAuthenticatePinFlowRequestを呼んで、ブラウザで認証後に表示されるPINを入力してもらい、その値とともに呼び出すこと
    '''</remarks>
    '''<param name="accessTokenUrl">アクセストークンの取得先URL</param>
    '''<param name="requestUri">AuthenticatePinFlowRequestで取得したリクエストトークン</param>
    '''<param name="pinCode">Webで認証後に表示されるPINコード</param>
    '''<returns>取得結果真偽値</returns>
    Public Function AuthenticatePinFlow(ByVal accessTokenUrl As String, _
                                        ByVal requestToken As String, _
                                        ByVal pinCode As String) As Boolean
        'PIN-based flow
        If String.IsNullOrEmpty(requestToken) Then Throw New Exception("Sequence error.(requestToken is blank)")

        'アクセストークン取得
        Dim accessTokenData As NameValueCollection = GetOAuthToken(New Uri(accessTokenUrl), pinCode, requestToken, Nothing)

        If accessTokenData IsNot Nothing Then
            token = accessTokenData.Item("oauth_token")
            tokenSecret = accessTokenData.Item("oauth_token_secret")
            'サービスごとの独自拡張対応
            If Me.userIdentKey <> "" Then
                authorizedUsername = accessTokenData.Item(Me.userIdentKey)
            Else
                authorizedUsername = ""
            End If
            If token = "" Then Return False
            Return True
        Else
            Return False
        End If
    End Function

    '''<summary>
    '''OAuth認証のアクセストークン取得。xAuth方式
    '''</summary>
    '''<param name="accessTokenUrl">アクセストークンの取得先URL</param>
    '''<param name="username">認証用ユーザー名</param>
    '''<param name="password">認証用パスワード</param>
    '''<returns>取得結果真偽値</returns>
    Public Function AuthenticateXAuth(ByVal accessTokenUrl As String, ByVal username As String, ByVal password As String) As Boolean Implements IHttpConnection.Authenticate
        'ユーザー・パスワードチェック
        If String.IsNullOrEmpty(username) OrElse String.IsNullOrEmpty(password) Then
            Throw New Exception("Sequence error.(username or password is blank)")
        End If
        'xAuthの拡張パラメータ設定
        Dim parameter As New Dictionary(Of String, String)
        parameter.Add("x_auth_mode", "client_auth")
        parameter.Add("x_auth_username", username)
        parameter.Add("x_auth_password", password)

        'アクセストークン取得
        Dim accessTokenData As NameValueCollection = GetOAuthToken(New Uri(accessTokenUrl), "", "", parameter)

        If accessTokenData IsNot Nothing Then
            token = accessTokenData.Item("oauth_token")
            tokenSecret = accessTokenData.Item("oauth_token_secret")
            'サービスごとの独自拡張対応
            If Me.userIdentKey <> "" Then
                authorizedUsername = accessTokenData.Item(Me.userIdentKey)
            Else
                authorizedUsername = ""
            End If
            If token = "" Then Return False
            Return True
        Else
            Return False
        End If
    End Function

    '''<summary>
    '''OAuth認証のリクエストトークン取得。リクエストトークンと組み合わせた認証用のUriも生成する
    '''</summary>
    '''<param name="accessTokenUrl">リクエストトークンの取得先URL</param>
    '''<param name="authorizeUrl">ブラウザで開く認証用URLのベース</param>
    '''<param name="requestToken">[OUT]取得したリクエストトークン</param>
    '''<returns>取得結果真偽値</returns>
    Private Function GetAuthenticatePageUri(ByVal requestTokenUrl As String, _
                                        ByVal authorizeUrl As String, _
                                        ByRef requestToken As String) As Uri
        Const tokenKey As String = "oauth_token"

        'リクエストトークン取得
        Dim reqTokenData As NameValueCollection = GetOAuthToken(New Uri(requestTokenUrl), "", "", Nothing)
        If reqTokenData IsNot Nothing Then
            requestToken = reqTokenData.Item(tokenKey)
            'Uri生成
            Dim ub As New UriBuilder(authorizeUrl)
            ub.Query = String.Format("{0}={1}", tokenKey, requestToken)
            Return ub.Uri
        Else
            Return Nothing
        End If
    End Function

    '''<summary>
    '''OAuth認証のトークン取得共通処理
    '''</summary>
    '''<param name="requestUri">各種トークンの取得先URL</param>
    '''<param name="pinCode">PINフロー時のアクセストークン取得時に設定。それ以外は空文字列</param>
    '''<param name="requestToken">PINフロー時のリクエストトークン取得時に設定。それ以外は空文字列</param>
    '''<param name="parameter">追加パラメータ。xAuthで使用</param>
    '''<returns>取得結果のデータ。正しく取得出来なかった場合はNothing</returns>
    Private Function GetOAuthToken(ByVal requestUri As Uri, ByVal pinCode As String, ByVal requestToken As String, ByVal parameter As Dictionary(Of String, String)) As NameValueCollection
        Dim webReq As HttpWebRequest = Nothing
        'HTTPリクエスト生成。PINコードもパラメータも未指定の場合はGETメソッドで通信。それ以外はPOST
        If String.IsNullOrEmpty(pinCode) AndAlso parameter Is Nothing Then
            webReq = CreateRequest("GET", requestUri, Nothing, False)
        Else
            webReq = CreateRequest("POST", requestUri, parameter, False) 'ボディに追加パラメータ書き込み
        End If
        'OAuth関連パラメータ準備。追加パラメータがあれば追加
        Dim query As New Dictionary(Of String, String)
        If parameter IsNot Nothing Then
            For Each kvp As KeyValuePair(Of String, String) In parameter
                query.Add(kvp.Key, kvp.Value)
            Next
        End If
        'PINコードが指定されていればパラメータに追加
        If Not String.IsNullOrEmpty(pinCode) Then query.Add("oauth_verifier", pinCode)
        'OAuth関連情報をHTTPリクエストに追加
        AppendOAuthInfo(webReq, query, requestToken, "")
        'HTTP応答取得
        Try
            Dim contentText As String = ""
            Dim status As HttpStatusCode = GetResponse(webReq, contentText, Nothing, False)
            If status = HttpStatusCode.OK Then
                Return ParseQueryString(contentText)
            Else
                Return Nothing
            End If
        Catch ex As Exception
            Return Nothing
        End Try
    End Function
#End Region

#Region "OAuth認証用ヘッダ作成・付加処理"
    '''<summary>
    '''HTTPリクエストにOAuth関連ヘッダを追加
    '''</summary>
    '''<param name="webRequest">追加対象のHTTPリクエスト</param>
    '''<param name="query">OAuth追加情報+クエリ or POSTデータ</param>
    '''<param name="token">アクセストークン、もしくはリクエストトークン。未取得なら空文字列</param>
    '''<param name="tokenSecret">アクセストークンシークレット。認証処理では空文字列</param>
    Private Sub AppendOAuthInfo(ByVal webRequest As HttpWebRequest, _
                                        ByVal query As Dictionary(Of String, String), _
                                        ByVal token As String, _
                                        ByVal tokenSecret As String)
        'OAuth共通情報取得
        Dim parameter As Dictionary(Of String, String) = GetOAuthParameter(token)
        'OAuth共通情報にquery情報を追加
        If query IsNot Nothing Then
            For Each item As KeyValuePair(Of String, String) In query
                parameter.Add(item.Key, item.Value)
            Next
        End If
        '署名の作成・追加
        parameter.Add("oauth_signature", CreateSignature(tokenSecret, webRequest.Method, webRequest.RequestUri, parameter))
        'HTTPリクエストのヘッダに追加
        Dim sb As New StringBuilder("OAuth ")
        For Each item As KeyValuePair(Of String, String) In parameter
            '各種情報のうち、oauth_で始まる情報のみ、ヘッダに追加する。各情報はカンマ区切り、データはダブルクォーテーションで括る
            If item.Key.StartsWith("oauth_") Then
                sb.AppendFormat("{0}=""{1}"",", item.Key, UrlEncode(item.Value))
            End If
        Next
        webRequest.Headers.Add(HttpRequestHeader.Authorization, sb.ToString)
    End Sub

    '''<summary>
    '''OAuthで使用する共通情報を取得する
    '''</summary>
    '''<param name="token">アクセストークン、もしくはリクエストトークン。未取得なら空文字列</param>
    '''<returns>OAuth情報のディクショナリ</returns>
    Private Function GetOAuthParameter(ByVal token As String) As Dictionary(Of String, String)
        Dim parameter As New Dictionary(Of String, String)
        parameter.Add("oauth_consumer_key", consumerKey)
        parameter.Add("oauth_signature_method", "HMAC-SHA1")
        parameter.Add("oauth_timestamp", Convert.ToInt64((DateTime.UtcNow - UnixEpoch).TotalSeconds).ToString())   'epoch秒
        parameter.Add("oauth_nonce", NonceRandom.Next(123400, 9999999).ToString())
        parameter.Add("oauth_version", "1.0")
        If Not String.IsNullOrEmpty(token) Then parameter.Add("oauth_token", token) 'トークンがあれば追加
        Return parameter
    End Function

    '''<summary>
    '''OAuth認証ヘッダの署名作成
    '''</summary>
    '''<param name="tokenSecret">アクセストークン秘密鍵</param>
    '''<param name="method">HTTPメソッド文字列</param>
    '''<param name="uri">アクセス先Uri</param>
    '''<param name="parameter">クエリ、もしくはPOSTデータ</param>
    '''<returns>署名文字列</returns>
    Private Function CreateSignature(ByVal tokenSecret As String, _
                                            ByVal method As String, _
                                            ByVal uri As Uri, _
                                            ByVal parameter As Dictionary(Of String, String) _
                                        ) As String
        'パラメタをソート済みディクショナリに詰替(OAuthの仕様)
        Dim sorted As New SortedDictionary(Of String, String)(parameter)
        'URLエンコード済みのクエリ形式文字列に変換
        Dim paramString As String = CreateQueryString(sorted)
        'アクセス先URLの整形
        Dim url As String = String.Format("{0}://{1}{2}", uri.Scheme, uri.Host, uri.AbsolutePath)
        '署名のベース文字列生成(&区切り)。クエリ形式文字列は再エンコードする
        Dim signatureBase As String = String.Format("{0}&{1}&{2}", method, UrlEncode(url), UrlEncode(paramString))
        '署名鍵の文字列をコンシューマー秘密鍵とアクセストークン秘密鍵から生成(&区切り。アクセストークン秘密鍵なくても&残すこと)
        Dim key As String = UrlEncode(consumerSecret) + "&"
        If Not String.IsNullOrEmpty(tokenSecret) Then key += UrlEncode(tokenSecret)
        '鍵生成&署名生成
        Dim hmac As New Cryptography.HMACSHA1(Encoding.ASCII.GetBytes(key))
        Dim hash As Byte() = hmac.ComputeHash(Encoding.ASCII.GetBytes(signatureBase))
        Return Convert.ToBase64String(hash)
    End Function

#End Region

    '''<summary>
    '''初期化。各種トークンの設定とユーザー識別情報設定
    '''</summary>
    '''<param name="consumerKey">コンシューマー鍵</param>
    '''<param name="consumerSecret">コンシューマー秘密鍵</param>
    '''<param name="accessToken">アクセストークン</param>
    '''<param name="accessTokenSecret">アクセストークン秘密鍵</param>
    '''<param name="userIdentifier">アクセストークン取得時に得られるユーザー識別情報。不要なら空文字列</param>
    Public Sub Initialize(ByVal consumerKey As String, _
                                    ByVal consumerSecret As String, _
                                    ByVal accessToken As String, _
                                    ByVal accessTokenSecret As String, _
                                    ByVal userIdentifier As String)
        Me.consumerKey = consumerKey
        Me.consumerSecret = consumerSecret
        Me.token = accessToken
        Me.tokenSecret = accessTokenSecret
        Me.userIdentKey = userIdentifier
    End Sub

    '''<summary>
    '''初期化。各種トークンの設定とユーザー識別情報設定
    '''</summary>
    '''<param name="consumerKey">コンシューマー鍵</param>
    '''<param name="consumerSecret">コンシューマー秘密鍵</param>
    '''<param name="accessToken">アクセストークン</param>
    '''<param name="accessTokenSecret">アクセストークン秘密鍵</param>
    '''<param name="username">認証済みユーザー名</param>
    '''<param name="userIdentifier">アクセストークン取得時に得られるユーザー識別情報。不要なら空文字列</param>
    Public Sub Initialize(ByVal consumerKey As String, _
                                ByVal consumerSecret As String, _
                                ByVal accessToken As String, _
                                ByVal accessTokenSecret As String, _
                                ByVal username As String, _
                                ByVal userIdentifier As String)
        Initialize(consumerKey, consumerSecret, accessToken, accessTokenSecret, userIdentifier)
        authorizedUsername = username
    End Sub

    '''<summary>
    '''アクセストークン
    '''</summary>
    Public ReadOnly Property AccessToken() As String
        Get
            Return token
        End Get
    End Property

    '''<summary>
    '''アクセストークン秘密鍵
    '''</summary>
    Public ReadOnly Property AccessTokenSecret() As String
        Get
            Return tokenSecret
        End Get
    End Property

    '''<summary>
    '''認証済みユーザー名
    '''</summary>
    Public ReadOnly Property AuthUsername() As String Implements IHttpConnection.AuthUsername
        Get
            Return authorizedUsername
        End Get
    End Property
End Class

他の類似クラスとの共通化のために定義したインターフェース

Imports System.Net

Public Interface IHttpConnection

    Function GetContent(ByVal method As String, _
            ByVal requestUri As Uri, _
            ByVal param As Dictionary(Of String, String), _
            ByRef content As String, _
            ByVal headerInfo As Dictionary(Of String, String)) As HttpStatusCode

    Function Authenticate(ByVal url As String, ByVal username As String, ByVal password As String) As Boolean

    ReadOnly Property AuthUsername() As String
End Interface

Ubuntu 8.04 LTS(Hardy Heron)のClamAV(0.94.x)サポート打ち切り

ちょっとメモ。

運用しているUbuntu 8.04(Hardy Heron)で動かしているClamAVが10月頃からウィルスを捕まえなくなっていてなぜだろう?と思っていたのですが、今日になって原因が判明した。


[Clamav-announce] End of Life Announcement: ClamAV 0.94.x
http://lurker.clamav.net/message/20091006.143601.d27bbd20.ja.html


2009年10月6日のMLで流れてた。


すでに2ヶ月経過しているけど、Ubuntuのセキュリティアップデートには更新が来てない。新しいバージョンのUbuntuには0.95系が乗っているみたいだけど。次のLTSのリリースは来年の4月頃なわけで、あと4ヶ月も待っていられない。

で、色々探したら、backportsには0.95が入ってた。安定運用を第一に考えていたので、souces.listにはbackportsは入れてなかったのです。


で、souces.listのhardy-backports行のコメントを外して更新したら、無事clamavだけ更新がかかった。一安心。


この件に関しては、(Ubuntu側で全パッケージを把握するのは難しいにしても)バージョンアップしないと新しい定義ファイルが受け付けられない、というセキュリティ的に致命的な問題なので、security updateで対応して欲しかったなぁ、と思った次第。

TweetUp Tokyo 09 へ行ってきた

10月15日に恵比寿で行われたTwitterオフィシャルのユーザーイベントへ行ってきました。
数週間前に行われた、こちらもオフィシャルの開発者イベントに出席したばかりだったこともあり初めは欠席するつもりだったのですが、ありがたいことにLTのお誘いを頂いたので出席を決めました。

イベント一週間前にお誘いを頂き、それからプレゼン資料を作りはじめたわけですが、お題を決めるのに時間を取られました。作りはじめたらサクッっと出来たのですが、途中ファイル形式の指定があって焼き直したり、いつの間にか資料提出の締切が早まったりと、微妙に準備不足な感がありました。最終的には、会場へ向かう電車のなかで最終確認をする羽目に。

家も勤務先も田舎のため、事前に決められていた集合時間には間に合わず、皆さんと同じ時間に受付をしました。ですが、Webからの参加申込をしていなかったためか参加者名簿に名前がない、というアクシデントも。すんなり通して頂けましたけど。

すぐに開発者の方々(cheebowさん、lynmockさん、takuma104さん)に会えたのは助かりました。

イベントが始まると、会場は人だらけで動き回るのに苦労するほど。開始してしばらくすると、そこかしこで話が盛り上がりはじめ、オープニングのbizの話を聞くのも難しい状態に。早々に諦めましたよ。

まぁ、それはいいとして、一番困ったのは、「誰が誰だかわからん!!」ことでした。名札にアイコン必須です。ちぃ覚えた!
もう少し人数少なければコミュニケーションの取りようもあったと思うのですが。

日本専用携帯電話向けTwitter公式サイト http://twtr.jp の発表があり、会場は盛り上がりました。スマートフォンは未対応のようで残念でしたが、iPhoneには対応していました。
その後、Willcomの公式コンテンツ対応と新PHSの発表があり、いよいよクライアント開発者のLTです。


始めはTwit!のcheebowさん。クライアント開発の苦労話と命名裏話を面白おかしく話されました。さすがうまいっす。みんなつぶやきのことをtwitって言うのやめようぜ!w

次に携帯百景のkimzaさん。元々はTwitterと連携していなかったとは驚き。単体サービスとしても面白そうだな、と認識を改めました。赤字はなんとかなるといいですねw

NatsuLion for iPhoneのtakuma104さん。Twitter向けのiPhoneアプリをこれでもか!と紹介されました。すげーなiPhone。でも、実は最近話題のTiltShiftの方もすごく気になっちゃいました。

P3:PeraPeraPrvのlynmockさん。P3の魅力(マルチプラットフォーム、カスタマイズ幅、大槍さんアイコンw)を詰め込んだプレゼンでした。グッズ楽しげ。欲しいわー。

そして私。お題は「Twitterの出会い力」。開発寄りの話題にするか、クライアントアピールにするか、など悩んだのですが、ユーザーイベントなのでクライアントの話も絡めつつTwitter自体にフォーカスしてみました。ある意味無難なお題になっちゃったかな?まぁ、大外しするよりはいいか、と。
お題は、まぁ流行りってことでつけたんですが、後でご本人登場して、ひそかに冷や汗かきました。
内容は、クライアント作ってたら協力者がいっぱいできて、twitterってすごいと思った、という体験談。そこから膨らませて、テレビでも話題になってるしどんどん広めて行きましょう!という締めに持って行った感じです。
こんなところがいいよね、とか書こうかとも思ったのですが、そんなこと語れるほど偉くも頭良くもないので体験談にとどめました。ここはぜひDGさんに頑張って欲しいところ。

その後、なんとか数人の方々とお話させて頂きました。ほんの数人でしたけど。

イベント終わった後は、cheebowさん、lynmockさん、arawasさん、takuma104さんと軽く飲みました。
みんな面白いし、こっちの方が落ち着きますねw
ぜひまた飲みたいです。

初のLTという貴重な体験と、ネットの向こう側にいた皆さんと実際に引き合わせてくれたこのイベントは、本当に良い思い出になりそうです。

091019追記
LT資料 http://www.slideshare.net/kirifeather/kirifeathers-lt-in-tweetup-tokyo-09-fall

This diary was written with HatenaSync

ReTweetについての戸惑い

RTって、

  • 周知(晒し上げ、オープンチャット)
  • 引用返信

に使われています。
で、引用返信の時と周知の時って、内容を見ればどっちの用途で使ったのか分かるんですが、見栄えも機能(到達範囲)も同じ訳です。
一方、通常の返信は、ある程度到達範囲が絞られていて、返信元の発言情報も付加されていて、より会話っぽくなります。

通常の返信はお互い向き合って普通の声量で話しているイメージなのですが、引用返信はそっぽを向いて大声を張り上げて返事をするイメージです。

こちらが通常返信で話しかけているのに相手が引用返信で返して来ると、「なんですぐ隣にいるのにそっぽ向いて返事するのさ」、と感じて、私は戸惑ってしまうのです。

両者の違いを知らずに使っている人も多いと思いますので、多くの方にこの違いを知って頂けたらいいな、と思うわけでした。

This diary was written with HatenaSync

マークについて

APIについては、まあ予想通りの反応。
ただ、「webモードとかapiモードって何?」という反応については説明不足だったかな、と反省しています。あと、警告メッセージの文章もイマイチ分かりにくい、もしくは誤解を招いたかと、こちらも反省点。

で、意外だったのはフィルタヒット時のマークについて。

今回、新着時未読クリアを実装するにあたって、フィルタヒット時は除外するようにしたのですが、その目印としてヒットマーク有無で判定するようにしました。

で、以前の「何もしない」というよくわからない名称のオプションを排して、マークを全てに付けるように変更しました。活用している、という話をあまり聞かなかったのでこの変更に踏み切ったわけですが、油断していました。

全てにマークがついて不便なのでバージョンアップしない、といった意見がある一方で、「♪」マークって何?というつぶやきも見かけるわけで、なかなか難しいところです。

不便、という意見以上に新着時未読クリアが便利!という意見が多ければこのままでもいいかと思ったのですが、予想以上に少なかったという(汗
完全に裏目にでた修正となりました。

マークを活用しているユーザーがいることが分かったので、この機能はパワーアップして再実装しようと思います。

フィルタ(振り分けルール)周りを弄るので、要望のあった除外条件も合わせて実装してみるつもりです。

機能の変更は難しいね、というお話でした。

This diary was written with HatenaSync