├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .AppleDouble 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | plugbotapi 2 | ======= 3 | 4 | An API for creating bots on plug.dj. 5 | 6 | This API uses the official Plug API, so it should be much more stable than the old plugapi. 7 | 8 | 9 | ## Installation 10 | This API requires phantomjs version 2.0 or greater. The websockets stack in 1.9 and lower does not cooperate with plug.dj, therefore 2.0 MUST be used. 11 | 12 | You can find information on how to compile and install 2.0 here: https://groups.google.com/forum/#!msg/phantomjs/iYyH6hF_xoQ/fTmVD8-NcFIJ 13 | 14 | ``` 15 | npm install plugbotapi 16 | ``` 17 | 18 | ## How to use 19 | 20 | ``` 21 | var PlugBotAPI = require('plugbotapi'); 22 | 23 | var plugbotapi = new PlugBotAPI({ 24 | email: 'user@domain.com', 25 | password: 'xxxxx' 26 | }); 27 | 28 | 29 | plugbotapi.on('roomJoin', function() { 30 | console.log("Connected!"); 31 | 32 | plugbotapi.chat('Hello World'); 33 | 34 | plugbotapi.getUsers(function(users) { 35 | console.log("Number of users in the room: " + users.length); 36 | }); 37 | 38 | plugbotapi.hasPermission('2492d8ab9fef78dad5620a300', 'API.ROLE.NONE', function(result) { 39 | console.log("permission: ", result); 40 | }); 41 | }); 42 | 43 | // A few sample events 44 | plugbotapi.on('chat', function(data) { 45 | console.log("got chat: ", data); 46 | }); 47 | 48 | plugbotapi.on('djAdvance', function(data) { 49 | console.log("dj advance: ", data); 50 | }); 51 | 52 | plugbotapi.on('voteUpdate', function(data) { 53 | console.log("vote update: ", data); 54 | }); 55 | 56 | var room = 'some-room'; 57 | plugbotapi.connect(room); 58 | ``` 59 | 60 | ### Events 61 | PlugBotAPI emits the following events. For documentation please refer to the official [Plug.dj API](http://support.plug.dj/hc/en-us/articles/201687377-Documentation#chat). 62 | 63 | * advance 64 | * chat 65 | * grabUpdate 66 | * historyUpdate 67 | * modSkip 68 | * scoreUpdate 69 | * userJoin 70 | * userLeave 71 | * userSkip 72 | * voteUpdate 73 | * waitListUpdate 74 | 75 | Also emitted is the following: 76 | 77 | * roomJoin: emitted when the bot has completed joining the room and is ready to send actions/receive events. 78 | * invalidLogin: emitted when the bot is unable to login, possibly due to an invalid auth cookie. 79 | * unableToConnect: emitted after the bot tries 15 times to connect but is unable to 80 | 81 | ## Actions 82 | 83 | All data returned by actions is returned inside the callback that must be specified. 84 | 85 | ### connect: (roomName) 86 | 87 | Connects to plug.dj and joins the specified room. 88 | 89 | ### chat: (message, callback) 90 | 91 | Sends _message_ in chat. 92 | 93 | ### getUsers: (callback) 94 | 95 | Returns (in the callback) an array of user objects for every user in the room. 96 | 97 | ### getWaitList: (callback) 98 | 99 | Returns an Array of user objects of users currently on the wait list. 100 | 101 | ### getUser: (userid, callback) 102 | 103 | Returns the user object of a specific user. If you do not pass a userID, it returns the user object of the bot. 104 | 105 | ### getDJ: (callback) 106 | 107 | Returns a user object of the current DJ. If there is no DJ, returns undefined. 108 | 109 | ### getAudience: (callback) 110 | 111 | Returns an Array of user objects of all the users in the audience (not including the DJ). 112 | 113 | ### getStaff: (callback) 114 | 115 | Returns an Array of user objects of the room's staff members that are currently in the room. 116 | 117 | ### getAdmins: (callback) 118 | 119 | Returns an Array of user objects of the Admins currently in the room. 120 | 121 | ### getAmbassadors: (callback) 122 | 123 | Returns an Array of user objects of the Ambassadors currently in the room. 124 | 125 | ### getHost: (callback) 126 | 127 | Returns the user object of the room host if they are currently in the room, undefined otherwise. 128 | 129 | ### getMedia: (callback) 130 | 131 | Returns the media object of the current playing media. 132 | 133 | ### getRoomScore: (callback) 134 | 135 | Returns a room score object with the properties positive, negative, curates, and score. 136 | 137 | ### getScore: (callback) 138 | 139 | Returns a room score object with the properties positive, negative, curates, and score. 140 | 141 | ### getHistory: (callback) 142 | 143 | Returns an Array of history objects of the Room History (once it's been loaded). 144 | 145 | ### hasPermission: (userid, role, callback) 146 | 147 | Returns a Boolean whether the userID passed has permission of the passed role. If you pass undefined or null for userID, it checks the permission of the logged in user. Pass an API.ROLE constant: 148 | 149 | * API.ROLE.NONE 150 | * API.ROLE.RESIDENTDJ 151 | * API.ROLE.BOUNCER 152 | * API.ROLE.MANAGER 153 | * API.ROLE.COHOST 154 | * API.ROLE.HOST 155 | * API.ROLE.AMBASSADOR 156 | * API.ROLE.ADMIN 157 | 158 | ### djJoin: (callback) 159 | 160 | Joins the booth or the wait list if the booth is full. 161 | 162 | ### djLeave: (callback) 163 | 164 | Leaves the booth or wait list. 165 | 166 | ### getWaitListPosition: (userid, callback) 167 | 168 | If the userID is in the wait list, it returns their position (0 index - so 0 means first position). Returns -1 if they're not in the wait list. If you do not pass a userID, it uses the logged in user ID. 169 | 170 | ### setStatus: (value, callback) 171 | 172 | Sets the user status (Available, AFK, Working, Sleeping). Pass an API.STATUS constant: 173 | 174 | * API.STATUS.AVAILABLE 175 | * API.STATUS.AFK 176 | * API.STATUS.WORKING 177 | * API.STATUS.GAMING 178 | 179 | Example: 180 | 181 | ``` 182 | plugbotapi.moderateBanUser('xxxxx', plugbotapi.API.STATUS.GAMING, function() { 183 | // Done 184 | }); 185 | ``` 186 | 187 | ### getNextMedia: (callback) 188 | 189 | Returns the user's queued up media. This is an object with two properties, media and inHistory. media is the media object and inHistory is a Boolean if the media is in the room history. 190 | 191 | ### getTimeElapsed: (callback) 192 | 193 | Returns how much time has elapsed for the currently playing media. If there is no media, it will return 0. 194 | 195 | ### getTimeRemaining: (callback) 196 | 197 | Returns how much time is remaining for the currently playing media. If there is no media, it will return 0. 198 | 199 | ### moderateForceSkip: (callback) 200 | 201 | Force skip the current DJ. 202 | 203 | ### moderateAddDJ: (userid, callback) 204 | 205 | Adds a user to the dj booth or wait list by passing that user's id. Users who do not have an active playlist with one item in it cannot be added. 206 | 207 | ### moderateRemoveDJ: (userid, callback) 208 | 209 | Removes a DJ from the booth or wait list by passing that user's id. 210 | 211 | ### moderateBanUser: (userid, [reason], [duration], [callback]) 212 | 213 | Bans a user from the room. 214 | 215 | Reason is an integer describing why the user is banned (but not really needed; defaults to 4). 216 | 217 | If the bot is only a bouncer, permanent bans are not available. Specify the duration with one of the following constants: 218 | 219 | * API.BAN.HOUR 220 | * API.BAN.DAY 221 | * API.BAN.PERMA 222 | 223 | If you do not specify a duration, a permanent ban will be the default unless the bot is a bouncer, in which case the ban will be for an hour. 224 | 225 | Example: 226 | 227 | ``` 228 | plugbotapi.moderateBanUser('xxxxx', plugbotapi.API.BAN.DAY, function() { 229 | // Done 230 | }); 231 | ``` 232 | 233 | ### moderateUnbanUser: (userid, callback) 234 | 235 | If the bot is a manager, unbans a user. 236 | 237 | ### moderateDeleteChat: (chatid, callback) 238 | 239 | Delete a chat message by its chatid. 240 | 241 | ### moderateSetRole: (userid, permission, callback) 242 | 243 | If the bot is a manager or above, this sets another user's role. Use one of the following constants: 244 | 245 | * API.ROLE.NONE 246 | * API.ROLE.RESIDENTDJ 247 | * API.ROLE.BOUNCER 248 | * API.ROLE.MANAGER 249 | * API.ROLE.COHOST 250 | 251 | Example: 252 | ``` 253 | plugbotapi.moderateSetRole('xxxx', plugbotapi.API.ROLE.RESIDENTDJ); 254 | ``` 255 | 256 | ### moderateMoveDJ: (userid, position, callback) 257 | 258 | If the bot is a manager or above, move the specified user in the waitlist. Pass position 1 for the top of the list. 259 | 260 | ### moderateLockWaitList: (locked, removeAll, callback) 261 | 262 | If the bot is a manager, lock/unlock the waitlist. 263 | 264 | ### woot: () 265 | 266 | Makes the bot woot the currently playing track. NOTE:There is no way to woot via the official Plug API, so this is and `meh` are sort of a hack - they appear to sporadically stop working. 267 | 268 | ### meh: () 269 | 270 | MAkes the bot meh the currently playing track. Same note as woot applies. 271 | 272 | 273 | ## Running multiple bots 274 | 275 | If you want to run more than one bot at a time, you will have to specify an alternate port for phantomjs to run on for each bot (except for one, which will use the default of 12300): 276 | 277 | ``` 278 | var PlugBotAPI = require('./plugbotapi'); 279 | var plugbotapi = new PlugBotAPI(h); 280 | plugbotapi.setPhantomPort(12301); 281 | ``` 282 | 283 | ## Debugging 284 | 285 | If you run into problems and would like some more visibility into what the virtual browser is doing, try this: 286 | 287 | ``` 288 | //plugbotapi.debug.SHOWAPI = false; // set this to false to hide the official Plug API events and actions that the virtual browser is sending and receiving. (default true) 289 | //plugbotapi.debug.SHOWOTHER = false; // set this to false to hide any other messages the virtual browser is logging to its console. (default true) 290 | 291 | // The API emits the 'debug' event: 292 | plugbotapi.on('debug', function(text) { 293 | console.log(text); 294 | }); 295 | ``` 296 | 297 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var __extends = 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; }; 3 | var phantom = require('phantom'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var __hasProp = {}.hasOwnProperty; 6 | var http = require('http'); 7 | var net = require('net'); 8 | var util = require('util'); 9 | 10 | PlugBotAPI = (function(_super) { 11 | 12 | __extends(PlugBotAPI, _super); 13 | 14 | function PlugBotAPI(creds) { 15 | this.creds = creds; 16 | this.page = false; 17 | this.pageReady = false; 18 | this.loggedin = false; 19 | this.cookies = {}; 20 | this.ph = false; 21 | this.autoRetryLogin = true; 22 | this.phantomPort = 12300; // default phantom port 23 | this.API = {}; // Plug constants 24 | this.debug.plugbotapi = this; 25 | } 26 | 27 | PlugBotAPI.prototype.debug = { 28 | SHOWAPI: true, 29 | SHOWOTHER: true, 30 | logapi: function() { 31 | if(this.SHOWAPI === true) 32 | this.log(util.format.apply(null, arguments)); 33 | }, 34 | logother: function() { 35 | if(this.SHOWOTHER === true) 36 | this.log(util.format.apply(null, arguments)); 37 | }, 38 | log: function(text) { 39 | this.plugbotapi.emit('debug', text); 40 | } 41 | }; 42 | 43 | PlugBotAPI.prototype.setPhantomPort = function(port) { 44 | this.phantomPort = port; 45 | }; 46 | 47 | PlugBotAPI.prototype.apiCall = function(call, arg, callback) { 48 | if (this.pageReady === true) { 49 | this.page.set('onError', function (msg, trace) { 50 | console.log("ERROR: ", msg); 51 | console.log("ERROR TRACE: ", trace); 52 | }); 53 | this.page.evaluate(function(obj) { 54 | var args = ''; 55 | if (obj.arg != null) { 56 | if (!Array.isArray(obj.arg)) 57 | obj.arg = [obj.arg]; 58 | for(var i=0;i= 2.0.0. You are running version " + version + "."); 89 | ph.exit(); 90 | process.exit(1); 91 | } 92 | }); 93 | 94 | if(typeof callback == 'function') 95 | callback(ph); 96 | }); 97 | }; 98 | 99 | PlugBotAPI.prototype.openPage = function(room) { 100 | var _this = this; 101 | this.ph.createPage(function(page) { 102 | for(var key in _this.cookies) { 103 | var domain = key.match(/^ajs/) ? '.plug.dj' : 'plug.dj'; 104 | var cookie = { 105 | name: key, 106 | value: _this.cookies[key], 107 | domain: domain, 108 | path: '/', 109 | httponly: true, 110 | secure: false, 111 | expires: (new Date().getTime() + (1000*60*60*24*7)) 112 | }; 113 | _this.ph.addCookie(cookie); 114 | } 115 | 116 | page.open('http://plug.dj/' + room, function(status) { 117 | // Check for invalid login 118 | page.evaluate(function() { 119 | return $('.existing button').length > 0; 120 | }, function(loginbutton) { 121 | if(loginbutton) { 122 | _this.emit('invalidLogin'); 123 | if(_this.autoRetryLogin === true && _this.creds.email != undefined && _this.creds.password != undefined) { 124 | _this.login(_this.creds, function () { 125 | _this.openPage(room); 126 | }); 127 | } 128 | } else { 129 | var tries = 0; 130 | var loadInterval = setInterval(function() { 131 | page.evaluate(function() { 132 | return $('.app-header').length > 0; 133 | }, function(found) { 134 | tries++; 135 | if(found) { 136 | _this.pageReady = true; 137 | 138 | page.set('onConsoleMessage', function(msg) { 139 | if(msg.match(/^API/)) { 140 | _this.debug.logapi(msg); 141 | } else { 142 | _this.debug.logother(msg); 143 | } 144 | if (msg.match(/^error/) && _this.pageReady === false) { 145 | _this.emit('connectionError', msg); 146 | } 147 | 148 | 149 | var apiRegexp = /^API.([^:]+):(.*)/g; 150 | var matches = apiRegexp.exec(msg); 151 | if (matches != null) { 152 | //console.log(matches[1] + ":" + matches[2]); 153 | // matches[1] = which event 154 | // matches[2] = json representation of data 155 | 156 | // Rename event to be camelCase 157 | var event = matches[1].toLowerCase().replace(/_([a-z])/g, function(a) { 158 | return a.replace('_', '').toUpperCase(); 159 | }); 160 | var data = JSON.parse(matches[2]); 161 | // emit this event out to the PlugBotAPI, for a bot to receive 162 | _this.emit(event, data); 163 | } 164 | }); 165 | 166 | // Setup events 167 | page.evaluate(function() { 168 | // First, get rid of the playback div so we don't needlessly use up all that bandwidth 169 | $('#playback').remove(); 170 | // Might as well get rid of these, perhaps lower cpu usage? 171 | $('#audience').remove(); 172 | $('#dj-booth').remove(); 173 | 174 | var events = ['ADVANCE', 'CHAT', 'GRAB_UPDATE', 'HISTORY_UPDATE', 'MOD_SKIP', 175 | 'SCORE_UPDATE', 'USER_JOIN', 'USER_LEAVE', 'USER_SKIP', 'VOTE_UPDATE', 'WAIT_LIST_UPDATE']; 176 | for (var i in events) { 177 | var thisEvent = events[i]; 178 | // First, let's turn off any listeners to the Plug API, in case we get disconnected and reconnected - we don't want these events to be duplicated. 179 | var line = 'API.off(API.' + thisEvent + ');'; 180 | eval(line); 181 | line = 'API.on(API.' + thisEvent + ', function(data) { data.fromID = data.uid; data.chatID = data.cid; console.log(\'API.' + thisEvent + ':\' + JSON.stringify(data)); }); '; 182 | eval(line); 183 | } 184 | return { 185 | ROLE: API.ROLE, 186 | STATUS: API.STATUS, 187 | BAN: API.BAN, 188 | MUTE: API.MUTE 189 | }; 190 | }, function (result) { 191 | _this.API.ROLE = result.ROLE; 192 | _this.API.STATUS = result.STATUS; 193 | _this.API.BAN = result.BAN; 194 | _this.API.MUTE = result.MUTE; 195 | setTimeout(function () { 196 | _this.emit('roomJoin'); 197 | }, 1000); 198 | 199 | }); 200 | 201 | 202 | 203 | clearInterval(loadInterval); 204 | } else if(tries > 15) { 205 | clearInterval(loadInterval); 206 | console.log("Sorry, I couldn't seem to connect."); 207 | _this.emit('unableToConnect'); 208 | } 209 | }); 210 | }, 2000); 211 | } 212 | }); 213 | 214 | 215 | }); 216 | 217 | _this.page = page; 218 | }); 219 | }; 220 | 221 | PlugBotAPI.prototype.connect = function(room) { 222 | var _this = this; 223 | if(this.ph === false) { 224 | // Need to create page 225 | this.createPage(room, function(ph) { 226 | _this.ph = ph; 227 | _this.connect(room); 228 | }); 229 | } else { 230 | if(this.loggedin === false && this.cookies.amplitude_id == undefined) { 231 | this.login(this.creds, function () { 232 | _this.openPage(room); 233 | }); 234 | } else { 235 | this.openPage(room); 236 | } 237 | 238 | } 239 | }; 240 | 241 | PlugBotAPI.prototype.login = function(creds, callback) { 242 | console.log("Logging in."); 243 | var _this = this; 244 | _this.ph.createPage(function(page) { 245 | _this.ph.addCookie('usr', _this.auth, 'plug.dj'); 246 | 247 | page.set('onError', function(msg, trace) { 248 | console.log("Error: " + msg + ": ", trace); 249 | }); 250 | 251 | page.open('http://plug.dj/', function(status) { 252 | setTimeout(function() { 253 | page.evaluate(function() { 254 | // console.log("finding login button"); 255 | if($('.existing').length > 0) { 256 | // console.log("clicking ", $('.existing button')); 257 | $('.existing button').click(); 258 | return true; 259 | } else 260 | return false; 261 | }, function(foundLoginButton) { 262 | if(foundLoginButton) { 263 | page.evaluate(function(creds) { 264 | $('#email').val(creds.email); 265 | $('#password').val(creds.password); 266 | $('.email-login button').click(); 267 | return true; 268 | }, function(result) { 269 | _this.loggedin = true; 270 | setTimeout(function() { 271 | page.evaluate(function() { 272 | return document.cookie; 273 | }, function(cookie) { 274 | var els = cookie.split('; '); 275 | _this.cookies = {}; 276 | for(var i=0;i", 11 | "main": "index.js", 12 | "directories": { 13 | "lib": "lib" 14 | }, 15 | "engines": { 16 | "node": "*" 17 | }, 18 | "dependencies" : { 19 | "phantom": "*", 20 | "plug-dj-login": "*", 21 | "net": "*" 22 | }, 23 | "devDependencies": {}, 24 | "optionalDependencies": { 25 | } 26 | } 27 | --------------------------------------------------------------------------------