ニコ生 コメントビューアー API - CommentClient.fs

namespace RLib.NiconicoAPI

#light

open RLib.NiconicoAPI.Xml

open System
open System.IO
open System.Net.Sockets
open System.Text
open System.Threading

(*
TODO: 機能追加
1. 各XMLのレコード定義
2. XMLテキストをレコードに変換
3. 0コメ取得コードを非同期処理として書き直す。
 (できる限り実現させる様にすることで、使う側が非常に楽になる)
*)

///送受信データの判別共用体
type CommentData =
    ///受信エラー
    | ReceiveNG of Exception * int * int
    ///正常受信
    | ReceiveOK of string * int * int
    ///送信エラー
    | SendNG of Exception * int * int
    ///正常送信
    | SendOK of string * int * int
        
///<summary>ニコ生コメントサーバの為の非同期送受信を提供します。</summary>
///<param name="setting">CommentClientの設定情報</param>
type CommentClient (statusType:GetPlayerStatus) =
    let _enc = Encoding.UTF8
    let _commentThreadID = "<thread thread=\"{0}\" version=\"20061206\" res_from=\"-200\"/>"
    let _commentSendTemp = @"<chat thread=""{0}"" ticket=""{1}"" vpos=""{2}"" postkey=""{3}"" mail=""{4}"" user_id=""{5}"" premium=""{6}"">{7}</chat>"

    let _client = new TcpClient(statusType.Ms.Addr, statusType.Ms.Port)
    let _stream = _client.GetStream()
    let _mainThreadID = Thread.CurrentThread.ManagedThreadId

    let _threadID = statusType.Ms.Thread

    /// 送信文字列をbyte配列に変換してNULLを付け足す。
    let toByte (comment:string) =
        Array.append
            <| _enc.GetBytes(comment)
            <| Array.zeroCreate 1

    ///キャンセルトークン
    let token = Async.DefaultCancellationToken

    ///非同期送信イベント
    let transceiverEvent = new Event<CommentData>()

    ///送受信イベントを提供する。
    [<CLIEventAttribute>]
    member x.Transceiver = transceiverEvent.Publish
    
    ///<summary>スレッドIDを送信する。</summary>
    ///<param name="threadid">ニコ生のスレッドID</param>
    member x.SendThreadId (threadid:string) =
        x.Send
            <| String.Format(_commentThreadID, threadid)
            
    ///CommentClientSettingで設定したスレッドIDを送信する。
    member x.SendThreadId () =
        x.SendThreadId _threadID
            
    ///<summary>コメントXMLに変換後に送信する。</summary>
    ///<param name="threadid">コメント</param>
    member x.SendComment comment =
        let comment = Commons.toHtmlDecode comment
        x.Send
            <| String.Format(_commentSendTemp, _threadID)

    ///<summary>コメントを非同期で送信する。</summary>
    ///<param name="comment">コメント</param>
    member x.Send (comment:string) =
        let asyncSend =
            async {
                let threadID = Thread.CurrentThread.ManagedThreadId
                try
                    let b = toByte comment
                    do! _stream.AsyncWrite(b, 0, b.Length)
                    transceiverEvent.Trigger
                        <| CommentData.SendOK(comment, _mainThreadID, threadID)
                with
                | ex ->
                    transceiverEvent.Trigger
                        <| CommentData.SendNG(ex, _mainThreadID, threadID)
            }
        Async.Start(asyncSend, token)
       
    ///<summary>同期的に1回分のコメントを取得する。</summary>
    member x.GetCooment() =
        let rec startmsg l =
            try
                match _stream.ReadByte() with
                | 0 ->
                    let comment =
                        l
                        |> List.map(char>>string)
                        |> List.reduce((+))
                    CommentData.ReceiveOK(comment, _mainThreadID, -1)
                | x -> startmsg (l@[x])
            with
            | ex -> CommentData.ReceiveNG(ex, _mainThreadID, -1)
        startmsg []

    ///<summary>永続的に非同期コメント受信を行う。</summary>
    member x.ReceiveStart() =
        // 永続非同期受信を行う
        let rec loop arr = async {
            let threadID = Thread.CurrentThread.ManagedThreadId
            try
                let! data = _stream.AsyncRead(1)
                let nextdata = Array.append arr data
                match data with
                | [| 0uy |] ->
                    //コメントに変換
                    let comment =
                        nextdata
                        |> Array.filter ((<>)0uy)
                        |> _enc.GetString
                    //受信イベントの発行
                    transceiverEvent.Trigger
                        <| CommentData.ReceiveOK(comment, _mainThreadID, threadID)
                | _ ->
                    //次の受信待ちを呼び出す。
                    do! loop nextdata
            with
            | ex ->
                transceiverEvent.Trigger
                    <| CommentData.ReceiveNG(ex, _mainThreadID, threadID)

            do! loop Array.empty
        }
        Async.Start(loop Array.empty, token)

    interface IDisposable with
        /// CommentClientに割り当てられたリソースをすべて解放する。
        member x.Dispose() =
            Async.CancelDefaultToken()
            try
                _stream.Dispose()
                _client.Close()
                // 必要? 念のため相手側のサーバのコネクションを
                // 完全に消すために使用。
                GC.Collect()
            with | _ -> ()
    • -