├── .gitignore ├── index.js ├── test └── index.js ├── examples └── usage.js ├── package.json ├── chat ├── net │ ├── message.js │ ├── client.js │ └── packet.js ├── utils │ └── utils.js └── room.js ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var ChatRoom = require('./chat/room'); 3 | 4 | module.exports = { 5 | ChatRoom: ChatRoom 6 | }; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var should = require('chai').should(); 2 | var douyu = require('../index'); 3 | 4 | describe("#initialize", function(){ 5 | it('initializes correctly', function(){ 6 | new douyu.ChatRoom("584854"); 7 | }); 8 | }); 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/usage.js: -------------------------------------------------------------------------------- 1 | // Douyu API Usages 2 | 3 | // Import library 4 | var douyu = require('douyu'); 5 | 6 | // Initialize Room entity 7 | var roomID = "424559"; 8 | var room = new douyu.ChatRoom(roomID); 9 | 10 | // System level events handler 11 | room.on('connect', function(message){ 12 | console.log('DouyuTV ChatRoom #' + roomID + ' connected.'); 13 | }); 14 | room.on('error', function(error){ 15 | console.error('Error: ' + error.toString()); 16 | }); 17 | room.on('close', function(hasError){ 18 | console.log('DouyuTV ChatRoom #' + roomID + ' disconnected' + hasError ? ' because of error.' : '.'); 19 | }); 20 | 21 | // Chat server events 22 | room.on('chatmsg', function(message){ 23 | console.log('[' + message.nn + ']: ' + message.txt); 24 | }); 25 | 26 | // Knock, knock ... 27 | room.open(); 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "douyu", 3 | "version": "0.1.0", 4 | "description": "NodeJS Wrapper for DouyuTV APIs", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --reporter spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/yingnansong/douyujs.git" 12 | }, 13 | "keywords": [ 14 | "douyu" 15 | ], 16 | "author": "Yingnan Song", 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/yingnansong/douyujs/blob/master/LICENSE" 21 | } 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/yingnansong/douyujs/issues" 25 | }, 26 | "homepage": "https://github.com/yingnansong/douyujs#readme", 27 | "devDependencies": { 28 | "chai": "^3.5.0", 29 | "mocha": "^2.5.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chat/net/message.js: -------------------------------------------------------------------------------- 1 | 2 | var utils = require('../utils/utils'); 3 | 4 | function Message(messageBody) { 5 | this.body = messageBody; 6 | this.bodySize = 0; 7 | } 8 | 9 | Message.prototype.toString = function(){ 10 | return utils.serialize(this.body) + '\0'; 11 | }; 12 | 13 | Message.prototype.attr = function(fieldName){ 14 | if(!this.body) { 15 | return null; 16 | } 17 | return this.body[fieldName]; 18 | }; 19 | 20 | /* 21 | * Find message from string buffer 22 | */ 23 | Message.sniff = function(buffer){ 24 | 25 | // console.log('[MessageSniff] Sniffing message'); 26 | 27 | if(!buffer || buffer.length <= 0) { 28 | return null; 29 | } 30 | 31 | var bodies = buffer.split('\0'); 32 | if(bodies.length <= 1) { 33 | return; 34 | } 35 | 36 | return Message.fromRaw(bodies[0]); 37 | } 38 | 39 | Message.fromRaw = function(raw){ 40 | 41 | if(!raw || raw.length <= 0) { 42 | return null; 43 | } 44 | 45 | var result = new Message(utils.deserialize(raw)); 46 | result.bodySize = raw.length; 47 | return result; 48 | 49 | }; 50 | 51 | module.exports = Message; 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DouyuTV API Wrapper 2 | =================== 3 | 4 | NodeJS Wrapper for DouyuTV APIs 5 | 6 | Supported Platforms 7 | ------------------- 8 | 9 | This library has been tested with: 10 | 11 | - IO.js v2.5.0 12 | 13 | Installation 14 | ------------ 15 | 16 | npm install douyu 17 | 18 | API Usage 19 | --------- 20 | 21 | // Import library 22 | var douyu = require('douyu'); 23 | 24 | // Initialize Room entity 25 | var roomID = "424559"; 26 | var room = new douyu.ChatRoom(roomID); 27 | 28 | // System level events handler 29 | room.on('connect', function(message){ 30 | console.log('DouyuTV ChatRoom #' + roomID + ' connected.'); 31 | }); 32 | room.on('error', function(error){ 33 | console.error('Error: ' + error.toString()); 34 | }); 35 | room.on('close', function(hasError){ 36 | console.log('DouyuTV ChatRoom #' + roomID + ' disconnected' + hasError ? ' because of error.' : '.'); 37 | }); 38 | 39 | // Chat server events 40 | room.on('chatmsg', function(message){ 41 | console.log('[' + message.nn + ']: ' + message.txt); 42 | }); 43 | 44 | // Knock, knock ... 45 | room.open(); 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yingnan Song 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chat/utils/utils.js: -------------------------------------------------------------------------------- 1 | 2 | var utils = { 3 | 4 | replaceAll: function(str, search, replacement) { 5 | if(str == null || str.length <= 0) { 6 | return ""; 7 | } 8 | return str.replace(new RegExp(search, 'g'), replacement); 9 | }, 10 | 11 | escape: function(field) { 12 | if(!field || field.length <= 0) { 13 | return ""; 14 | } 15 | field = "" + field 16 | field = utils.replaceAll(field, "@", "@A"); 17 | field = utils.replaceAll(field, "/", "@S"); 18 | return field; 19 | }, 20 | 21 | unescape: function(field) { 22 | if(!field || field.length <= 0) { 23 | return ""; 24 | } 25 | field = "" + field 26 | field = utils.replaceAll(field, "@S", "/"); 27 | field = utils.replaceAll(field, "@A", "@"); 28 | return field; 29 | }, 30 | 31 | serialize: function serialize(data) { 32 | var kvPairs = []; 33 | for(var key in data) { 34 | if(!data.hasOwnProperty(key)) { 35 | continue; 36 | } 37 | 38 | kvPairs.push(utils.escape(key) + "@=" + utils.escape(data[key])); 39 | } 40 | return kvPairs.join("/") + "/"; 41 | }, 42 | 43 | deserialize: function(raw) { 44 | var result = {}; 45 | var kvPairs = raw.split("/"); 46 | kvPairs.forEach(function(kvStr){ 47 | var kv = kvStr.split("@="); 48 | if(kv.length != 2) { 49 | return; 50 | } 51 | var key = utils.unescape(kv[0]); 52 | var value = utils.unescape(kv[1]); 53 | if(value.indexOf("@=") >= 0) { 54 | value = utils.deserialize(value); 55 | } 56 | result[key] = value; 57 | }); 58 | return result; 59 | }, 60 | }; 61 | 62 | module.exports = utils; -------------------------------------------------------------------------------- /chat/room.js: -------------------------------------------------------------------------------- 1 | 2 | var events = require('events'); 3 | var util = require('util'); 4 | var Client = require('./net/client'); 5 | 6 | function Room(roomID){ 7 | events.EventEmitter.call(this); 8 | this.roomID = roomID; 9 | this.client = null; 10 | } 11 | util.inherits(Room, events.EventEmitter); 12 | 13 | Room.prototype.open = function(){ 14 | this.client = new Client(); 15 | this.client.on('connect', this.onConnected.bind(this)); 16 | this.client.on('message', this.onMessage.bind(this)); 17 | this.client.connect(); 18 | } 19 | 20 | Room.prototype.keepAlive = function(){ 21 | this.client.send({ 22 | type: 'keepalive', 23 | tick: Math.floor(new Date().getTime() * 0.001) 24 | }); 25 | } 26 | 27 | Room.prototype.onConnected = function(){ 28 | 29 | var self = this; 30 | 31 | // console.log('[Room] Client connected. Sending LOGIN request.'); 32 | 33 | // Send LOGIN request 34 | this.client.send({ 35 | type: 'loginreq', 36 | roomid: this.roomID 37 | }); 38 | 39 | // Send KEEP_ALIVE request every 45 seconds 40 | setInterval(function(){ 41 | self.keepAlive(); 42 | }, 45000); 43 | 44 | this.emit('connect'); 45 | 46 | this.client.on('error', this.onError.bind(this)); 47 | this.client.on('close', this.onClosed.bind(this)); 48 | } 49 | 50 | 51 | Room.prototype.onError = function(err){ 52 | console.error('[Room] Error: ' + err.toString()); 53 | this.emit('error', err); 54 | } 55 | 56 | Room.prototype.onClosed = function(had_error){ 57 | this.emit('close', had_error); 58 | } 59 | 60 | Room.prototype.onMessage = function(message){ 61 | 62 | var messageType = message.attr('type'); 63 | if(!messageType) { 64 | console.error('[Room] Cannot get type of message'); 65 | return; 66 | } 67 | 68 | if(messageType == 'loginres') { 69 | // console.log('[Room] LOGIN response received. Sending JOIN_GROUP request.'); 70 | 71 | // Send JOIN_GROUP request 72 | this.client.send({ 73 | type: 'joingroup', 74 | rid: this.roomID, 75 | gid: -9999 76 | }); 77 | } 78 | 79 | // console.log('[Room] Received message: ' + JSON.stringify(message.body)); 80 | this.emit(messageType, message.body); 81 | } 82 | 83 | module.exports = Room; 84 | 85 | -------------------------------------------------------------------------------- /chat/net/client.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | var util = require('util'); 4 | var Packet = require('./packet'); 5 | var Message = require('./message'); 6 | 7 | var host = 'openbarrage.douyutv.com'; 8 | var port = 8601; 9 | 10 | function Client() { 11 | events.EventEmitter.call(this); 12 | this.init(); 13 | } 14 | util.inherits(Client, events.EventEmitter); 15 | 16 | Client.prototype.init = function(){ 17 | this.s = null; 18 | this.rawBuffer = ''; 19 | this.messageBuffer = ''; 20 | }; 21 | 22 | Client.prototype.connect = function(){ 23 | this.s = new net.Socket(); 24 | this.s.setEncoding('hex'); 25 | this.s.connect(port, host, this.onConnected.bind(this)); 26 | }; 27 | 28 | Client.prototype.onConnected = function(){ 29 | // console.log('[Client] Socket connected'); 30 | this.s.on('data', this.onData.bind(this)); 31 | this.s.on('error', this.onError.bind(this)); 32 | this.s.on('close', this.onClosed.bind(this)); 33 | this.emit('connect'); 34 | }; 35 | 36 | Client.prototype.onError = function(err){ 37 | this.emit('error', err); 38 | } 39 | 40 | Client.prototype.onClosed = function(had_error){ 41 | this.emit('close', had_error); 42 | } 43 | 44 | Client.prototype.onData = function(data){ 45 | 46 | // console.log('[Client] Received socket data. Length: ' + data.length); 47 | // console.log('[Client] ' + data); 48 | 49 | if(!data) { 50 | return; 51 | } 52 | 53 | this.rawBuffer += data; 54 | 55 | while(true) { 56 | 57 | // Sniff for network packets 58 | var packet = null; 59 | try { 60 | packet = Packet.sniff(this.rawBuffer); 61 | } catch(e) { 62 | this.emit('error', e); 63 | this.s.destroy(); 64 | return; 65 | } 66 | 67 | if(!packet) { 68 | break; 69 | } 70 | 71 | // Move packet data out of rawBuffer 72 | var bytesConsumed = packet.getPacketFrameSize(); 73 | // console.log('[Client] Packet got. Frame size: ' + bytesConsumed); 74 | this.rawBuffer = this.rawBuffer.substr(bytesConsumed); 75 | 76 | this.messageBuffer += packet.body; 77 | while(true) { 78 | 79 | // Sniff for messages 80 | var message = Message.sniff(this.messageBuffer); 81 | if(!message) { 82 | break; 83 | } 84 | 85 | // Move message data out of messageBuffer 86 | this.messageBuffer = this.messageBuffer.substr(message.bodySize + 1); 87 | 88 | this.emit('message', message); 89 | } 90 | } 91 | 92 | }; 93 | 94 | Client.prototype.send = function(message){ 95 | return this.s.write(Packet.fromMessage(new Message(message)).toRaw()); 96 | }; 97 | 98 | module.exports = Client; 99 | 100 | -------------------------------------------------------------------------------- /chat/net/packet.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function Packet(packetBody, frameLength) { 4 | this.body = packetBody; 5 | this.frameLength = frameLength; 6 | } 7 | 8 | Packet.frameHeaderLength = 4; 9 | Packet.headerLength = 8; 10 | 11 | /* 12 | * Returns the size of hex string consumed by this packet 13 | */ 14 | Packet.prototype.getPacketFrameSize = function(){ 15 | return 2 * (this.frameLength + 4); 16 | }; 17 | 18 | Packet.prototype.toRaw = function(){ 19 | 20 | if(!this.body || this.body.length <= 0) { 21 | return null; 22 | } 23 | 24 | var bufferHeader = new Buffer(Packet.frameHeaderLength + Packet.headerLength); 25 | var bufferBody = new Buffer(this.body, 'utf8'); 26 | var totalLength = bufferBody.length + Packet.frameHeaderLength + Packet.headerLength; 27 | bufferHeader.writeInt32LE(totalLength - Packet.frameHeaderLength, 0); 28 | bufferHeader.writeInt32LE(totalLength - Packet.frameHeaderLength, 4); 29 | bufferHeader.writeInt16LE(689, 8); 30 | bufferHeader.writeInt16LE(0, 10); 31 | return Buffer.concat([bufferHeader, bufferBody], totalLength); 32 | 33 | }; 34 | 35 | /* 36 | * Find packet from hex string 37 | */ 38 | Packet.sniff = function(bufferHex){ 39 | 40 | // console.log('[PacketSniff] Sniffing packet'); 41 | 42 | var bytesAvailable = bufferHex.length / 2; 43 | 44 | if(bytesAvailable <= Packet.frameHeaderLength + Packet.headerLength) { 45 | return null; 46 | } 47 | 48 | var buffer = new Buffer(bufferHex, 'hex'); 49 | // console.log('Converted to buffer of size: ' + buffer.length); 50 | var packetLength = buffer.readInt32LE(0); 51 | var packetLength2 = buffer.readInt32LE(Packet.frameHeaderLength); 52 | var packetType = buffer.readInt16LE(Packet.frameHeaderLength + 4); 53 | var encrypt = buffer.readInt8(Packet.frameHeaderLength + 6); 54 | var reserved = buffer.readInt8(Packet.frameHeaderLength + 7); 55 | var body = buffer.toString('utf8', Packet.frameHeaderLength + Packet.headerLength) 56 | 57 | // console.log('[PacketSniff] packetLength: ' + packetLength); 58 | // console.log('[PacketSniff] packetLength2: ' + packetLength2); 59 | // console.log('[PacketSniff] packetType: ' + packetType); 60 | // console.log('[PacketSniff] encrypt: ' + encrypt); 61 | // console.log('[PacketSniff] reserved: ' + reserved); 62 | // console.log('[PacketSniff] body: ' + body); 63 | 64 | if(packetLength <= 0 || packetLength2 <= 0) { 65 | console.warn('[PacketSniff] Invalid packet lengths'); 66 | throw new Error('packet_len_invalid'); 67 | return null; 68 | } 69 | 70 | if(packetLength != packetLength2) { 71 | console.warn('[PacketSniff] Mismatched packet lengths'); 72 | throw new Error('packet_len_mismatch'); 73 | return null; 74 | } 75 | 76 | if(bytesAvailable >= packetLength + 4) { 77 | 78 | if(bytesAvailable > packetLength + 4) { 79 | body = buffer.toString('utf8', Packet.frameHeaderLength + Packet.headerLength, packetLength + 4); 80 | // console.log('[PacketSniff] body trimmed: ' + body); 81 | } 82 | 83 | return new Packet(body, packetLength); 84 | } 85 | 86 | return null; 87 | }; 88 | 89 | Packet.fromMessage = function(message){ 90 | return new Packet(message.toString()); 91 | }; 92 | 93 | module.exports = Packet; 94 | 95 | --------------------------------------------------------------------------------