UNPKG

node-nicovideo-api

Version:

nicovideo api (video, live, etc..) wrapper package for node.js

602 lines (489 loc) 16.7 kB
// 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);