C# でニコニコ生放送のコメント拾ったり投稿したり

細かい説明とかは面倒なんで書きません。さっさとはじめましょう。
ニコニコ**生放送**でしか確認できていません。
あ、私 C# しか書けないんで無いと思いますけど「他の言語でもお願いします」とか無理です。はい。
ここで書いたコードはまとめて [GitHub](https://github.com/iedred7584/Niconama-Comment) に上げておきますので下手くそな説明は必要ないという方は [GitHub](https://github.com/iedred7584/Niconama-Comment) に飛んでくださいな。
また、ここでは詳細な説明は省いているので [GitHub](https://github.com/iedred7584/Niconama-Comment) のサンプルコードをご活用いただけるとありがたいです。

#コメント取得に必要な処理
- ログイン
- 生放送の lv を取得 (チャンネル名、コミュニティ名から取得する場合)
- API で諸々の情報を取得
- 接続

ひとつずつ書いていきましょう。

#ログイン

```cs:Login.cs
var cookie_container = new CookieContainer();
using (var h = new HttpClientHandler() { CookieContainer = cookie_container })
using (var c = new HttpClient(h) { BaseAddress = new Uri("https://secure.nicovideo.jp/secure/login?site=niconico") })
{
    var content = new FormUrlEncodedContent(
        new Dictionary<string, string> {
            { "next_url", "" },
            { "mail", "メールアドレス" },
            { "password", "パスワード" }
        }
    );
    var login = c.PostAsync("", content).Result;
}
```

はい、これで ```cookie_container``` にニコニコのログインで取得したクッキーが入っています。

#lv を取得
最初に lv を用意しておく場合はこの処理は必要ありません。
冒頭にも書いたように、ニコニコチャンネル・コミュニティ名のアドレスから取得する場合に使います。

```cs:GetLv.cs
string lv;
using (var h = new HttpClientHandler() { CookieContainer = cookie_container })
using (var c = new HttpClient(h) { BaseAddress = new Uri("生放送のページURL") })
{
    var ss = c.GetAsync("").Result.Content.ReadAsStringAsync().Result;
    lv = new Regex("<meta property=\"og:url\" content=\"http://live.nicovideo.jp/watch/(.*?)\"", RegexOptions.Multiline).Match(ss).Groups[1].Value;
}
```

これで ```string``` 変数 ```lv``` に ```lv000000000``` のかたちで入っています。

#API から情報を取得
```GetPlayerStatus``` から情報を取得します。
```string``` 変数 ```lv``` に ```lv``` を ```lv000000000``` のかたちで入れておいてください。

```cs:GetPlayerStatus.cs
using (var h = new HttpClientHandler() { CookieContainer = cookie_container })
using (var c = new HttpClient(h) { BaseAddress = new Uri("http://live.nicovideo.jp/api/getplayerstatus?v=" + lv) })
{
   var gps = c.GetAsync("").Result.Content.ReadAsStringAsync().Result.Replace("\n", "");
   var gps_match = new Regex(@"<addr>(.*?)</addr><port>(.*?)</port><thread>(.*?)</thread>", RegexOptions.None).Match(gps);
   var address = gps_match.Groups[1].Value;
   var port = gps_match.Groups[2].Value;
   var thread = gps_match.Groups[3].Value;
   var time_match = new Regex(@"<open_time>(.*?)</open_time><start_time>(.*?)</start_time>", RegexOptions.None).Match(gps);
   var open_time = time_match.Groups[1].Value;
   var start_time = time_match.Groups[2].Value;
}
```

はい、これで接続に必要な情報を取得することができました。

#接続してコメントを受信する
とりあえず接続してみましょう。

```cs:Connect.cs
var request = "<thread thread=\"" + thread + "\" version=\"20061206\" res_from=\"-1000\" /> ";
TcpClient tcp = new TcpClient(address, int.Parse(port));
NetworkStream ns = tcp.GetStream();
byte[] sendBytes = Encoding.UTF8.GetBytes(request);
sendBytes[sendBytes.Length - 1] = 0;
ns.Write(sendBytes, 0, sendBytes.Length);
```

はい、接続できました。

ここからコメントが逐次受信されるわけですが、それを処理するのにすこし手こずりました。
相変わらずのウンコードですが私はこのコードでやりたかったことができました。

```cs:Comment.cs
var resSize = 0;
var merge = "";
while (ns.CanRead)
{
    var resBytes = new byte[500];
    resSize = ns.Read(resBytes, 0, resBytes.Length);
    if (resSize == 0) break;
    var message = Encoding.UTF8.GetString(resBytes);
    var elements = message.Split(new string[] { "\0" }, StringSplitOptions.RemoveEmptyEntries);
    foreach (var r in elements)
    {
        var receive = r;
        if (!receive.EndsWith("</chat>"))
        {
            merge += receive;
            continue;
        }
        if (!receive.StartsWith("<chat")) { receive = merge + receive; merge = ""; }
        else if (receive.EndsWith("</chat>")) { receive = merge + receive; merge = ""; }
        else if (merge != "") { receive = merge + receive; merge = ""; }
        if (receive.StartsWith("<chat") && receive.EndsWith("</chat>"))
        {
            var comment = new Regex(">(.*)<", RegexOptions.None).Match(receive).Groups[1].Value.Replace("&lt;", "<").Replace("&gt;", ">");
            if (comment != "")
            {
                if (comment.Contains("/hb ifseetno")) continue;
                var no = double.Parse(new Regex("no=\"(.*?)\"", RegexOptions.None).Match(receive).Groups[1].Value);
                var vpos = DateTimeOffset.FromUnixTimeSeconds((long)(double.Parse(new Regex("date=\"(.*?)\"", RegexOptions.None).Match(receive).Groups[1].Value) - double.Parse(open_time))).ToString("HH:mm:ss");
                var date = DateTimeOffset.FromUnixTimeSeconds(long.Parse(new Regex("date=\"(.*?)\"", RegexOptions.None).Match(receive).Groups[1].Value)).LocalDateTime.ToString("HH:mm:ss");
                var id = new Regex("user_id=\"(.*?)\"", RegexOptions.None).Match(receive).Groups[1].Value;
                Console.WriteLine($"[{no}] ({id}) {comment} 時刻:{date} 経過時刻:{vpos}");
            }
        }
    }
}
```

変な感じに分割されて送られてくるので結構面倒でした。

それと、ただ受信しているだけだとニコニコ側から切断されてしまいます。荒らし対策でしょうかね。
コメント投稿もできなくなってしまうので、定期的（1～3分おき）に以下のコードを実行して ```HeartBeat``` 処理をします。
```lv``` に ```lv000000000``` のかたちで ```lv``` を入れておいてください。

```cs:HeartBeat.cs
using (var h = new HttpClientHandler() { CookieContainer = cookie_container })
using (var c = new HttpClient(h))
{
    var content = new FormUrlEncodedContent(new Dictionary<string, string> { { "v", lv } });
    var heartbeat = c.PostAsync("http://ow.live.nicovideo.jp/api/heartbeat", content).Result;
}
```

##過去コメント取得
```when``` に取得するコメントの範囲を指定します。

```cs:OldComment.cs
var waybackkey = "";
using (var h = new HttpClientHandler() { CookieContainer = cookie_container })
using (var c = new HttpClient(h)) waybackkey = c.GetStringAsync("http://watch.live.nicovideo.jp/api/getwaybackkey?thread=" + thread).Result.Replace("waybackkey=", "");
var sendBytes_ = Encoding.UTF8.GetBytes("<thread thread=\"" + thread + "\" version=\"20061206\" res_from=\"-1000\" waybackkey=\"" + waybackkey + "\"  user_id=\"64924091\" when=\"" + when + "\"/> ");
sendBytes_[sendBytes_.Length - 1] = 0;
ns.Write(sendBytes_, 0, sendBytes_.Length);
```

これで過去コメントが流れてきます。

#コメント投稿
接続した状態でないとコメント投稿できないのでまずは接続してください。
```comment``` にコメントしたい文章を入れておいてください。

```cs:PostComment.cs
var unixtime = (long)DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds - (long)double.Parse(open_time);
using (var h = new HttpClientHandler() { CookieContainer = cookie_container })
using (var c = new HttpClient(h))
{
    var postkey = c.GetStringAsync("http://ow.live.nicovideo.jp/api/getpostkey?thread=" + thread).Result.Replace("postkey=", "");
    var sendBytes_ = Encoding.UTF8.GetBytes("<chat thread=\"" + thread + "\" ticket=\"" + ticket + "\" vpos=\"" + unixtime + "\" postkey=\"" + postkey + "\" mail=\"184\" user_id=\"" + "64924091" + "\" premium=\"\">" + comment + "</chat> ");
    sendBytes_[sendBytes_.Length - 1] = 0;
    ns.Write(sendBytes_, 0, sendBytes_.Length);
}
```

ここまででコメント受信・投稿ができました。
お読みいただきありがとうございました。
ご質問・ご指摘等ございましたらコメントをお願いいたします。

[ingen084](https://twitter.com/ingen084)様の[こちらのページ](http://blog.ingen084.net/blog/1691)
を参考にさせていただきました。ありがとうございます。

では、ノシ