├── .eslintrc.json ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── index.js ├── lib ├── actionHandler.js ├── collections │ ├── baseCollection.js │ ├── chatCollection.js │ ├── queueCollection.js │ └── userCollection.js ├── data │ ├── endpoints.js │ ├── events.js │ └── roles.js ├── errors │ ├── error.js │ └── requestError.js ├── eventHandler.js ├── models │ ├── mediaModel.js │ ├── playModel.js │ ├── roomModel.js │ ├── selfModel.js │ └── userModel.js ├── requestHandler.js ├── socketHandler.js └── utils.js └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "rules": { 7 | "comma-dangle": [2, "never"], 8 | "no-cond-assign": [2, "except-parens"], 9 | "no-console": 1, 10 | "no-constant-condition": 2, 11 | "no-control-regex": 2, 12 | "no-debugger": 2, 13 | "no-dupe-args": 2, 14 | "no-dupe-keys": 2, 15 | "no-duplicate-case": 2, 16 | "no-empty-character-class": 2, 17 | "no-empty": 2, 18 | "no-ex-assign": 2, 19 | "no-extra-boolean-cast": 2, 20 | "no-extra-parens": [2, "all", {"conditionalAssign": false}], 21 | "no-extra-semi": 2, 22 | "no-func-assign": 2, 23 | "no-inner-declarations": 2, 24 | "no-invalid-regexp": 2, 25 | "no-irregular-whitespace": 2, 26 | "no-negated-in-lhs": 2, 27 | "no-obj-calls": 2, 28 | "no-regex-spaces": 2, 29 | "no-sparse-arrays": 2, 30 | "no-unexpected-multiline": 2, 31 | "no-unreachable": 2, 32 | "use-isnan": 2, 33 | "valid-jsdoc": 2, 34 | "valid-typeof": 2, 35 | 36 | "accessor-pairs": 2, 37 | "array-callback-return": 1, 38 | "block-scoped-var": 2, 39 | "complexity": 0, 40 | "consistent-return": 0, 41 | "curly": [2, "multi-line"], 42 | "default-case": 2, 43 | "dot-location": [2, "property"], 44 | "dot-notation": 1, 45 | "eqeqeq": [2, "allow-null"], 46 | "guard-for-in": 2, 47 | "no-alert": 2, 48 | "no-caller": 2, 49 | "no-case-declarations": 1, 50 | "no-div-regex": 2, 51 | "no-else-return": 0, 52 | "no-empty-function": 2, 53 | "no-empty-pattern": 2, 54 | "no-eq-null": 0, 55 | "no-eval": 2, 56 | "no-extend-native": 2, 57 | "no-extra-bind": 2, 58 | "no-extra-label": 2, 59 | "no-fallthrough": 2, 60 | "no-floating-decimal": 2, 61 | "no-implicit-coercion": 2, 62 | "no-implicit-globals": 2, 63 | "no-implied-eval": 2, 64 | "no-invalid-this": 0, 65 | "no-iterator": 2, 66 | "no-labels": 2, 67 | "no-lone-blocks": 2, 68 | "no-loop-func": 2, 69 | "no-magic-numbers": 0, 70 | "no-multi-spaces": 2, 71 | "no-multi-str": 2, 72 | "no-native-reassign": 2, 73 | "no-new-func": 2, 74 | "no-new-wrappers": 2, 75 | "no-new": 1, 76 | "no-octal-escape": 2, 77 | "no-octal": 2, 78 | "no-param-reassign": 0, 79 | "no-process-env": 0, 80 | "no-proto": 2, 81 | "no-redeclare": 2, 82 | "no-return-assign": 2, 83 | "no-script-url": 2, 84 | "no-self-assign": 2, 85 | "no-self-compare": 2, 86 | "no-sequences": 2, 87 | "no-throw-literal": 2, 88 | "no-unmodified-loop-condition": 2, 89 | "no-unused-expressions": 2, 90 | "no-unused-labels": 2, 91 | "no-useless-call": 2, 92 | "no-useless-concat": 2, 93 | "no-void": 2, 94 | "no-warning-comments": 1, 95 | "no-with": 2, 96 | "radix": 2, 97 | "vars-on-top": 0, 98 | "wrap-iife": [2, "inside"], 99 | "yoda": 2, 100 | 101 | "strict": [2, "global"], 102 | 103 | "init-declarations": 0, 104 | "no-catch-shadow": 0, 105 | "no-delete-var": 2, 106 | "no-label-var": 2, 107 | "no-shadow-restricted-names": 2, 108 | "no-shadow": 0, 109 | "no-undef-init": 2, 110 | "no-undef": 2, 111 | "no-undefined": 0, 112 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 113 | "no-use-before-define": [2, "nofunc"], 114 | 115 | "callback-return": 2, 116 | "global-require": 1, 117 | "handle-callback-err": 1, 118 | "no-mixed-requires": 1, 119 | "no-new-require": 2, 120 | "no-path-concat": 2, 121 | "no-process-exit": 2, 122 | "no-restricted-imports": 0, 123 | "no-restricted-modules": 0, 124 | "no-sync": 0, 125 | 126 | "array-bracket-spacing": [2, "never"], 127 | "block-spacing": [2, "never"], 128 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 129 | "camelcase": [2, {"properties": "always"}], 130 | "comma-spacing": [2, {"before": false, "after": true}], 131 | "comma-style": [2, "last"], 132 | "computed-property-spacing": [2, "never"], 133 | "consistent-this": 0, 134 | "eol-last": 2, 135 | "func-names": 0, 136 | "func-style": [2, "declaration"], 137 | "id-length": 0, 138 | "id-match": 0, 139 | "id-blacklist": 0, 140 | "indent": [2, 4], 141 | "jsx-quotes": 0, 142 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 143 | "keyword-spacing": [2, {"before": true, "after": true}], 144 | "linebreak-style": [2, "unix"], 145 | "lines-around-comment": [2, {"beforeBlockComment": true}], 146 | "max-depth": [2, 5], 147 | "max-len": [1, 120, 4], 148 | "max-nested-callbacks": [1, 4], 149 | "max-params": 0, 150 | "max-statements": 0, 151 | "new-cap": [2, {"newIsCap": true, "capIsNew": true}], 152 | "new-parens": 2, 153 | "newline-after-var": 0, 154 | "newline-per-chained-call": 0, 155 | "no-array-constructor": 2, 156 | "no-bitwise": 0, 157 | "no-continue": 0, 158 | "no-inline-comments": 0, 159 | "no-lonely-if": 2, 160 | "no-mixed-spaces-and-tabs": 2, 161 | "no-multiple-empty-lines": [2, {"max": 1}], 162 | "no-negated-condition": 2, 163 | "no-nested-ternary": 2, 164 | "no-new-object": 2, 165 | "no-plusplus": 0, 166 | "no-restricted-syntax": 0, 167 | "no-whitespace-before-property": 2, 168 | "no-spaced-func": 2, 169 | "no-ternary": 0, 170 | "no-trailing-spaces": 2, 171 | "no-underscore-dangle": 0, 172 | "no-unneeded-ternary": [2, {"defaultAssignment": true}], 173 | "object-curly-spacing": [2, "never"], 174 | "one-var": 0, 175 | "one-var-declaration-per-line": [2, "initializations"], 176 | "operator-assignment": 0, 177 | "operator-linebreak": [2, "after"], 178 | "padded-blocks": [2, "never"], 179 | "quote-props": [2, "consistent-as-needed", {"keywords": true}], 180 | "quotes": [2, "single", "avoid-escape"], 181 | "require-jsdoc": 0, 182 | "semi-spacing": [2, {"before": false, "after": true}], 183 | "semi": [2, "always"], 184 | "sort-vars": 0, 185 | "sort-imports": 0, 186 | "space-before-blocks": [2, "always"], 187 | "space-before-function-paren": [2, "never"], 188 | "space-in-parens": [2, "never"], 189 | "space-infix-ops": 2, 190 | "space-unary-ops": 2, 191 | "spaced-comment": 0, 192 | "wrap-regex": 0 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [2.1.0] - 2024-03-13 4 | ### Added 5 | - Added fetching & caching of media info in queue, `playlist/details` no longer includes it 6 | - Added moderation option event & endpoint data [#39](https://github.com/anjanms/DubAPI/pull/39) ([@Al3366](https://github.com/Al3366)) 7 | - Added event handling for `room-allow-guest-chat`, `room-allow-guest-embed`, `room-slow-mode` 8 | - Added `moderateSetOption` method to change moderator accessible settings 9 | 10 | ### Changed 11 | - Updated role & permission data to match website [#39](https://github.com/anjanms/DubAPI/pull/39) ([@Al3366](https://github.com/Al3366)) 12 | 13 | ### Fixed 14 | - Fixed a TypeError on advance when `songInfo` is null 15 | 16 | ## [2.0.0] - 2021-04-08 17 | ### Added 18 | - Added `dj` role and methods [#35](https://github.com/anjanms/DubAPI/pull/35) ([@Zolfax](https://github.com/Zolfax)) 19 | 20 | ### Changed 21 | - Changed site domain to `queup.net` [#38](https://github.com/anjanms/DubAPI/pull/38) ([@dakoenig](https://github.com/dakoenig)) 22 | - Enabled HTTP keep alive 23 | 24 | ### Fixed 25 | - Fixed a race condition in room connect flow that leaves the bot user undefined 26 | 27 | ### Dependencies 28 | - Replaced the now deprecated [request](https://github.com/request/request/issues/3142) with [got](https://github.com/sindresorhus/got) as HTTP client 29 | - Updated node engines to `^12.12.0 || >=14.0.0` 30 | 31 | ## [1.6.9] - 2018-05-01 32 | ### Dependencies 33 | - Updated engine.io-client to 3.2.1 [#37](https://github.com/anjanms/DubAPI/issues/37) 34 | 35 | ## [1.6.8] - 2017-02-21 36 | ### Changed 37 | - Send presence enter on socket reconnect too 38 | 39 | ## [1.6.7] - 2017-02-21 40 | ### Changed 41 | - Now sends presence enter message when attaching to room channels 42 | 43 | ### Dependencies 44 | - Updated engine.io-client to 2.0.0 45 | 46 | ## [1.6.6] - 2017-01-18 47 | ### Fixed 48 | - Fixed a TypeError on bot disconnect [#34](https://github.com/anjanms/DubAPI/issues/34) 49 | 50 | ### Changed 51 | - Changed dependency version selector to appease [David DM](https://david-dm.org/anjanms/DubAPI) 52 | 53 | ## [1.6.5] - 2016-12-22 54 | ### Fixed 55 | - Fixed a crash when creating a RequestError 56 | - Fixed a case where the bot could be left without a socket connection 57 | - Removed an extra authToken request that was accidentally left behind 58 | 59 | ## [1.6.4] - 2016-12-22 60 | ### Changed 61 | - Now using Dubtrack's own WebSockets [#32](https://github.com/anjanms/DubAPI/issues/32) 62 | 63 | ## [1.6.3] - 2016-09-09 64 | ### Changed 65 | - Set Ably environment to `dubtrack` 66 | 67 | ## [1.6.2] - 2016-07-01 68 | ### Fixed 69 | - Fixed bot not showing in presence 70 | 71 | ## [1.6.1] - 2016-06-30 72 | ### Added 73 | - Added API User-Agent to requests 74 | 75 | ### Dependencies 76 | - Changed realtime from PubNub to Ably 77 | 78 | ## [1.6.0] - 2016-03-01 79 | ### Added 80 | - Added callback functionality to sendChat [#23](https://github.com/anjanms/DubAPI/issues/23) 81 | 82 | ### Dependencies 83 | - Updated PubNub to 3.13.0 84 | 85 | ## [1.5.1] - 2016-02-16 86 | ### Dependencies 87 | - Updated PubNub to 3.9.0 88 | - Updated ESLint to 2.0.0 89 | 90 | ## [1.5.0] - 2016-02-11 91 | ### Added 92 | - Added user queue methods (queuePlaylist, clearQueue, pauseQueue) [#21](https://github.com/anjanms/DubAPI/issues/21) [#22](https://github.com/anjanms/DubAPI/pull/22) ([@Fuechschen](https://github.com/Fuechschen)) 93 | - Added queueMedia method [#21](https://github.com/anjanms/DubAPI/issues/21) 94 | 95 | ## [1.4.0] - 2016-02-04 96 | ### Added 97 | - Added chat-mention permission 98 | - Added moderatePauseDJ [#20](https://github.com/anjanms/DubAPI/pull/20) ([@Fuechschen](https://github.com/Fuechschen)) 99 | 100 | ## [1.3.0] - 2016-01-28 101 | ### Added 102 | - Added user profileImage support 103 | - Added `user-update` event for profileImage updates 104 | - Added moderateLockQueue [#19](https://github.com/anjanms/DubAPI/pull/19) ([@Fuechschen](https://github.com/Fuechschen)) 105 | - Added support for case-insensitive matching in getUserByName [#18](https://github.com/anjanms/DubAPI/issues/18) 106 | 107 | ### Changed 108 | - Changed existing `user-update` event to `user_update` to match dubtrack 109 | 110 | ## [1.2.1] - 2016-01-17 111 | ### Changed 112 | - Changed media objects to be undefined when missing `type` or `fkid` [#16](https://github.com/anjanms/DubAPI/issues/16) 113 | 114 | ## [1.2.0] - 2016-01-13 115 | ### Added 116 | - Added support for grabs 117 | 118 | ### Fixed 119 | - Fixed a crash when media name is undefined [#16](https://github.com/anjanms/DubAPI/issues/16) 120 | 121 | ## [1.1.0] - 2015-12-14 122 | ### Added 123 | - Added option to limit chat message splits 124 | - Added automatic re-join when the bot erroneously leaves the room 125 | 126 | ### Changed 127 | - Changed max chat message length to 255 128 | 129 | ### Fixed 130 | - Fixed users still in the cache not being re-added to the collection 131 | - Fixed old events being processed when the bot reconnects 132 | - Fixed internal models accidentally being exposed to event listeners 133 | - Fixed a crash when the bot disconnects and still has requests queued 134 | 135 | ### Removed 136 | - Removed `chatid` property with duplicate value from `delete-chat-message` event (use `id` instead) 137 | 138 | ## [1.0.3] - 2015-12-08 139 | ### Added 140 | - Added updating of usernames on chat-message event [#10](https://github.com/anjanms/DubAPI/issues/10) 141 | 142 | ### Changed 143 | - Changed updub/downdub to use new endpoint [#11](https://github.com/anjanms/DubAPI/pull/11) ([@thedark1337](https://github.com/thedark1337)) 144 | 145 | ## [1.0.2] - 2015-12-01 146 | ### Fixed 147 | - Fixed a crash caused by song being undefined in responses from `room/%RID%/playlist/active` [#8](https://github.com/anjanms/DubAPI/issues/8) 148 | - Fixed a crash caused by the array containing null in responses from `room/%RID%/playlist/details` [#8](https://github.com/anjanms/DubAPI/issues/8) 149 | 150 | ## [1.0.1] - 2015-11-29 151 | ### Fixed 152 | - Fixed a crash in moderateBanUser and moderateMoveUser on node 0.10 153 | 154 | ## [1.0.0] - 2015-11-29 155 | ### Added 156 | - Added moderateSetRole and moderateUnsetRole methods [#5](https://github.com/anjanms/DubAPI/pull/5) ([@Fuechschen](https://github.com/Fuechschen)) 157 | - Added moderateRemoveSong and moderateRemoveDJ methods [#5](https://github.com/anjanms/DubAPI/pull/5) ([@Fuechschen](https://github.com/Fuechschen)) 158 | - Added automatic relogin and request retrying 159 | - Added getQueue and getQueuePosition methods [#3](https://github.com/anjanms/DubAPI/issues/3) 160 | - Added moderateMoveDJ method 161 | 162 | ### Fixed 163 | - Fixed a TypeError when a kick message is not defined 164 | - Fixed media objects emitted via RTEs not being clones 165 | - Fixed some methods missing state checks 166 | - Fixed methods accepting non-finite numbers 167 | 168 | ### Changed 169 | - Changed artificial advance events to include lastPlay and raw 170 | - Changed split chat messages to be queued immediately instead of waiting for server response 171 | - Changed chat messages to not be emitted if one already exists in chat history with the same id 172 | - Changed moderation methods to return true if the request was queued, false otherwise 173 | - Changed methods that should return arrays to return empty arrays when state checks fail 174 | - Changed methods that should return numbers to return -1 when state checks fail 175 | - Enabled strict mode 176 | - Enabled gzip compression 177 | 178 | [2.1.0]: https://github.com/anjanms/DubAPI/compare/v2.0.0...v2.1.0 179 | [2.0.0]: https://github.com/anjanms/DubAPI/compare/v1.6.9...v2.0.0 180 | [1.6.9]: https://github.com/anjanms/DubAPI/compare/v1.6.8...v1.6.9 181 | [1.6.8]: https://github.com/anjanms/DubAPI/compare/v1.6.7...v1.6.8 182 | [1.6.7]: https://github.com/anjanms/DubAPI/compare/v1.6.6...v1.6.7 183 | [1.6.6]: https://github.com/anjanms/DubAPI/compare/v1.6.5...v1.6.6 184 | [1.6.5]: https://github.com/anjanms/DubAPI/compare/v1.6.4...v1.6.5 185 | [1.6.4]: https://github.com/anjanms/DubAPI/compare/v1.6.3...v1.6.4 186 | [1.6.3]: https://github.com/anjanms/DubAPI/compare/v1.6.2...v1.6.3 187 | [1.6.2]: https://github.com/anjanms/DubAPI/compare/v1.6.1...v1.6.2 188 | [1.6.1]: https://github.com/anjanms/DubAPI/compare/v1.6.0...v1.6.1 189 | [1.6.0]: https://github.com/anjanms/DubAPI/compare/v1.5.1...v1.6.0 190 | [1.5.1]: https://github.com/anjanms/DubAPI/compare/v1.5.0...v1.5.1 191 | [1.5.0]: https://github.com/anjanms/DubAPI/compare/v1.4.0...v1.5.0 192 | [1.4.0]: https://github.com/anjanms/DubAPI/compare/v1.3.0...v1.4.0 193 | [1.3.0]: https://github.com/anjanms/DubAPI/compare/v1.2.1...v1.3.0 194 | [1.2.1]: https://github.com/anjanms/DubAPI/compare/v1.2.0...v1.2.1 195 | [1.2.0]: https://github.com/anjanms/DubAPI/compare/v1.1.0...v1.2.0 196 | [1.1.0]: https://github.com/anjanms/DubAPI/compare/v1.0.3...v1.1.0 197 | [1.0.3]: https://github.com/anjanms/DubAPI/compare/v1.0.2...v1.0.3 198 | [1.0.2]: https://github.com/anjanms/DubAPI/compare/v1.0.1...v1.0.2 199 | [1.0.1]: https://github.com/anjanms/DubAPI/compare/v1.0.0...v1.0.1 200 | [1.0.0]: https://github.com/anjanms/DubAPI/compare/0.2.4...v1.0.0 201 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright © 2015 Anthony Neal Schneider Jr (anjanms) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the “Software”), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DubAPI [![][versionbadge]][versionlink] [![][licensebadge]][licenselink] 2 | 3 | ## About 4 | 5 | A Node.js API for creating queup.net bots. 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install dubapi 11 | ``` 12 | 13 | Optionally, the [websocket implementation](https://github.com/websockets/ws) can make use of native addons for [performance and spec compliance](https://github.com/websockets/ws#opt-in-for-performance-and-spec-compliance). 14 | 15 | ``` 16 | npm install --save-optional bufferutil utf-8-validate 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | 23 | var DubAPI = require('dubapi'); 24 | 25 | new DubAPI({username: '', password: ''}, function(err, bot) { 26 | if (err) return console.error(err); 27 | 28 | console.log('Running DubAPI v' + bot.version); 29 | 30 | function connect() {bot.connect('friendship-is-magic');} 31 | 32 | bot.on('connected', function(name) { 33 | console.log('Connected to ' + name); 34 | }); 35 | 36 | bot.on('disconnected', function(name) { 37 | console.log('Disconnected from ' + name); 38 | 39 | setTimeout(connect, 15000); 40 | }); 41 | 42 | bot.on('error', function(err) { 43 | console.error(err); 44 | }); 45 | 46 | bot.on(bot.events.chatMessage, function(data) { 47 | console.log(data.user.username + ': ' + data.message); 48 | }); 49 | 50 | connect(); 51 | }); 52 | 53 | ``` 54 | ## Credit 55 | 56 | - Design cues taken from [PlugAPI](https://github.com/plugCubed/plugAPI) 57 | 58 | [versionlink]: https://www.npmjs.com/package/dubapi 59 | [versionbadge]: https://img.shields.io/npm/v/dubapi "npm version" 60 | 61 | [licenselink]: https://github.com/anjanms/DubAPI/blob/master/LICENSE.md 62 | [licensebadge]: https://img.shields.io/npm/l/dubapi "npm license" 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'), 4 | eventEmitter = require('events').EventEmitter; 5 | 6 | var RoomModel = require('./lib/models/roomModel.js'), 7 | SelfModel = require('./lib/models/selfModel.js'), 8 | UserModel = require('./lib/models/userModel.js'); 9 | 10 | var RequestHandler = require('./lib/requestHandler.js'), 11 | ActionHandler = require('./lib/actionHandler.js'), 12 | SocketHandler = require('./lib/socketHandler.js'), 13 | EventHandler = require('./lib/eventHandler.js'); 14 | 15 | var DubAPIError = require('./lib/errors/error.js'), 16 | DubAPIRequestError = require('./lib/errors/requestError.js'); 17 | 18 | var pkg = require('./package.json'), 19 | utils = require('./lib/utils.js'), 20 | events = require('./lib/data/events.js'), 21 | roles = require('./lib/data/roles.js'), 22 | endpoints = require('./lib/data/endpoints.js'); 23 | 24 | function DubAPI(auth, callback) { 25 | if (typeof auth !== 'object') throw new TypeError('auth must be an object'); 26 | 27 | if (typeof auth.username !== 'string') throw new TypeError('auth.username must be a string'); 28 | if (typeof auth.password !== 'string') throw new TypeError('auth.password must be a string'); 29 | 30 | if (typeof callback !== 'function') throw new TypeError('callback must be a function'); 31 | 32 | var that = this; 33 | 34 | eventEmitter.call(this); 35 | 36 | this._ = {}; 37 | 38 | this._.connected = false; 39 | this._.actHandler = new ActionHandler(this, auth); 40 | this._.reqHandler = new RequestHandler(this); 41 | this._.sokHandler = new SocketHandler(this); 42 | 43 | this._.slug = undefined; 44 | this._.self = undefined; 45 | this._.room = undefined; 46 | 47 | this.mutedTriggerEvents = false; 48 | this.maxChatMessageSplits = 1; 49 | 50 | this._.actHandler.doLogin(function(err) { 51 | if (err) return callback(err); 52 | 53 | that._.reqHandler.queue({method: 'GET', url: endpoints.authSession}, function(code, body) { 54 | if (code !== 200) return callback(new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.authSession))); 55 | 56 | that._.self = new SelfModel(body.data); 57 | 58 | that._.sokHandler.connect(); 59 | 60 | callback(undefined, that); 61 | }); 62 | }); 63 | } 64 | 65 | util.inherits(DubAPI, eventEmitter); 66 | 67 | DubAPI.prototype.events = events; 68 | DubAPI.prototype.roles = roles; 69 | DubAPI.prototype.version = pkg.version; 70 | 71 | /* 72 | * External Functions 73 | */ 74 | 75 | DubAPI.prototype.connect = function(slug) { 76 | if (this._.slug !== undefined) return; 77 | 78 | this._.slug = slug; 79 | 80 | var that = this; 81 | 82 | that._.reqHandler.queue({method: 'GET', url: endpoints.room}, function(code, body) { 83 | if (code !== 200) { 84 | that.emit('error', new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.room))); 85 | return that.disconnect(); 86 | } 87 | 88 | var roomJoinEndpoint = endpoints.roomUsers.replace('%RID%', body.data._id); 89 | 90 | that._.reqHandler.queue({method: 'POST', url: roomJoinEndpoint}, function(code, body) { 91 | if ([200, 401].indexOf(code) === -1) { 92 | that.emit('error', new DubAPIRequestError(code, roomJoinEndpoint)); 93 | return that.disconnect(); 94 | } else if (code === 401) { 95 | that.emit('error', new DubAPIError(that._.self.username + ' is banned from ' + that._.slug)); 96 | return that.disconnect(); 97 | } 98 | 99 | that._.room = new RoomModel(body.data.room); 100 | 101 | body.data.user._user = utils.clone(that._.self); 102 | 103 | that._.room.users.add(new UserModel(body.data.user)); 104 | 105 | that._.sokHandler.attachChannel('room:' + that._.room.id, utils.bind(EventHandler, that)); 106 | 107 | that._.reqHandler.queue({method: 'GET', url: endpoints.roomUsers}, function(code, body) { 108 | if (code !== 200) { 109 | that.emit('error', new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.roomUsers))); 110 | return that.disconnect(); 111 | } 112 | 113 | body.data.map(function(data) {return new UserModel(data);}).forEach(function(userModel) { 114 | that._.room.users.add(userModel); 115 | }); 116 | 117 | that._.actHandler.updatePlay(); 118 | that._.actHandler.updateQueue(); 119 | 120 | that._.connected = true; 121 | that.emit('connected', that._.room.name); 122 | }); 123 | }); 124 | }); 125 | }; 126 | 127 | DubAPI.prototype.disconnect = function() { 128 | if (this._.slug === undefined) return; 129 | 130 | var name = this._.room ? this._.room.name : undefined; 131 | 132 | this._.reqHandler.clear(); 133 | 134 | if (this._.room) { 135 | clearTimeout(this._.room.playTimeout); 136 | this._.sokHandler.detachChannel('room:' + this._.room.id); 137 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.roomUsers}); 138 | } 139 | 140 | this._.slug = undefined; 141 | this._.room = undefined; 142 | 143 | if (this._.connected) { 144 | this.emit('disconnected', name); 145 | this._.connected = false; 146 | } 147 | }; 148 | 149 | DubAPI.prototype.reconnect = function() { 150 | if (this._.slug === undefined) return; 151 | 152 | var slug = this._.slug; 153 | 154 | this.disconnect(); 155 | this.connect(slug); 156 | }; 157 | 158 | DubAPI.prototype.sendChat = function(message, callback) { 159 | if (!this._.connected) return; 160 | 161 | if (typeof message !== 'string') throw new TypeError('message must be a string'); 162 | 163 | message = message.trim(); 164 | 165 | if (message.length === 0) throw new Error('message cannot be empty'); 166 | 167 | message = utils.encodeHTMLEntities(message); 168 | 169 | message = message.match(/(.{1,255})(?:\s|$)|(.{1,255})/g); 170 | 171 | var body = {}; 172 | 173 | body.type = 'chat-message'; 174 | body.realTimeChannel = this._.room.realTimeChannel; 175 | 176 | for (var i = 0; i < message.length; i++) { 177 | body.time = Date.now(); 178 | body.message = message[i]; 179 | 180 | this._.reqHandler.queue({method: 'POST', url: endpoints.chat, json: utils.clone(body), isChat: true}, callback); 181 | 182 | callback = undefined; 183 | 184 | if (i >= this.maxChatMessageSplits) break; 185 | } 186 | }; 187 | 188 | DubAPI.prototype.getChatHistory = function() { 189 | if (!this._.connected) return []; 190 | 191 | return utils.clone(this._.room.chat, {deep: true}); 192 | }; 193 | 194 | DubAPI.prototype.getRoomMeta = function() { 195 | if (!this._.connected) return; 196 | 197 | return this._.room.getMeta(); 198 | }; 199 | 200 | DubAPI.prototype.getQueue = function() { 201 | if (!this._.connected) return []; 202 | 203 | return utils.clone(this._.room.queue, {deep: true}); 204 | }; 205 | 206 | DubAPI.prototype.getQueuePosition = function(uid) { 207 | if (!this._.connected) return -1; 208 | 209 | return this._.room.queue.indexWhere({uid: uid}); 210 | }; 211 | 212 | /* 213 | * User Queue Functions 214 | */ 215 | 216 | DubAPI.prototype.queueMedia = function(type, fkid, callback) { 217 | if (!this._.connected) return false; 218 | 219 | if (typeof type !== 'string') throw new TypeError('type must be a string'); 220 | if (typeof fkid !== 'string') throw new TypeError('fkid must be a string'); 221 | 222 | var form = {songType: type, songId: fkid}; 223 | 224 | this._.reqHandler.queue({method: 'POST', url: endpoints.roomPlaylist, form: form}, callback); 225 | 226 | return true; 227 | }; 228 | 229 | DubAPI.prototype.queuePlaylist = function(playlistid, callback) { 230 | if (!this._.connected) return false; 231 | 232 | if (typeof playlistid !== 'string') throw new TypeError('playlistid must be a string'); 233 | 234 | this._.reqHandler.queue({method: 'POST', url: endpoints.queuePlaylist.replace('%PID%', playlistid)}, callback); 235 | 236 | return true; 237 | }; 238 | 239 | DubAPI.prototype.clearQueue = function(callback) { 240 | if (!this._.connected) return false; 241 | 242 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.roomPlaylist}, callback); 243 | 244 | return true; 245 | }; 246 | 247 | DubAPI.prototype.pauseQueue = function(pause, callback) { 248 | if (!this._.connected) return false; 249 | 250 | if (typeof pause !== 'boolean') throw new TypeError('pause must be a boolean'); 251 | 252 | var form = {queuePaused: pause ? 1 : 0}; 253 | 254 | this._.reqHandler.queue({method: 'PUT', url: endpoints.queuePause, form: form}, callback); 255 | 256 | return true; 257 | }; 258 | 259 | /* 260 | * Moderation Functions 261 | */ 262 | 263 | DubAPI.prototype.moderateSkip = function(callback) { 264 | if (!this._.connected || !this._.room.play) return false; 265 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('skip')) return false; 266 | 267 | if (this._.room.play.skipped) return false; 268 | 269 | var form = {realTimeChannel: this._.room.realTimeChannel}, 270 | uri = endpoints.chatSkip.replace('%PID%', this._.room.play.id); 271 | 272 | this._.reqHandler.queue({method: 'POST', url: uri, form: form}, callback); 273 | 274 | return true; 275 | }; 276 | 277 | DubAPI.prototype.moderateDeleteChat = function(cid, callback) { 278 | if (!this._.connected) return false; 279 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('delete-chat')) return false; 280 | 281 | if (typeof cid !== 'string') throw new TypeError('cid must be a string'); 282 | 283 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.chatDelete.replace('%CID%', cid)}, callback); 284 | 285 | return true; 286 | }; 287 | 288 | DubAPI.prototype.moderateBanUser = function(uid, time, callback) { 289 | if (!this._.connected) return false; 290 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('ban')) return false; 291 | 292 | if (typeof time === 'function') { 293 | callback = time; 294 | time = undefined; 295 | } 296 | 297 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 298 | if (time !== undefined && !Number.isInteger(time)) throw new TypeError('time must be undefined or an integer'); 299 | if (time && time < 0) throw new RangeError('time must be zero or greater'); 300 | 301 | var user = this._.room.users.findWhere({id: uid}); 302 | if (user && user.role !== null) return false; 303 | 304 | var form = {realTimeChannel: this._.room.realTimeChannel, time: time ? time : 0}; 305 | 306 | this._.reqHandler.queue({method: 'POST', url: endpoints.chatBan.replace('%UID%', uid), form: form}, callback); 307 | 308 | return true; 309 | }; 310 | 311 | DubAPI.prototype.moderateUnbanUser = function(uid, callback) { 312 | if (!this._.connected) return false; 313 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('ban')) return false; 314 | 315 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 316 | 317 | var form = {realTimeChannel: this._.room.realTimeChannel}; 318 | 319 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.chatBan.replace('%UID%', uid), form: form}, callback); 320 | 321 | return true; 322 | }; 323 | 324 | DubAPI.prototype.moderateKickUser = function(uid, msg, callback) { 325 | if (!this._.connected) return false; 326 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('kick')) return false; 327 | 328 | if (typeof msg === 'function') { 329 | callback = msg; 330 | msg = undefined; 331 | } 332 | 333 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 334 | if (['string', 'undefined'].indexOf(typeof msg) === -1) throw new TypeError('msg must be a string or undefined'); 335 | 336 | var user = this._.room.users.findWhere({id: uid}); 337 | if (user && user.role !== null) return false; 338 | 339 | var form = {realTimeChannel: this._.room.realTimeChannel, message: msg ? utils.encodeHTMLEntities(msg) : ''}; 340 | 341 | this._.reqHandler.queue({method: 'POST', url: endpoints.chatKick.replace('%UID%', uid), form: form}, callback); 342 | 343 | return true; 344 | }; 345 | 346 | DubAPI.prototype.moderateMuteUser = function(uid, callback) { 347 | if (!this._.connected) return false; 348 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('mute')) return false; 349 | 350 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 351 | 352 | var user = this._.room.users.findWhere({id: uid}); 353 | if (user && user.role !== null) return false; 354 | 355 | var form = {realTimeChannel: this._.room.realTimeChannel}; 356 | 357 | this._.reqHandler.queue({method: 'POST', url: endpoints.chatMute.replace('%UID%', uid), form: form}, callback); 358 | 359 | return true; 360 | }; 361 | 362 | DubAPI.prototype.moderateUnmuteUser = function(uid, callback) { 363 | if (!this._.connected) return false; 364 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('mute')) return false; 365 | 366 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 367 | 368 | var form = {realTimeChannel: this._.room.realTimeChannel}; 369 | 370 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.chatMute.replace('%UID%', uid), form: form}, callback); 371 | 372 | return true; 373 | }; 374 | 375 | DubAPI.prototype.moderateMoveDJ = function(uid, position, callback) { 376 | if (!this._.connected) return false; 377 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('queue-order')) return false; 378 | 379 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 380 | if (!Number.isInteger(position)) throw new TypeError('position must be an integer'); 381 | 382 | var index = this._.room.queue.indexWhere({uid: uid}); 383 | 384 | if (position < 0) position = 0; 385 | else if (position >= this._.room.queue.length) position = this._.room.queue.length - 1; 386 | 387 | if (index === position || index === -1) return false; 388 | 389 | var queue = this._.room.queue.map(function(queueItem) {return queueItem.uid;}); 390 | 391 | queue.splice(position, 0, queue.splice(index, 1)[0]); 392 | 393 | this._.reqHandler.queue({method: 'POST', url: endpoints.roomQueueOrder, form: {order: queue}}, callback); 394 | 395 | return true; 396 | }; 397 | 398 | DubAPI.prototype.moderateRemoveDJ = function(uid, callback) { 399 | if (!this._.connected) return false; 400 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('queue-order')) return false; 401 | 402 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 403 | 404 | if (this._.room.queue.indexWhere({uid: uid}) === -1) return false; 405 | 406 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.roomQueueRemoveUser.replace('%UID%', uid)}, callback); 407 | 408 | return true; 409 | }; 410 | 411 | DubAPI.prototype.moderateRemoveSong = function(uid, callback) { 412 | if (!this._.connected) return false; 413 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('queue-order')) return false; 414 | 415 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 416 | 417 | if (this._.room.queue.indexWhere({uid: uid}) === -1) return false; 418 | 419 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.roomQueueRemoveSong.replace('%UID%', uid)}, callback); 420 | 421 | return true; 422 | }; 423 | 424 | DubAPI.prototype.moderatePauseDJ = function(uid, callback) { 425 | if (!this._.connected) return false; 426 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('queue-order')) return false; 427 | 428 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 429 | 430 | if (this._.room.queue.indexWhere({uid: uid}) === -1) return false; 431 | 432 | this._.reqHandler.queue({method: 'PUT', url: endpoints.roomQueuePauseUser.replace('%UID%', uid)}, callback); 433 | 434 | return true; 435 | }; 436 | 437 | DubAPI.prototype.moderateSetRole = function(uid, role, callback) { 438 | if (!this._.connected) return false; 439 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('set-roles')) return false; 440 | 441 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 442 | if (typeof role !== 'string') throw new TypeError('role must be a string'); 443 | if (roles[role] === undefined) throw new DubAPIError('role not found'); 444 | 445 | var form = {realTimeChannel: this._.room.realTimeChannel}; 446 | 447 | this._.reqHandler.queue({method: 'POST', url: endpoints.setRole.replace('%UID%', uid).replace('%ROLEID%', roles[role].id), form: form}, callback); 448 | 449 | return true; 450 | }; 451 | 452 | DubAPI.prototype.moderateUnsetRole = function(uid, role, callback) { 453 | if (!this._.connected) return false; 454 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('set-roles')) return false; 455 | 456 | if (typeof uid !== 'string') throw new TypeError('uid must be a string'); 457 | if (typeof role !== 'string') throw new TypeError('role must be a string'); 458 | if (roles[role] === undefined) throw new DubAPIError('role not found'); 459 | 460 | var form = {realTimeChannel: this._.room.realTimeChannel}; 461 | 462 | this._.reqHandler.queue({method: 'DELETE', url: endpoints.setRole.replace('%UID%', uid).replace('%ROLEID%', role), form: form}, callback); 463 | 464 | return true; 465 | }; 466 | 467 | DubAPI.prototype.moderateLockQueue = function(locked, callback) { 468 | if (!this._.connected) return false; 469 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('lock-queue')) return false; 470 | 471 | if (this._.room.lockQueue === locked) return false; 472 | 473 | if (typeof locked !== 'boolean') throw new TypeError('locked must be a boolean'); 474 | 475 | var form = {lockQueue: 0}; 476 | if (locked) form.lockQueue = 1; 477 | 478 | this._.reqHandler.queue({method: 'PUT', url: endpoints.lockQueue, form: form}, callback); 479 | 480 | return true; 481 | }; 482 | 483 | DubAPI.prototype.moderateSetOption = function(option, value, callback) { 484 | if (!this._.connected) return false; 485 | if (!this._.room.users.findWhere({id: this._.self.id}).hasPermission('mod-settings')) return false; 486 | 487 | if (this._.room[option] === value) return false; 488 | 489 | if (option === 'allowGuestsToChat' && typeof value !== 'boolean') { 490 | throw new TypeError('allowGuestsToChat must be a boolean'); 491 | } 492 | 493 | if (option === 'allowGuestsToEmbed' && typeof value !== 'boolean') { 494 | throw new TypeError('allowGuestsToEmbed must be a boolean'); 495 | } 496 | 497 | if (option === 'slowMode' && typeof value !== 'boolean') { 498 | throw new TypeError('slowMode must be a boolean'); 499 | } 500 | 501 | var form = {action: option, value: value}; 502 | 503 | this._.reqHandler.queue({method: 'POST', url: endpoints.roomModSettings, form: form}, callback); 504 | 505 | return true; 506 | }; 507 | 508 | /* 509 | * Media Functions 510 | */ 511 | 512 | DubAPI.prototype.updub = function(callback) { 513 | if (!this._.connected || !this._.room.play || this._.room.play.dubs[this._.self.id] === 'updub') return; 514 | 515 | this._.reqHandler.queue({method: 'POST', url: endpoints.roomPlaylistVote.replace('%PLAYLISTID%', this._.room.play.id), form: {type: 'updub'}}, callback); 516 | }; 517 | 518 | DubAPI.prototype.downdub = function(callback) { 519 | if (!this._.connected || !this._.room.play || this._.room.play.dubs[this._.self.id] === 'downdub') return; 520 | 521 | this._.reqHandler.queue({method: 'POST', url: endpoints.roomPlaylistVote.replace('%PLAYLISTID%', this._.room.play.id), form: {type: 'downdub'}}, callback); 522 | }; 523 | 524 | DubAPI.prototype.getMedia = function() { 525 | if (!this._.connected || !this._.room.play) return; 526 | 527 | return utils.clone(this._.room.play.media); 528 | }; 529 | 530 | DubAPI.prototype.getScore = function() { 531 | if (!this._.connected || !this._.room.play) return; 532 | 533 | return this._.room.play.getScore(); 534 | }; 535 | 536 | DubAPI.prototype.getPlayID = function() { 537 | if (!this._.connected || !this._.room.play) return; 538 | 539 | return this._.room.play.id; 540 | }; 541 | 542 | DubAPI.prototype.getTimeRemaining = function() { 543 | if (!this._.connected || !this._.room.play) return -1; 544 | 545 | return this._.room.play.getTimeRemaining(); 546 | }; 547 | 548 | DubAPI.prototype.getTimeElapsed = function() { 549 | if (!this._.connected || !this._.room.play) return -1; 550 | 551 | return this._.room.play.getTimeElapsed(); 552 | }; 553 | 554 | /* 555 | * User Functions 556 | */ 557 | 558 | DubAPI.prototype.getUser = function(uid) { 559 | if (!this._.connected) return; 560 | 561 | return utils.clone(this._.room.users.findWhere({id: uid})); 562 | }; 563 | 564 | DubAPI.prototype.getUserByName = function(username, ignoreCase) { 565 | if (!this._.connected) return; 566 | 567 | return utils.clone(this._.room.users.findWhere({username: username}, {ignoreCase: ignoreCase})); 568 | }; 569 | 570 | DubAPI.prototype.getSelf = function() { 571 | if (!this._.connected) return; 572 | 573 | return utils.clone(this._.room.users.findWhere({id: this._.self.id})); 574 | }; 575 | 576 | DubAPI.prototype.getCreator = function() { 577 | if (!this._.connected) return; 578 | 579 | return utils.clone(this._.room.users.findWhere({id: this._.room.user})); 580 | }; 581 | 582 | DubAPI.prototype.getDJ = function() { 583 | if (!this._.connected || !this._.room.play) return; 584 | 585 | return utils.clone(this._.room.users.findWhere({id: this._.room.play.user})); 586 | }; 587 | 588 | DubAPI.prototype.getUsers = function() { 589 | if (!this._.connected) return []; 590 | 591 | return utils.clone(this._.room.users); 592 | }; 593 | 594 | DubAPI.prototype.getStaff = function() { 595 | if (!this._.connected) return []; 596 | 597 | return utils.clone(this._.room.users.filter(function(user) {return user.role !== null;})); 598 | }; 599 | 600 | /* 601 | * Role Functions 602 | */ 603 | 604 | DubAPI.prototype.isCreator = function(user) { 605 | if (!this._.connected || user === undefined) return false; 606 | return user.id === this._.room.user; 607 | }; 608 | 609 | DubAPI.prototype.isOwner = function(user) { 610 | if (!this._.connected || user === undefined) return false; 611 | return user.role === roles['co-owner'].id; 612 | }; 613 | 614 | DubAPI.prototype.isManager = function(user) { 615 | if (!this._.connected || user === undefined) return false; 616 | return user.role === roles['manager'].id; 617 | }; 618 | 619 | DubAPI.prototype.isMod = function(user) { 620 | if (!this._.connected || user === undefined) return false; 621 | return user.role === roles['mod'].id; 622 | }; 623 | 624 | DubAPI.prototype.isVIP = function(user) { 625 | if (!this._.connected || user === undefined) return false; 626 | return user.role === roles['vip'].id; 627 | }; 628 | 629 | DubAPI.prototype.isResidentDJ = function(user) { 630 | if (!this._.connected || user === undefined) return false; 631 | return user.role === roles['resident-dj'].id; 632 | }; 633 | 634 | DubAPI.prototype.isDJ = function(user) { 635 | if (!this._.connected || user === undefined) return false; 636 | return user.role === roles['dj'].id; 637 | }; 638 | 639 | DubAPI.prototype.isMember = function(user) { 640 | if (!this._.connected || user === undefined) return false; 641 | return user.role === roles['member'].id; 642 | }; 643 | 644 | DubAPI.prototype.isStaff = function(user) { 645 | if (!this._.connected || user === undefined) return false; 646 | return user.role !== null; 647 | }; 648 | 649 | /* 650 | * Permission Functions 651 | */ 652 | 653 | DubAPI.prototype.hasPermission = function(user, permission) { 654 | if (!this._.connected || user === undefined) return false; 655 | return this._.room.users.findWhere({id: user.id}).hasPermission(permission); 656 | }; 657 | 658 | module.exports = DubAPI; 659 | -------------------------------------------------------------------------------- /lib/actionHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MediaModel = require('./models/mediaModel.js'), 4 | PlayModel = require('./models/playModel.js'); 5 | 6 | var DubAPIError = require('./errors/error.js'), 7 | DubAPIRequestError = require('./errors/requestError.js'); 8 | 9 | var endpoints = require('./data/endpoints.js'), 10 | events = require('./data/events.js'), 11 | utils = require('./utils.js'); 12 | 13 | var mediaCache = {}; 14 | 15 | function ActionHandler(dubAPI, auth) { 16 | this.doLogin = doLogin.bind(dubAPI, auth); 17 | this.clearPlay = clearPlay.bind(dubAPI); 18 | this.updatePlay = updatePlay.bind(dubAPI); 19 | this.updateQueue = updateQueue.bind(dubAPI); 20 | this.updateQueueDebounce = utils.debounce(this.updateQueue, 5000); 21 | } 22 | 23 | function doLogin(auth, callback) { 24 | var that = this; 25 | 26 | this._.reqHandler.send({method: 'POST', url: endpoints.authDubtrack, form: auth}, function(code, body) { 27 | if ([200, 400].indexOf(code) === -1) { 28 | return callback(new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.authDubtrack))); 29 | } else if (code === 400) { 30 | return callback(new DubAPIError('Authentication Failed: ' + body.data.details.message)); 31 | } 32 | 33 | callback(undefined); 34 | }); 35 | } 36 | 37 | function clearPlay() { 38 | clearTimeout(this._.room.playTimeout); 39 | 40 | var message = {type: events.roomPlaylistUpdate}; 41 | 42 | if (this._.room.play) { 43 | message.lastPlay = { 44 | id: this._.room.play.id, 45 | media: utils.clone(this._.room.play.media), 46 | user: utils.clone(this._.room.users.findWhere({id: this._.room.play.user})), 47 | score: this._.room.play.getScore() 48 | }; 49 | } 50 | 51 | this._.room.play = undefined; 52 | this._.room.users.set({dub: undefined}); 53 | 54 | this.emit('*', message); 55 | this.emit(events.roomPlaylistUpdate, message); 56 | } 57 | 58 | function updatePlay() { 59 | var that = this; 60 | 61 | clearTimeout(that._.room.playTimeout); 62 | 63 | that._.reqHandler.queue({method: 'GET', url: endpoints.roomPlaylistActive}, function(code, body) { 64 | if ([200, 404].indexOf(code) === -1) { 65 | that.emit('error', new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.roomPlaylistActive))); 66 | return; 67 | } else if (code === 404) { 68 | that._.actHandler.clearPlay(); 69 | return; 70 | } 71 | 72 | if (body.data.song === undefined) { 73 | //Dubtrack API sometimes doesn't define a song. 74 | //Schedule an update in the future, maybe it will then. 75 | that._.room.playTimeout = setTimeout(that._.actHandler.updatePlay, 30000); 76 | return; 77 | } 78 | 79 | var message = {type: events.roomPlaylistUpdate}, 80 | newPlay = new PlayModel(body.data.song), 81 | curPlay = that._.room.play; 82 | 83 | if (curPlay && newPlay.id === curPlay.id) { 84 | if (Date.now() - newPlay.played > newPlay.songLength) that._.actHandler.clearPlay(); 85 | return; 86 | } 87 | 88 | // 20210510 QueUp API sometimes doesn't define songInfo 89 | if (body.data.songInfo != null) { 90 | newPlay.media = new MediaModel(body.data.songInfo); 91 | 92 | if (newPlay.media.type === undefined || newPlay.media.fkid === undefined) { 93 | newPlay.media = undefined; 94 | } 95 | } 96 | 97 | message.raw = body.data; 98 | 99 | if (that._.room.play) { 100 | message.lastPlay = { 101 | id: curPlay.id, 102 | media: utils.clone(curPlay.media), 103 | user: utils.clone(that._.room.users.findWhere({id: curPlay.user})), 104 | score: curPlay.getScore() 105 | }; 106 | } 107 | 108 | message.id = newPlay.id; 109 | message.media = utils.clone(newPlay.media); 110 | message.user = utils.clone(that._.room.users.findWhere({id: newPlay.user})); 111 | 112 | that._.reqHandler.queue({method: 'GET', url: endpoints.roomPlaylistActiveDubs}, function(code, body) { 113 | that._.room.play = newPlay; 114 | that._.room.playTimeout = setTimeout(that._.actHandler.updatePlay, newPlay.getTimeRemaining() + 15000); 115 | 116 | that._.room.users.set({dub: undefined}); 117 | 118 | if (code === 200) { 119 | body.data.currentSong = new PlayModel(body.data.currentSong); 120 | 121 | if (newPlay.id === body.data.currentSong.id) { 122 | newPlay.updubs = body.data.currentSong.updubs; 123 | newPlay.downdubs = body.data.currentSong.downdubs; 124 | newPlay.grabs = body.data.currentSong.grabs; 125 | 126 | body.data.upDubs.forEach(function(dub) { 127 | newPlay.dubs[dub.userid] = 'updub'; 128 | 129 | var user = that._.room.users.findWhere({id: dub.userid}); 130 | if (user) user.set({dub: 'updub'}); 131 | }); 132 | 133 | body.data.downDubs.forEach(function(dub) { 134 | newPlay.dubs[dub.userid] = 'downdub'; 135 | 136 | var user = that._.room.users.findWhere({id: dub.userid}); 137 | if (user) user.set({dub: 'downdub'}); 138 | }); 139 | } 140 | } else { 141 | that.emit('error', new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.roomPlaylistActiveDubs))); 142 | } 143 | 144 | that.emit('*', message); 145 | that.emit(events.roomPlaylistUpdate, message); 146 | }); 147 | }); 148 | } 149 | 150 | function updateQueue() { 151 | var that = this; 152 | 153 | that._.reqHandler.queue({method: 'GET', url: endpoints.roomPlaylistDetails}, function(code, body) { 154 | if (code !== 200) { 155 | that.emit('error', new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.roomPlaylistDetails))); 156 | return; 157 | } 158 | 159 | var promises = body.data.reduce(function(promises, queueItem) { 160 | if (queueItem === null) return promises; 161 | 162 | if (mediaCache[queueItem.songid]) { 163 | mediaCache[queueItem.songid].ts = Date.now(); 164 | return promises; 165 | } 166 | 167 | promises.push(new Promise(function(resolve, reject) { 168 | that._.reqHandler.send({method: 'GET', url: endpoints.song.replace('%SONGID%', queueItem.songid)}, function(code, body) { 169 | if (code !== 200) { 170 | that.emit('error', new DubAPIRequestError(code, that._.reqHandler.endpoint(endpoints.song.replace('%SONGID%', queueItem.songid)))); 171 | // Yes this should probably reject, but that changes the function of Promise.all 172 | // This result is not used, we're simply awaiting completion of all the requests 173 | return resolve(); 174 | } 175 | 176 | mediaCache[queueItem.songid] = {media: new MediaModel(body.data), ts: Date.now()}; 177 | 178 | return resolve(); 179 | }); 180 | })); 181 | 182 | return promises; 183 | }, []); 184 | 185 | Promise.all(promises).then(function() { 186 | var message = {type: events.roomPlaylistQueueUpdate}; 187 | 188 | that._.room.queue.clear(); 189 | 190 | body.data.forEach(function(queueItem) { 191 | //Dubtrack API sometimes returns an array containing null. 192 | if (queueItem === null) return; 193 | 194 | queueItem = { 195 | id: queueItem._id, 196 | uid: queueItem._user._id, 197 | media: mediaCache[queueItem.songid] ? mediaCache[queueItem.songid].media : undefined, 198 | get user() {return that._.room.users.findWhere({id: this.uid});} 199 | }; 200 | 201 | that._.room.queue.add(queueItem); 202 | }); 203 | 204 | message.raw = body.data; 205 | message.queue = utils.clone(that._.room.queue, {deep: true}); 206 | 207 | that.emit('*', message); 208 | that.emit(events.roomPlaylistQueueUpdate, message); 209 | }); 210 | 211 | setImmediate(function() { 212 | for (var key in mediaCache) { 213 | if (mediaCache.hasOwnProperty(key) && Date.now() - mediaCache[key].ts > 900000) delete mediaCache[key]; 214 | } 215 | }); 216 | }); 217 | } 218 | 219 | module.exports = ActionHandler; 220 | -------------------------------------------------------------------------------- /lib/collections/baseCollection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function BaseCollection() { 4 | //Nothing to do 5 | } 6 | 7 | BaseCollection.prototype = Object.create(Array.prototype); 8 | 9 | BaseCollection.prototype.maxLength = Infinity; 10 | 11 | BaseCollection.prototype.add = function(item) { 12 | if (!item || this.findWhere({id: item.id}) !== undefined) return false; 13 | this.push(item); 14 | if (this.length > this.maxLength) this.shift(); 15 | return true; 16 | }; 17 | 18 | BaseCollection.prototype.where = function(attrs) { 19 | var keys = Object.keys(attrs), 20 | results = []; 21 | 22 | for (var itemIndex = 0; itemIndex < this.length; itemIndex++) { 23 | for (var itemKey = 0; itemKey < keys.length; itemKey++) { 24 | if (this[itemIndex][keys[itemKey]] !== attrs[keys[itemKey]]) break; 25 | if (itemKey === keys.length - 1) results.push(this[itemIndex]); 26 | } 27 | } 28 | 29 | return results; 30 | }; 31 | 32 | BaseCollection.prototype.findWhere = function(attrs) { 33 | return this.where(attrs)[0]; 34 | }; 35 | 36 | BaseCollection.prototype.remove = function(item) { 37 | if (!item || this.findWhere({id: item.id}) === undefined) return false; 38 | 39 | for (var itemIndex = 0; itemIndex < this.length; itemIndex++) { 40 | if (this[itemIndex].id !== item.id) continue; 41 | this.splice(itemIndex, 1); 42 | break; 43 | } 44 | 45 | return true; 46 | }; 47 | 48 | BaseCollection.prototype.clear = function() { 49 | this.splice(0, this.length); 50 | }; 51 | 52 | module.exports = BaseCollection; 53 | -------------------------------------------------------------------------------- /lib/collections/chatCollection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BaseCollection = require('./baseCollection.js'); 4 | 5 | function ChatCollection() { 6 | this.maxLength = 512; 7 | } 8 | 9 | ChatCollection.prototype = Object.create(BaseCollection.prototype); 10 | 11 | module.exports = ChatCollection; 12 | -------------------------------------------------------------------------------- /lib/collections/queueCollection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BaseCollection = require('./baseCollection.js'); 4 | 5 | function QueueCollection() { 6 | //Nothing to do 7 | } 8 | 9 | QueueCollection.prototype = Object.create(BaseCollection.prototype); 10 | 11 | QueueCollection.prototype.indexWhere = function(attrs) { 12 | var keys = Object.keys(attrs); 13 | 14 | for (var itemIndex = 0; itemIndex < this.length; itemIndex++) { 15 | for (var itemKey = 0; itemKey < keys.length; itemKey++) { 16 | if (this[itemIndex][keys[itemKey]] !== attrs[keys[itemKey]]) break; 17 | if (itemKey === keys.length - 1) return itemIndex; 18 | } 19 | } 20 | 21 | return -1; 22 | }; 23 | 24 | module.exports = QueueCollection; 25 | -------------------------------------------------------------------------------- /lib/collections/userCollection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BaseCollection = require('./baseCollection.js'); 4 | 5 | function UserCollection() { 6 | this.cache = []; 7 | } 8 | 9 | UserCollection.prototype = Object.create(BaseCollection.prototype); 10 | 11 | UserCollection.prototype.add = function(userModel) { 12 | if (!userModel || this.findWhere({id: userModel.id}, {ignoreCache: true}) !== undefined) return false; 13 | this.push(userModel); 14 | return true; 15 | }; 16 | 17 | UserCollection.prototype.where = function(attrs, opts) { 18 | var keys = Object.keys(attrs), 19 | results = []; 20 | 21 | opts = opts || {}; 22 | 23 | for (var userIndex = 0; userIndex < this.length; userIndex++) { 24 | for (var userKey = 0; userKey < keys.length; userKey++) { 25 | if (opts.ignoreCase && typeof this[userIndex][keys[userKey]] === 'string' && typeof attrs[keys[userKey]] === 'string') { 26 | if (this[userIndex][keys[userKey]].toLowerCase() !== attrs[keys[userKey]].toLowerCase()) break; 27 | } else if (this[userIndex][keys[userKey]] !== attrs[keys[userKey]]) break; 28 | 29 | if (userKey === keys.length - 1) { 30 | results.push(this[userIndex]); 31 | if (opts.singleMatch) return results; 32 | } 33 | } 34 | } 35 | 36 | if (opts.ignoreCache) return results; 37 | 38 | for (var cacheIndex = 0; cacheIndex < this.cache.length; cacheIndex++) { 39 | for (var cacheKey = 0; cacheKey < keys.length; cacheKey++) { 40 | if (opts.ignoreCase && typeof this[cacheIndex][keys[cacheKey]] === 'string' && typeof attrs[keys[cacheKey]] === 'string') { 41 | if (this[cacheIndex][keys[cacheKey]].toLowerCase() !== attrs[keys[cacheKey]].toLowerCase()) break; 42 | } else if (this.cache[cacheIndex][keys[cacheKey]] !== attrs[keys[cacheKey]]) break; 43 | 44 | if (cacheKey === keys.length - 1) { 45 | results.push(this.cache[cacheIndex]); 46 | if (opts.singleMatch) return results; 47 | } 48 | } 49 | } 50 | 51 | return results; 52 | }; 53 | 54 | UserCollection.prototype.findWhere = function(attrs, opts) { 55 | opts = opts || {}; 56 | opts.singleMatch = true; 57 | return this.where(attrs, opts)[0]; 58 | }; 59 | 60 | UserCollection.prototype.set = function(attrs) { 61 | var keys = Object.keys(attrs); 62 | 63 | for (var userIndex = 0; userIndex < this.length; userIndex++) { 64 | for (var userKey = 0; userKey < keys.length; userKey++) { 65 | this[userIndex][keys[userKey]] = attrs[keys[userKey]]; 66 | } 67 | } 68 | }; 69 | 70 | UserCollection.prototype.remove = function(userModel) { 71 | if (!userModel || this.findWhere({id: userModel.id}, {ignoreCache: true}) === undefined) return false; 72 | 73 | function removeFromCache() { 74 | for (var cacheIndex = 0; cacheIndex < this.cache.length; cacheIndex++) { 75 | if (this.cache[cacheIndex].id !== userModel.id) continue; 76 | this.cache.splice(cacheIndex, 1); 77 | break; 78 | } 79 | } 80 | 81 | for (var userIndex = 0; userIndex < this.length; userIndex++) { 82 | if (this[userIndex].id !== userModel.id) continue; 83 | this.cache.push(this.splice(userIndex, 1)[0]); 84 | setTimeout(removeFromCache.bind(this), 10000); 85 | break; 86 | } 87 | 88 | return true; 89 | }; 90 | 91 | module.exports = UserCollection; 92 | -------------------------------------------------------------------------------- /lib/data/endpoints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | authDubtrack: 'auth/login', 5 | authSession: 'auth/session', 6 | authToken: 'auth/token', 7 | chat: 'chat/%RID%', 8 | chatBan: 'chat/ban/%RID%/user/%UID%', 9 | chatDelete: 'chat/%RID%/%CID%', 10 | chatKick: 'chat/kick/%RID%/user/%UID%', 11 | chatMute: 'chat/mute/%RID%/user/%UID%', 12 | chatSkip: 'chat/skip/%RID%/%PID%', 13 | room: 'room/%SLUG%', 14 | roomPlaylist: 'room/%RID%/playlist', 15 | roomPlaylistActive: 'room/%RID%/playlist/active', 16 | roomPlaylistActiveDubs: 'room/%RID%/playlist/active/dubs', 17 | roomPlaylistDetails: 'room/%RID%/playlist/details', 18 | roomPlaylistVote: 'room/%RID%/playlist/%PLAYLISTID%/dubs', 19 | roomQueueOrder: 'room/%RID%/queue/order', 20 | roomQueueRemoveSong: 'room/%RID%/queue/user/%UID%', 21 | roomQueueRemoveUser: 'room/%RID%/queue/user/%UID%/all', 22 | roomQueuePauseUser: 'room/%RID%/queue/user/%UID%/pause', 23 | roomUsers: 'room/%RID%/users', 24 | roomModSettings: 'room/%RID%/modsettings', 25 | roomAuditLog: 'room/%RID%/audit', 26 | setRole: '/chat/%ROLEID%/%RID%/user/%UID%', 27 | song: 'song/%SONGID%', 28 | lockQueue: '/room/%RID%/lockQueue', 29 | queuePlaylist: 'room/%RID%/queueplaylist/%PID%', 30 | queuePause: 'room/%RID%/queue/pause', 31 | userPlaylists: 'playlist', 32 | userPlaylist: 'playlist/%PID%' 33 | }; 34 | -------------------------------------------------------------------------------- /lib/data/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | chatMessage: 'chat-message', 5 | chatSkip: 'chat-skip', 6 | deleteChatMessage: 'delete-chat-message', 7 | roomPlaylistDub: 'room_playlist-dub', 8 | roomPlaylistGrab: 'room_playlist-queue-update-grabs', 9 | roomPlaylistQueueUpdate: 'room_playlist-queue-update-dub', 10 | roomPlaylistUpdate: 'room_playlist-update', 11 | roomUpdate: 'room-update', 12 | roomSlowMode: 'room-slow-mode', 13 | roomAllowGuestsToChat: 'room-allow-guest-chat', 14 | roomAllowGuestsToEmbed: 'room-allow-guest-embed', 15 | userBan: 'user-ban', 16 | userImageUpdate: 'user-update', 17 | userJoin: 'user-join', 18 | userKick: 'user-kick', 19 | userLeave: 'user-leave', 20 | userMute: 'user-mute', 21 | userSetRole: 'user-setrole', 22 | userUnban: 'user-unban', 23 | userUnmute: 'user-unmute', 24 | userUnsetRole: 'user-unsetrole', 25 | userUpdate: 'user_update' 26 | }; 27 | -------------------------------------------------------------------------------- /lib/data/roles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var roles = {}; 4 | 5 | roles['co-owner'] = roles['5615fa9ae596154a5c000000'] = { 6 | id: '5615fa9ae596154a5c000000', 7 | type: 'co-owner', 8 | label: 'Co-Owner', 9 | rights: [ 10 | 'update-room', 11 | 'set-roles', 12 | 'set-managers', 13 | 'skip', 14 | 'queue-order', 15 | 'kick', 16 | 'ban', 17 | 'mute', 18 | 'set-dj', 19 | 'lock-queue', 20 | 'delete-chat', 21 | 'chat-mention', 22 | 'chat', 23 | 'mod-settings' 24 | ] 25 | }; 26 | 27 | roles['manager'] = roles['5615fd84e596150061000003'] = { 28 | id: '5615fd84e596150061000003', 29 | type: 'manager', 30 | label: 'Manager', 31 | rights: [ 32 | 'set-roles', 33 | 'skip', 34 | 'queue-order', 35 | 'kick', 36 | 'ban', 37 | 'mute', 38 | 'set-dj', 39 | 'lock-queue', 40 | 'delete-chat', 41 | 'chat-mention', 42 | 'chat', 43 | 'mod-settings' 44 | ] 45 | }; 46 | 47 | roles['mod'] = roles['52d1ce33c38a06510c000001'] = { 48 | id: '52d1ce33c38a06510c000001', 49 | type: 'mod', 50 | label: 'Moderator', 51 | rights: [ 52 | 'skip', 53 | 'queue-order', 54 | 'kick', 55 | 'ban', 56 | 'mute', 57 | 'set-dj', 58 | 'lock-queue', 59 | 'delete-chat', 60 | 'chat-mention', 61 | 'chat', 62 | 'mod-settings' 63 | ] 64 | }; 65 | 66 | roles['vip'] = roles['5615fe1ee596154fc2000001'] = { 67 | id: '5615fe1ee596154fc2000001', 68 | type: 'vip', 69 | label: 'VIP', 70 | rights: [ 71 | 'skip', 72 | 'set-dj', 73 | 'chat' 74 | ] 75 | }; 76 | 77 | roles['resident-dj'] = roles['5615feb8e596154fc2000002'] = { 78 | id: '5615feb8e596154fc2000002', 79 | type: 'resident-dj', 80 | label: 'Resident DJ', 81 | rights: [ 82 | 'set-dj', 83 | 'chat' 84 | ] 85 | }; 86 | 87 | roles['dj'] = roles['564435423f6ba174d2000001'] = { 88 | id: '564435423f6ba174d2000001', 89 | type: 'dj', 90 | label: 'DJ', 91 | rights: [ 92 | 'chat' 93 | ] 94 | }; 95 | 96 | roles['member'] = roles['65ef1e77ac32214febf09295'] = { 97 | id: '65ef1e77ac32214febf09295', 98 | type: 'member', 99 | label: 'Member', 100 | rights: [ 101 | 'chat' 102 | ] 103 | }; 104 | 105 | module.exports = roles; 106 | -------------------------------------------------------------------------------- /lib/errors/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function DubAPIError(message) { 4 | Error.captureStackTrace(this); 5 | this.name = 'DubAPIError'; 6 | this.message = message || ''; 7 | } 8 | 9 | DubAPIError.prototype = Object.create(Error.prototype); 10 | 11 | module.exports = DubAPIError; 12 | -------------------------------------------------------------------------------- /lib/errors/requestError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function DubAPIRequestError(code, endpoint) { 4 | Error.captureStackTrace(this); 5 | this.name = 'DubAPIRequestError'; 6 | this.message = 'Response ' + code + ' from ' + endpoint; 7 | } 8 | 9 | DubAPIRequestError.prototype = Object.create(Error.prototype); 10 | 11 | module.exports = DubAPIRequestError; 12 | -------------------------------------------------------------------------------- /lib/eventHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var MediaModel = require('./models/mediaModel.js'), 4 | PlayModel = require('./models/playModel.js'), 5 | UserModel = require('./models/userModel.js'); 6 | 7 | var DubAPIError = require('./errors/error.js'); 8 | 9 | var utils = require('./utils.js'), 10 | events = require('./data/events.js'), 11 | endpoints = require('./data/endpoints.js'); 12 | 13 | function EventHandler(msg) { 14 | if (!this._.connected) return; 15 | 16 | var userUpdateID, data, raw; 17 | 18 | if ((userUpdateID = msg.type.match(/^user(-|_)update(?:-|_)([0-9a-f]+)$/))) { 19 | msg.type = 'user' + userUpdateID[1] + 'update'; 20 | userUpdateID = userUpdateID[2]; 21 | } 22 | 23 | raw = utils.clone(msg, {deep: true}); 24 | 25 | switch (msg.type) { 26 | case events.userJoin: 27 | msg.roomUser._user = msg.user; 28 | msg.user = new UserModel(msg.roomUser); 29 | delete msg.roomUser; 30 | 31 | if (this._.room.play) msg.user.dub = this._.room.play.dubs[msg.user.id]; 32 | 33 | if (!this._.room.users.add(msg.user)) return; 34 | break; 35 | case events.userLeave: 36 | msg.user = this._.room.users.findWhere({id: msg.user._id}); 37 | delete msg.room; 38 | 39 | //Handle erroneous leave events for the bot 40 | if (msg.user && msg.user.id === this._.self.id) { 41 | //Delay queuing the join request so ban and kick events have a chance to stop it 42 | setTimeout(function() { 43 | if (this._.connected) this._.reqHandler.queue({method: 'POST', url: endpoints.roomUsers}); 44 | }.bind(this), 5000); 45 | } 46 | 47 | if (!this._.room.users.remove(msg.user)) return; 48 | break; 49 | case events.userSetRole: 50 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 51 | msg.user = this._.room.users.findWhere({id: msg.modUser._id}); 52 | delete msg.modUser; 53 | 54 | if (msg.user) msg.user.role = msg.role_object._id; 55 | delete msg.role_object; 56 | break; 57 | case events.userUnsetRole: 58 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 59 | msg.user = this._.room.users.findWhere({id: msg.modUser._id}); 60 | delete msg.modUser; 61 | 62 | if (msg.user) msg.user.role = null; 63 | delete msg.role_object; 64 | break; 65 | case events.userBan: 66 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 67 | msg.user = this._.room.users.findWhere({id: msg.kickedUser._id}); 68 | delete msg.kickedUser; 69 | 70 | if (msg.user && msg.user.id === this._.self.id) { 71 | setImmediate(function() { 72 | this.emit('error', new DubAPIError('Banned from ' + this._.room.name)); 73 | this.disconnect(); 74 | }.bind(this)); 75 | } 76 | break; 77 | case events.userUnban: 78 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 79 | msg.user = this._.room.users.findWhere({id: msg.kickedUser._id}); 80 | delete msg.kickedUser; 81 | break; 82 | case events.userKick: 83 | if (msg.message) msg.message = utils.decodeHTMLEntities(msg.message.trim()); 84 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 85 | msg.user = this._.room.users.findWhere({id: msg.kickedUser._id}); 86 | delete msg.kickedUser; 87 | 88 | if (msg.user && msg.user.id === this._.self.id) { 89 | setImmediate(function() { 90 | this.emit('error', new DubAPIError('Kicked from ' + this._.room.name)); 91 | this.disconnect(); 92 | }.bind(this)); 93 | } 94 | break; 95 | case events.userMute: 96 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 97 | msg.user = this._.room.users.findWhere({id: msg.mutedUser._id}); 98 | delete msg.mutedUser; 99 | 100 | if (msg.user) msg.user.muted = true; 101 | break; 102 | case events.userUnmute: 103 | msg.mod = this._.room.users.findWhere({id: msg.user._id}); 104 | msg.user = this._.room.users.findWhere({id: msg.mutedUser._id}); 105 | delete msg.mutedUser; 106 | 107 | if (msg.user) msg.user.muted = false; 108 | break; 109 | case events.userUpdate: 110 | data = msg.user; 111 | 112 | msg.user = this._.room.users.findWhere({id: userUpdateID}); 113 | 114 | if (msg.user) msg.user.set(data); 115 | break; 116 | case events.userImageUpdate: 117 | msg.user = this._.room.users.findWhere({id: userUpdateID}); 118 | 119 | if (msg.user) msg.user.profileImage = msg.img; 120 | delete msg.img; 121 | break; 122 | case events.chatMessage: 123 | data = msg.user; 124 | 125 | msg.id = msg.chatid; 126 | msg.message = utils.decodeHTMLEntities(msg.message.trim()); 127 | msg.user = this._.room.users.findWhere({id: msg.user._id}); 128 | 129 | delete msg.chatid; 130 | delete msg.queue_object; 131 | 132 | //Update usernames, since user-update doesn't handle username changes 133 | if (msg.user && data.username !== msg.user.username && typeof data.username === 'string') { 134 | msg.user.username = data.username; 135 | } 136 | 137 | if (msg.user && msg.user.muted && !this.mutedTriggerEvents) return; 138 | if (!this._.room.chat.add(utils.clone(msg, {deep: true}))) return; 139 | break; 140 | case events.deleteChatMessage: 141 | msg.id = msg.chatid; 142 | msg.user = this._.room.users.findWhere({id: msg.user._id}); 143 | 144 | delete msg.chatid; 145 | 146 | this._.room.chat.remove(this._.room.chat.findWhere({id: msg.id})); 147 | break; 148 | case events.chatSkip: 149 | msg.user = this._.room.users.findWhere({username: msg.username}); 150 | delete msg.username; 151 | 152 | if (this._.room.play) this._.room.play.skipped = true; 153 | break; 154 | case events.roomUpdate: 155 | if (this._.room && this._.room.id !== msg.room._id) return; 156 | 157 | this._.room.set(msg.room); 158 | msg.room = this._.room.getMeta(); 159 | break; 160 | case events.roomAllowGuestsToChat: 161 | case events.roomAllowGuestsToEmbed: 162 | case events.roomSlowMode: 163 | if (this._.room && this._.room.id !== msg.room._id) return; 164 | 165 | if (msg.type === events.roomAllowGuestsToChat && typeof msg.room.allowGuestsToChat === 'boolean') { 166 | this._.room.allowGuestsToChat = msg.room.allowGuestsToChat; 167 | } 168 | 169 | if (msg.type === events.roomAllowGuestsToEmbed && typeof msg.room.allowGuestsToEmbed === 'boolean') { 170 | this._.room.allowGuestsToEmbed = msg.room.allowGuestsToEmbed; 171 | } 172 | 173 | if (msg.type === events.roomSlowMode && typeof msg.room.slowMode === 'boolean') { 174 | this._.room.slowMode = msg.room.slowMode; 175 | } 176 | 177 | msg.room = this._.room.getMeta(); 178 | msg.user = this._.room.users.findWhere({id: msg.user._id}); 179 | break; 180 | case events.roomPlaylistUpdate: 181 | msg.song = new PlayModel(msg.song); 182 | msg.song.media = new MediaModel(msg.songInfo); 183 | delete msg.songInfo; 184 | 185 | if (this._.room.play && msg.song.id === this._.room.play.id) return; 186 | 187 | if (msg.song.media.type === undefined || msg.song.media.fkid === undefined) msg.song.media = undefined; 188 | 189 | this._.actHandler.updateQueueDebounce(); 190 | 191 | if (this._.room.play) { 192 | msg.lastPlay = { 193 | id: this._.room.play.id, 194 | media: utils.clone(this._.room.play.media), 195 | user: utils.clone(this._.room.users.findWhere({id: this._.room.play.user})), 196 | score: this._.room.play.getScore() 197 | }; 198 | } 199 | 200 | this._.room.play = msg.song; 201 | delete msg.song; 202 | 203 | clearTimeout(this._.room.playTimeout); 204 | this._.room.playTimeout = setTimeout(this._.actHandler.updatePlay, this._.room.play.getTimeRemaining() + 15000); 205 | 206 | this._.room.users.set({dub: undefined}); 207 | 208 | msg.media = this._.room.play.media; 209 | msg.user = this._.room.users.findWhere({id: this._.room.play.user}); 210 | msg.id = this._.room.play.id; 211 | break; 212 | case events.roomPlaylistDub: 213 | msg.song = new PlayModel(msg.playlist); 214 | delete msg.playlist; 215 | 216 | if (!this._.room.play || msg.song.id !== this._.room.play.id) return; 217 | 218 | this._.room.play.updubs = msg.song.updubs; 219 | this._.room.play.downdubs = msg.song.downdubs; 220 | 221 | delete msg.song; 222 | 223 | msg.user = this._.room.users.findWhere({id: msg.user._id}); 224 | 225 | if (msg.user) { 226 | this._.room.play.dubs[msg.user.id] = msg.dubtype; 227 | msg.user.set({dub: msg.dubtype}); 228 | } 229 | break; 230 | case events.roomPlaylistGrab: 231 | msg.song = new PlayModel(msg.playlist); 232 | delete msg.playlist; 233 | 234 | if (!this._.room.play || msg.song.id !== this._.room.play.id) return; 235 | 236 | this._.room.play.grabs = msg.song.grabs; 237 | 238 | delete msg.song; 239 | 240 | msg.user = this._.room.users.findWhere({id: msg.user._id}); 241 | break; 242 | case events.roomPlaylistQueueUpdate: 243 | this._.actHandler.updateQueueDebounce(); 244 | return; 245 | //no default 246 | } 247 | 248 | msg.raw = raw; 249 | 250 | if (msg.mod instanceof UserModel) msg.mod = utils.clone(msg.mod); 251 | if (msg.user instanceof UserModel) msg.user = utils.clone(msg.user); 252 | if (msg.media instanceof MediaModel) msg.media = utils.clone(msg.media); 253 | 254 | this.emit('*', msg); 255 | this.emit(msg.type, msg); 256 | } 257 | 258 | module.exports = EventHandler; 259 | -------------------------------------------------------------------------------- /lib/models/mediaModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../utils.js'); 4 | 5 | var propertyFilter = ['__v', '_id', 'name', 'updub', 'downdub', 'userid']; 6 | 7 | function MediaModel(data) { 8 | this.id = data._id; 9 | this.name = utils.decodeHTMLEntities(data.name); 10 | 11 | for (var key in data) { 12 | if (data.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = data[key]; 13 | } 14 | } 15 | 16 | module.exports = MediaModel; 17 | -------------------------------------------------------------------------------- /lib/models/playModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var propertyFilter = ['__v', '_id', '_user', '_song', 'songid', 'userid', 'roomid']; 4 | 5 | function PlayModel(data) { 6 | this.id = data._id; 7 | this.user = data.userid; 8 | 9 | this.updubs = 0; 10 | this.grabs = 0; 11 | this.downdubs = 0; 12 | 13 | for (var key in data) { 14 | if (data.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = data[key]; 15 | } 16 | 17 | this.dubs = {}; 18 | this.media = undefined; 19 | } 20 | 21 | PlayModel.prototype.getTimeElapsed = function() { 22 | return Math.min(this.songLength, Date.now() - this.played); 23 | }; 24 | 25 | PlayModel.prototype.getTimeRemaining = function() { 26 | return Math.max(0, this.played + this.songLength - Date.now()); 27 | }; 28 | 29 | PlayModel.prototype.getScore = function() { 30 | return {updubs: this.updubs, grabs: this.grabs, downdubs: this.downdubs}; 31 | }; 32 | 33 | module.exports = PlayModel; 34 | -------------------------------------------------------------------------------- /lib/models/roomModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ChatCollection = require('../collections/chatCollection.js'), 4 | QueueCollection = require('../collections/queueCollection.js'), 5 | UserCollection = require('../collections/userCollection.js'); 6 | 7 | var utils = require('../utils.js'); 8 | 9 | var propertyFilter = ['__v', '_id', '_user', 'userid', 'currentSong', 'otSession']; 10 | 11 | function RoomModel(data) { 12 | this.id = data._id; 13 | this.user = data.userid; 14 | 15 | for (var key in data) { 16 | if (data.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = data[key]; 17 | } 18 | 19 | this.chat = new ChatCollection(); 20 | this.queue = new QueueCollection(); 21 | this.users = new UserCollection(); 22 | 23 | this.play = undefined; 24 | this.playTimeout = undefined; 25 | } 26 | 27 | RoomModel.prototype.set = function(attrs) { 28 | for (var key in attrs) { 29 | if (attrs.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = attrs[key]; 30 | } 31 | }; 32 | 33 | RoomModel.prototype.getMeta = function() { 34 | return utils.clone(this, {deep: true, exclude: [['chat', 'queue', 'users', 'play', 'playTimeout']]}); 35 | }; 36 | 37 | module.exports = RoomModel; 38 | -------------------------------------------------------------------------------- /lib/models/selfModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var propertyFilter = ['__v', '_id', 'userInfo']; 4 | 5 | function SelfModel(data) { 6 | this.id = data._id; 7 | 8 | for (var key in data) { 9 | if (data.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = data[key]; 10 | } 11 | } 12 | 13 | module.exports = SelfModel; 14 | -------------------------------------------------------------------------------- /lib/models/userModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var roles = require('../data/roles.js'); 4 | 5 | var propertyFilter = ['__v', '_id', '_user', 'updub', 'downdub', 'userid', 'roleid', 'roomid', 'ot_token']; 6 | 7 | function UserModel(data) { 8 | this.id = data.userid; 9 | this.role = data.roleid ? data.roleid._id : null; 10 | this.created = data._user.created; 11 | this.username = data._user.username; 12 | this.profileImage = data._user.profileImage; 13 | 14 | for (var key in data) { 15 | if (data.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = data[key]; 16 | } 17 | 18 | this.dub = undefined; 19 | } 20 | 21 | UserModel.prototype.set = function(attrs) { 22 | for (var key in attrs) { 23 | if (attrs.hasOwnProperty(key) && propertyFilter.indexOf(key) === -1) this[key] = attrs[key]; 24 | } 25 | }; 26 | 27 | UserModel.prototype.hasPermission = function(perm) { 28 | if (this.role && roles[this.role] && roles[this.role].rights.indexOf(perm) !== -1) return true; 29 | return false; 30 | }; 31 | 32 | module.exports = UserModel; 33 | -------------------------------------------------------------------------------- /lib/requestHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var https = require('https'); 4 | 5 | var pkg = require('../package.json'); 6 | 7 | var got = require('got').extend({ 8 | agent: {https: new https.Agent({keepAlive: true})}, 9 | decompress: true, 10 | followRedirect: false, 11 | headers: {'user-agent': 'DubAPI/' + pkg.version}, 12 | prefixUrl: 'https://api.queup.net/', 13 | responseType: 'json', 14 | retry: 0, 15 | throwHttpErrors: false 16 | }); 17 | 18 | var CookieJar = require('tough-cookie').CookieJar; 19 | 20 | var DubAPIError = require('./errors/error.js'), 21 | DubAPIRequestError = require('./errors/requestError.js'); 22 | 23 | var utils = require('./utils.js'); 24 | 25 | function RequestHandler(dubAPI) { 26 | this._ = {}; 27 | this._.dubAPI = dubAPI; 28 | 29 | this._.cookieJar = new CookieJar(); 30 | 31 | this._.ticking = false; 32 | this._.limit = 10; 33 | this._.queue = []; 34 | this._.sent = 0; 35 | 36 | //Bind functions once instead of every time we need them 37 | this._tick = utils.bind(this._tick, this); 38 | this._decrementSent = utils.bind(this._decrementSent, this); 39 | } 40 | 41 | RequestHandler.prototype.queue = function(options, callback) { 42 | if (typeof options !== 'object') throw new TypeError('options must be an object'); 43 | if (typeof options.url !== 'string') throw new TypeError('options.url must be a string'); 44 | 45 | var isChat = options.isChat; 46 | delete options.isChat; 47 | 48 | options.url = this.endpoint(options.url); 49 | 50 | this._.queue.push({options: options, callback: callback, isChat: isChat}); 51 | 52 | if (!this._.ticking) this._tick(); 53 | 54 | return true; 55 | }; 56 | 57 | RequestHandler.prototype.clear = function() { 58 | this._.queue.splice(0, this._.queue.length); 59 | }; 60 | 61 | RequestHandler.prototype.send = function(options, callback) { 62 | if (typeof options !== 'object') throw new TypeError('options must be an object'); 63 | if (typeof options.url !== 'string') throw new TypeError('options.url must be a string'); 64 | 65 | delete options.isChat; 66 | 67 | options.url = this.endpoint(options.url); 68 | 69 | this._sendRequest({options: options, callback: callback}); 70 | 71 | return true; 72 | }; 73 | 74 | RequestHandler.prototype._tick = function() { 75 | if (this._.queue.length === 0) { 76 | this._.ticking = false; 77 | return; 78 | } 79 | 80 | this._.ticking = true; 81 | 82 | if (this._.sent >= this._.limit) { 83 | setTimeout(this._tick, 5000); 84 | return; 85 | } 86 | 87 | var queueItem = this._.queue.shift(); 88 | 89 | if (queueItem) { 90 | if (queueItem.isChat) queueItem.options.timeout = 2500; 91 | 92 | this._.sent++; 93 | 94 | setTimeout(this._decrementSent, queueItem.isChat ? 5000 : 30000); 95 | 96 | this._sendRequest(queueItem); 97 | } 98 | 99 | if (!queueItem || !queueItem.isChat) setImmediate(this._tick); 100 | }; 101 | 102 | RequestHandler.prototype._sendRequest = function(queueItem) { 103 | queueItem.options.cookieJar = this._.cookieJar; 104 | 105 | var that = this; 106 | 107 | got(queueItem.options).then(function(res) { 108 | if (!queueItem.isRetry && res.statusCode === 302 && res.headers.location === '/auth/login') { 109 | that._.dubAPI._.actHandler.doLogin(function(err) { 110 | if (err) that._.dubAPI.emit('error', err); 111 | 112 | queueItem.isRetry = true; 113 | that._.queue.unshift(queueItem); 114 | if (!that._.ticking || queueItem.isChat) that._tick(); 115 | }); 116 | return; 117 | } 118 | 119 | if (queueItem.isChat) that._tick(); 120 | 121 | if (typeof queueItem.callback === 'function') queueItem.callback(res.statusCode, res.body); 122 | else if (res.statusCode !== 200) that._.dubAPI.emit('error', new DubAPIRequestError(res.statusCode, queueItem.options.url)); 123 | }).catch(function(err) { 124 | if (queueItem.isChat && err.code === 'ETIMEDOUT') err = new DubAPIError('Chat request timed out'); 125 | 126 | that._.dubAPI.emit('error', err); 127 | 128 | //Will not work for chat request timeouts 129 | if (!queueItem.isRetry && ['ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT'].indexOf(err.code) !== -1) { 130 | queueItem.isRetry = true; 131 | that._.queue.unshift(queueItem); 132 | if (!that._.ticking || queueItem.isChat) that._tick(); 133 | return; 134 | } 135 | 136 | if (queueItem.isChat) that._tick(); 137 | }); 138 | }; 139 | 140 | RequestHandler.prototype._decrementSent = function() { 141 | this._.sent--; 142 | }; 143 | 144 | RequestHandler.prototype.endpoint = function(endpoint) { 145 | if (endpoint.indexOf('%SLUG%') !== -1 && this._.dubAPI._.slug) { 146 | endpoint = endpoint.replace('%SLUG%', this._.dubAPI._.slug); 147 | } 148 | 149 | if (endpoint.indexOf('%RID%') !== -1 && this._.dubAPI._.room) { 150 | endpoint = endpoint.replace('%RID%', this._.dubAPI._.room.id); 151 | } 152 | 153 | return endpoint; 154 | }; 155 | 156 | module.exports = RequestHandler; 157 | -------------------------------------------------------------------------------- /lib/socketHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EngineIOClient = require('engine.io-client'); 4 | 5 | var DubAPIRequestError = require('./errors/requestError.js'); 6 | 7 | var utils = require('./utils.js'); 8 | 9 | var endpoints = require('./data/endpoints.js'); 10 | 11 | function SocketHandler(dubAPI) { 12 | this._ = {}; 13 | this._.dubAPI = dubAPI; 14 | this._.socket = undefined; 15 | this._.channels = {}; 16 | this._.reconnect = true; 17 | 18 | this.connectBind = utils.bind(this.connect, this); 19 | 20 | this.onOpenBind = utils.bind(this.onOpen, this); 21 | this.onMessageBind = utils.bind(this.onMessage, this); 22 | this.onErrorBind = utils.bind(this.onError, this); 23 | this.onCloseBind = utils.bind(this.onClose, this); 24 | } 25 | 26 | SocketHandler.prototype.connect = function() { 27 | if (this._.socket) return; 28 | 29 | this._.reconnect = true; 30 | 31 | var that = this; 32 | 33 | this._.dubAPI._.reqHandler.queue({method: 'GET', url: endpoints.authToken}, function(code, body) { 34 | if (code !== 200) { 35 | that._.dubAPI.emit('error', new DubAPIRequestError(code, that._.dubAPI._.reqHandler.endpoint(endpoints.authToken))); 36 | setTimeout(that.connectBind, 5000); 37 | return; 38 | } 39 | 40 | that._.socket = new EngineIOClient({ 41 | hostname: 'ws.queup.net', 42 | secure: true, 43 | path: '/ws', 44 | query: {access_token: body.data.token}, //eslint-disable-line camelcase 45 | transports: ['websocket'] 46 | }); 47 | 48 | that._.socket.on('open', that.onOpenBind); 49 | that._.socket.on('message', that.onMessageBind); 50 | that._.socket.on('error', that.onErrorBind); 51 | that._.socket.on('close', that.onCloseBind); 52 | }); 53 | }; 54 | 55 | SocketHandler.prototype.onOpen = function() { 56 | var channels = Object.keys(this._.channels); 57 | 58 | for (var i = 0; i < channels.length; i++) { 59 | this._.socket.send(JSON.stringify({action: 10, channel: channels[i]})); 60 | 61 | if (/^room:/.test(channels[i])) { 62 | this._.socket.send(JSON.stringify({action: 14, channel: channels[i], presence: {action: 0, data: {}}})); 63 | } 64 | } 65 | 66 | this._.dubAPI.emit('socket:open'); 67 | }; 68 | 69 | SocketHandler.prototype.onMessage = function(data) { 70 | try { 71 | data = JSON.parse(data); 72 | } catch (err) { 73 | this._.dubAPI.emit('error', err); 74 | return; 75 | } 76 | 77 | this._.dubAPI.emit('socket:message', data); 78 | 79 | if (data.action === 15 && this._.channels[data.channel]) { 80 | try { 81 | data.message.data = JSON.parse(data.message.data); 82 | } catch (err) { 83 | this._.dubAPI.emit('error', err); 84 | return; 85 | } 86 | 87 | this._.channels[data.channel](data.message.data); 88 | } 89 | }; 90 | 91 | SocketHandler.prototype.onError = function(err) { 92 | this._.dubAPI.emit('error', err); 93 | }; 94 | 95 | SocketHandler.prototype.onClose = function() { 96 | this._.socket = undefined; 97 | 98 | if (this._.reconnect) setTimeout(this.connectBind, 5000); 99 | 100 | this._.dubAPI.emit('socket:close'); 101 | }; 102 | 103 | SocketHandler.prototype.attachChannel = function(channel, callback) { 104 | if (this._.socket && !this._.channels[channel]) { 105 | this._.socket.send(JSON.stringify({action: 10, channel: channel})); 106 | 107 | if (/^room:/.test(channel)) { 108 | this._.socket.send(JSON.stringify({action: 14, channel: channel, presence: {action: 0, data: {}}})); 109 | } 110 | } 111 | 112 | this._.channels[channel] = callback; 113 | }; 114 | 115 | SocketHandler.prototype.detachChannel = function(channel) { 116 | if (this._.socket && this._.channels[channel]) this._.socket.send(JSON.stringify({action: 12, channel: channel})); 117 | 118 | delete this._.channels[channel]; 119 | }; 120 | 121 | SocketHandler.prototype.disconnect = function() { 122 | if (!this._.socket) return; 123 | 124 | this._.reconnect = false; 125 | this._.socket.close(); 126 | }; 127 | 128 | module.exports = SocketHandler; 129 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function noop() { 4 | //No operation 5 | } 6 | 7 | exports.noop = noop; 8 | 9 | function bind(func, context) { 10 | return function boundFunc() { 11 | return func.apply(context, arguments); 12 | }; 13 | } 14 | 15 | exports.bind = bind; 16 | 17 | function debounce(func, wait, immediate) { 18 | var timeout; 19 | 20 | return function debouncedFunc() { 21 | var callNow = immediate && !timeout, 22 | context = this, 23 | args = arguments; 24 | 25 | clearTimeout(timeout); 26 | 27 | timeout = setTimeout(function() { 28 | timeout = null; 29 | 30 | if (!immediate) func.apply(context, args); 31 | }, wait); 32 | 33 | if (callNow) func.apply(context, args); 34 | }; 35 | } 36 | 37 | exports.debounce = debounce; 38 | 39 | function encodeHTMLEntities(str) { 40 | if (typeof str !== 'string') return str; 41 | 42 | str = str.replace(/&/g, '&') 43 | .replace(/'/g, ''') 44 | .replace(/"/g, '"') 45 | .replace(//g, '>'); 47 | 48 | return str; 49 | } 50 | 51 | exports.encodeHTMLEntities = encodeHTMLEntities; 52 | 53 | function decodeHTMLEntities(str) { 54 | if (typeof str !== 'string') return str; 55 | 56 | str = str.replace(/'|'/g, '\'') 57 | .replace(/"|"/g, '"') 58 | .replace(/&/g, '&') 59 | .replace(/</g, '<') 60 | .replace(/>/g, '>'); 61 | 62 | return str; 63 | } 64 | 65 | exports.decodeHTMLEntities = decodeHTMLEntities; 66 | 67 | function clone(obj, options) { 68 | options = options || {}; 69 | 70 | if (options.deep === undefined) options.deep = false; 71 | if (options.exclude === undefined) options.exclude = []; 72 | 73 | function copy(obj, level) { 74 | if (obj == null || typeof obj !== 'object') return obj; 75 | 76 | var clone, i; 77 | 78 | if (obj instanceof Array) { 79 | clone = []; 80 | 81 | for (i = 0; i < obj.length; i++) { 82 | if (!obj.hasOwnProperty(i)) continue; 83 | if (options.deep && level < 4) clone.push(copy(obj[i], level + 1)); 84 | else clone.push(obj[i]); 85 | } 86 | } else { 87 | clone = {}; 88 | 89 | for (i in obj) { 90 | if (!obj.hasOwnProperty(i)) continue; 91 | if (options.exclude[level] !== undefined && options.exclude[level].indexOf(i) !== -1) continue; 92 | if (options.deep && level < 4) clone[i] = copy(obj[i], level + 1); 93 | else clone[i] = obj[i]; 94 | } 95 | } 96 | 97 | return clone; 98 | } 99 | 100 | return copy(obj, 0); 101 | } 102 | 103 | exports.clone = clone; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dubapi", 3 | "version": "2.1.0", 4 | "description": "A Node.js API for creating queup.net bots", 5 | "main": "index.js", 6 | "author": "anjanms", 7 | "dependencies": { 8 | "engine.io-client": "^3.5.1", 9 | "got": "^11.8.2", 10 | "tough-cookie": "^4.0.0" 11 | }, 12 | "devDependencies": { 13 | "eslint": "^2.13.1" 14 | }, 15 | "engines": { 16 | "node": "^12.12.0 || >=14.0.0" 17 | }, 18 | "keywords": [ 19 | "bot", 20 | "api", 21 | "queup", 22 | "queup.net" 23 | ], 24 | "license": "MIT", 25 | "repository": "github:anjanms/DubAPI" 26 | } 27 | --------------------------------------------------------------------------------