node-nicovideo-api
Version:
nicovideo api (video, live, etc..) wrapper package for node.js
602 lines (489 loc) • 16.7 kB
JavaScript
// Generated by CoffeeScript 1.10.0
/**
* 放送中の番組のコメントの取得と投稿を行うクラスです。
* @class CommentProvider
*/
(function() {
var COMMANDS, Cheerio, CommentProvider, Deferred, Emitter, NicoException, NicoLiveComment, NicoUrl, Request, Socket, _, chatResults, deepFreeze, sprintf,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
_ = require("lodash");
Cheerio = require("cheerio");
deepFreeze = require("deep-freeze");
Request = require("request-promise");
Deferred = require("promise-native-deferred");
Socket = require("net").Socket;
sprintf = require("sprintf").sprintf;
Emitter = require("disposable-emitter");
NicoUrl = require("../NicoURL");
NicoException = require("../NicoException");
NicoLiveComment = require("./NicoLiveComment");
chatResults = deepFreeze({
SUCCESS: 0,
CONTINUOUS_POST: 1,
THREAD_ID_ERROR: 2,
TICKET_ERROR: 3,
DIFFERENT_POSTKEY: 4,
_DIFFERENT_POSTKEY: 8,
LOCKED: 5
});
COMMANDS = {
connect: _.template("<thread thread=\"<%- thread %>\" version=\"20061206\"\n res_from=\"-<%- firstGetComments %>\"/>"),
post: _.template("<chat thread=\"<%-threadId%>\" ticket=\"<%-ticket%>\"\n postkey=\"<%-postKey%>\" mail=\"<%-command%>\" user_id=\"<%-userId%>\"\n premium=\"<%-isPremium%>\"><%-comment%></chat>")
};
module.exports = CommentProvider = (function(superClass) {
extend(CommentProvider, superClass);
CommentProvider.ChatResult = chatResults;
/**
* @param {NicoLiveInfo} liveInfo
* @return {Promise}
*/
CommentProvider.instanceFor = function(liveInfo) {
if (liveInfo == null) {
throw new TypeError("liveInfo must be instance of NicoLiveInfo");
}
return Promise.resolve(new CommentProvider(liveInfo));
};
/**
* @private
* @propery {NicoLiveInfo} _live
*/
CommentProvider.prototype._live = null;
/**
* @private
* @propery {net.Socket} _socket
*/
CommentProvider.prototype._socket = null;
/**
* @private
* @propery {Object} _postInfo
*/
CommentProvider.prototype._postInfo = null;
/**
* @property {Boolean} isFirstResponseProsessed
*/
CommentProvider.prototype.isFirstResponseProsessed = false;
/**
* @constructor
* @param {NicoLiveInfo} _live
*/
function CommentProvider(_live) {
this._live = _live;
CommentProvider.__super__.constructor.apply(this, arguments);
this.isFirstResponseProsessed = false;
this._postInfo = {
ticket: null,
postKey: null,
threadId: null
};
}
/**
* このインスタンスが保持しているNicoLiveInfoオブジェクトを取得します。
* @method getLiveInfo
* @return {NicoLiveInfo}
*/
CommentProvider.prototype.getLiveInfo = function() {
return this._live;
};
/**
* @private
* @method _canContinue
*/
CommentProvider.prototype._canContinue = function() {
if (this.disposed) {
throw new Error("CommentProvider has been disposed");
}
};
/**
* [Method for testing] Stream given xml data as socket received data.
* @private
* @method _pourXMLData
* @param {String} xml
*/
CommentProvider.prototype._pourXMLData = function(xml) {
return this._didReceiveData(xml);
};
/**
* コメントサーバーへ接続します。
*
* 既に接続済みの場合は接続を行いません。
* 再接続する場合は `CommentProvider#reconnect`を利用してください。
*
* @method connect
* @fires CommentProvider#did-connect
* @param {Object} [options]
* @param {Number} [options.firstGetComments=100] 接続時に取得するコメント数
* @param {Number} [options.timeoutMs=5000] タイムアウトまでのミリ秒
* @return {Promise}
*/
CommentProvider.prototype.connect = function(options) {
var serverInfo;
if (options == null) {
options = {};
}
this._canContinue();
if (this._socket != null) {
return Promise.resolve(this);
}
serverInfo = this._live.get("comment");
options = _.defaults({}, options, {
firstGetComments: 100,
timeoutMs: 5000
});
return new Promise((function(_this) {
return function(resolve, reject) {
var timerId;
timerId = null;
_this._socket = new Socket;
_this._socket.once("connect", function() {
var params;
_this.once("_did-receive-connection-response", function() {
clearTimeout(timerId);
resolve(_this);
});
params = _.assign({}, {
firstGetComments: options.firstGetComments
}, serverInfo);
_this._socket.write(COMMANDS.connect(params) + '\0');
}).on("data", _this._didReceiveData.bind(_this)).on("error", _this._didErrorOnSocket.bind(_this)).on("close", _this._didCloseSocket.bind(_this));
_this._socket.connect({
host: serverInfo.addr,
port: serverInfo.port
});
return timerId = setTimeout(function() {
reject(new Error("[CommentProvider: " + _this._live.id + "] Connection timed out."));
}, options.timeoutMs);
};
})(this));
};
/**
* @method reconnect
* @param {Object} options 接続設定(connectメソッドと同じ)
* @return {Promise}
*/
CommentProvider.prototype.reconnect = function(options) {
this._canContinue();
if (this._socket != null) {
this._socket.destroy();
}
this._socket = null;
return this.connect();
};
/**
* コメントサーバから切断します。
* @method disconnect
* @fires CommentProvider#did-disconnect
*/
CommentProvider.prototype.disconnect = function() {
this._canContinue();
if (this._socket == null) {
return;
}
this._socket.removeAllListeners();
this._socket.destroy();
this._socket = null;
this.emit("did-close-connection");
};
/**
* APIからpostkeyを取得します。
* @private
* @method _ferchPostKey
* @return {Promise}
*/
CommentProvider.prototype._fetchPostKey = function() {
var postKey, threadId, url;
this._canContinue();
threadId = this._live.get("comment.thread");
url = sprintf(NicoUrl.Live.GET_POSTKEY, threadId);
postKey = "";
return Request.get({
resolveWithFullResponse: true,
url: url,
jar: this._live._session.cookie
}).then((function(_this) {
return function(res) {
if (res.statusCode === 200) {
postKey = /^postkey=(.*)\s*/.exec(res.body);
if (postKey != null) {
postKey = postKey[1];
}
}
if (postKey !== "") {
_this._postInfo.postKey = postKey;
return Promise.resolve(postKey);
} else {
return Promise.reject(new Error("Failed to fetch post key"));
}
};
})(this));
};
/**
* コメントを投稿します。
* @method postComment
* @param {String} msg 投稿するコメント
* @param {String|Array.<String>} [command] コマンド(184, bigなど)
* @param {Number} [timeoutMs]
* @return {Promise}
*/
CommentProvider.prototype.postComment = function(msg, command, timeoutMs) {
if (command == null) {
command = "";
}
if (timeoutMs == null) {
timeoutMs = 3000;
}
this._canContinue();
if (typeof msg !== "string" || msg.replace(/\s/g, "") === "") {
return Promise.reject(new Error("Can not post empty comment"));
}
if (this._socket == null) {
return Promise.reject(new Error("No connected to the comment server."));
}
if (Array.isArray(command)) {
command = command.join(" ");
}
return this._fetchPostKey().then((function(_this) {
return function() {
var defer, disposer, postInfo, timerId;
defer = new Deferred;
timerId = null;
postInfo = {
userId: _this._live.get("user.id"),
isPremium: _this._live.get("user.isPremium") | 0,
comment: msg,
command: command,
threadId: _this._postInfo.threadId,
postKey: _this._postInfo.postKey,
ticket: _this._postInfo.ticket
};
disposer = _this._onDidReceivePostResult(function(arg) {
var status;
status = arg.status;
disposer.dispose();
clearTimeout(timerId);
switch (status) {
case chatResults.SUCCESS:
defer.resolve();
break;
case chatResults.THREAD_ID_ERROR:
defer.reject(new NicoException({
message: "Failed to post comment. (reason: thread id error)",
code: status
}));
break;
case chatResults.TICKET_ERROR:
defer.reject(new NicoException({
message: "Failed to post comment. (reason: ticket error)",
code: status
}));
break;
case chatResults.DIFFERENT_POSTKEY:
case chatResults._DIFFERENT_POSTKEY:
defer.reject(new NicoException({
message: "Failed to post comment. (reason: postkey is defferent)",
code: status
}));
break;
case chatResults.LOCKED:
defer.reject(new NicoException({
message: "Your posting has been locked.",
code: status
}));
break;
case chatResults.CONTINUOUS_POST:
defer.reject(new NicoException({
message: "Can not post continuous the same comment.",
code: status
}));
break;
default:
defer.reject(new NicoException({
message: "Failed to post comment. (status: " + status + ")",
code: status
}));
}
});
timerId = setTimeout(function() {
disposer.dispose();
return defer.reject(new Error("Post result response is timed out."));
}, timeoutMs);
_this._socket.write(COMMANDS.post(postInfo) + "\0");
return defer.promise;
};
})(this));
};
/**
* インスタンスを破棄します。
* @method dispose
*/
CommentProvider.prototype.dispose = function() {
this._live = null;
this._postInfo = null;
this.disconnect();
return CommentProvider.__super__.dispose.apply(this, arguments);
};
/**
* コメント受信処理
* @private
* @method _didReceiveData
* @param {String} xml
*/
CommentProvider.prototype._didReceiveData = function(xml) {
var $elements, comments;
this.emit("did-receive-data", xml);
comments = [];
$elements = Cheerio.load(xml)(":root");
$elements.each((function(_this) {
return function(i, element) {
var $element, comment, status;
$element = Cheerio(element);
switch (element.name) {
case "thread":
_this._postInfo.ticket = $element.attr("ticket");
_this.emit("_did-receive-connection-response");
break;
case "chat":
comment = NicoLiveComment.fromRawXml($element.toString(), _this._live.get("user.id"));
comments.push(comment);
_this.emit("did-receive-comment", comment);
if (comment.isPostByDistributor() && comment.comment === "/disconnect") {
_this.emit("did-end-live", _this._live);
_this.disconnect();
}
break;
case "chat_result":
status = $element.attr("status");
status = status | 0;
comment = NicoLiveComment.fromRawXml($element.find("chat").toString(), _this._live.get("user.id"));
_this.emit("did-receive-post-result", {
status: status
});
_this.emit("did-receive-comment", comment);
}
};
})(this));
if (this.isFirstResponseProsessed === false) {
this.isFirstResponseProsessed = true;
this.lockAutoEmit("did-process-first-response", comments);
}
};
/**
* コネクション上のエラー処理
* @private
* @method _didErrorOnSocket
*/
CommentProvider.prototype._didErrorOnSocket = function(error) {
this.emit("did-error", error);
};
/**
* コネクションが閉じられた時の処理
* @private
* @method _didCloseSocket
*/
CommentProvider.prototype._didCloseSocket = function(hadError) {
if (hadError) {
this.emit("error", "Connection closing error (unknown)");
}
this.emit("did-close-connection");
};
/**
* コメントサーバのスレッドID変更を監視するリスナ
* @private
* @method _didRefreshLiveInfo
*/
CommentProvider.prototype._didRefreshLiveInfo = function() {
this._postInfo.threadId = this._live.get("comment").thread;
};
/**
* @private
* @event CommentProvider#did-receive-post-result
* @param {Number} status
*/
/**
* @private
* @method _onDidReceivePostResult
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype._onDidReceivePostResult = function(listener) {
return this.on("did-receive-post-result", listener);
};
/**
* Fire on received and processed thread info and comments first
* @event CommentProvider#did-process-first-response
* @param {Array.<NicoLiveComment>}
*/
/**
* @method onDidProcessFirstResponse
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype.onDidProcessFirstResponse = function(listener) {
return this.on("did-process-first-response", listener);
};
/**
* Fire on raw response received
* @event CommentProvider#did-receive-data
* @params {String} data
*/
/**
* @method onDidReceiveData
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype.onDidReceiveData = function(listener) {
return this.on("did-receive-data", listener);
};
/**
* Fire on comment received
* @event CommentProvider#did-receive-comment
* @params {NicoLiveComment} comment
*/
/**
* @method onDidReceiveComment
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype.onDidReceiveComment = function(listener) {
return this.on("did-receive-comment", listener);
};
/**
* Fire on error raised on Connection
* @event CommentProvider#did-error
* @params {Error} error
*/
/**
* @method onDidError
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype.onDidError = function(listener) {
return this.on("did-error", listener);
};
/**
* Fire on connection closed
* @event CommentProvider#did-close-connection
*/
/**
* @method onDidCloseConnection
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype.onDidCloseConnection = function(listener) {
return this.on("did-close-connection", listener);
};
/**
* Fire on live ended
* @event CommentProvider#did-end-live
*/
/**
* @method onDidEndLive
* @param {Function} listener
* @return {Disposable}
*/
CommentProvider.prototype.onDidEndLive = function(listener) {
return this.on("did-end-live", listener);
};
return CommentProvider;
})(Emitter);
}).call(this);