UNPKG

50.7 kBJavaScriptView Raw
1'use strict';
2
3var assert = require('assert');
4var Promise = require('bluebirdish');
5var fetch = require('node-fetch');
6var createDebug = require('debug');
7var https = require('https');
8var login = require('plug-login');
9var socket = require('plug-socket');
10var EventEmitter = require('events');
11
12function delay (time) {
13 return new Promise(function (resolve) {
14 return setTimeout(resolve, time)
15 })
16}
17
18/**
19 * Create a linear incremental backoff function. Returns a function that wraps
20 * a Promise-returning function.
21 *
22 * **Usage**
23 *
24 * const func = () => Promise.resolve('done')
25 * const backoff = createBackoff({ increment: 200, max: 2200 })
26 * const throttled = backoff(func)
27 * throttled().then(console.log) // Resolves immediately
28 * throttled().then(console.log) // Resolves after 200 ms
29 */
30var linearPromiseBackoffQueue = function createBackoff (opts) {
31 assert.equal(typeof opts, 'object', 'Expected an options object');
32 assert.equal(typeof opts.increment, 'number', '`opts.increment` should be a number');
33 assert.equal(typeof opts.max, 'number', '`opts.max` should be a number');
34
35 var increment = opts.increment;
36 var max = opts.max;
37 var queuedCalls = 0;
38 var currentDelay = 0;
39 var lastCall = opts.Promise ? opts.Promise.resolve() : Promise.resolve();
40
41 function queueCall () {
42 queuedCalls += 1;
43 // Keep increasing the delay as more calls are queued.
44 currentDelay = Math.min(currentDelay + increment, max);
45 }
46 function unqueueCall () {
47 queuedCalls -= 1;
48 // If our queue is empty, reset the delay.
49 if (queuedCalls === 0) {
50 currentDelay = 0;
51 }
52 }
53
54 return function (fn) { return function () {
55 var self = this;
56 var args = Array(arguments.length);
57 for (var i = 0; i < arguments.length; i++) {
58 args[i] = arguments[i];
59 }
60 queueCall();
61 var waitDelay = currentDelay;
62
63 // `result` will resolve with the result of the function call.
64 var result = lastCall.then(function () {
65 return fn.apply(self, args)
66 });
67
68 // `lastCall` will resolve after the backoff duration is over.
69 lastCall = result
70 .then(function () { return null }) // Ignore return value of previous call.
71 .catch(function () { return null }) // Ignore errors.
72 .then(function () { return delay(waitDelay) })
73 .then(function (value) {
74 unqueueCall();
75 return value
76 });
77
78 return result
79 } }
80};
81
82function polyfillGetOwnPropertyDescriptors (obj) {
83 return Object.getOwnPropertyNames(obj).concat( Object.getOwnPropertySymbols(obj)).reduce((acc, name) => {
84 acc[name] = Object.getOwnPropertyDescriptor(obj, name);
85 return acc
86 }, {})
87}
88
89var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || polyfillGetOwnPropertyDescriptors;
90
91/**
92 * Copy property descriptors.
93 *
94 * @param {object} base
95 * @param {Array.<object>} others
96 */
97function copyProperties (base) {
98 var others = [], len = arguments.length - 1;
99 while ( len-- > 0 ) others[ len ] = arguments[ len + 1 ];
100
101 others.forEach((obj) => {
102 Object.defineProperties(base, getOwnPropertyDescriptors(obj));
103 });
104 return base
105}
106
107/**
108 * Flatten nested arrays.
109 *
110 * flatten([ 4, 5, [ 8, 7 ] ])
111 * // → [ 4, 5, 8, 7 ]
112 *
113 * @param {Array} arrs
114 */
115function flatten (arrs) {
116 var ref;
117
118 return (ref = []).concat.apply(ref, arrs)
119}
120
121/**
122 * Get the ID for an object.
123 *
124 * @param {object} item
125 * @param {string} idProp
126 */
127function getId (item, idProp) {
128 if ( idProp === void 0 ) idProp = 'id';
129
130 return typeof item === 'object' ? item[idProp] : item
131}
132
133/**
134 * Get IDs for an array of objects.
135 *
136 * @param {Array.<object>} items
137 */
138function getIds (items, idProp) {
139 if ( idProp === void 0 ) idProp = 'id';
140
141 var arr = Array.isArray(items) ? items : [ items ];
142 return arr.map((item) => getId(item, idProp))
143}
144
145/**
146 * Parse a string timestamp from plug.dj.
147 *
148 * @param {string} timestamp
149 */
150function parseDate (timestamp) {
151 return new Date(`${timestamp} UTC`)
152}
153
154/**
155 * Fill in fn's arguments with ...args from the beginning of fn's arguments
156 * list.
157 *
158 * @param {Function} fn
159 * @param {...*} args
160 */
161function partial (fn) {
162 var args = [], len = arguments.length - 1;
163 while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ];
164
165 return function () {
166 var rest = [], len = arguments.length;
167 while ( len-- ) rest[ len ] = arguments[ len ];
168
169 return fn.apply(void 0, args.concat( rest ));
170 }
171}
172
173var ROLE = {
174 NONE: 0,
175 DJ: 1000,
176 BOUNCER: 2000,
177 MANAGER: 3000,
178 COHOST: 4000,
179 HOST: 5000,
180
181 PROMOTER: 500,
182 PLOT: 750,
183 SITEMOD: 2500,
184 AMBASSADOR: 3000,
185 ADMIN: 5000
186};
187
188var MEDIA_SOURCE = {
189 YOUTUBE: 1,
190 SOUNDCLOUD: 2
191};
192
193var BAN_DURATION = {
194 HOUR: 'h',
195 DAY: 'd',
196 PERMA: 'f'
197};
198
199var BAN_REASON = {
200 SPAMMING: 1,
201 VERBAL_ABUSE: 2,
202 OFFENSIVE_PLAYS: 3,
203 GENRE: 4,
204 ATTITUDE: 5
205};
206
207var MUTE_DURATION = {
208 SHORT: 's',
209 MEDIUM: 'm',
210 LONG: 'l'
211};
212
213var MUTE_REASON = {
214 VIOLATING_RULES: 1,
215 VERBAL_ABUSE: 2,
216 SPAMMING: 3,
217 LANGUAGE: 4,
218 ATTITUDE: 5
219};
220
221var WAITLIST_BAN_DURATION = {
222 SHORT: 's',
223 MEDIUM: 'm',
224 LONG: 'l',
225 PERMA: 'f'
226};
227
228var WAITLIST_BAN_REASON = BAN_REASON;
229
230var constants = /*#__PURE__*/Object.freeze({
231 ROLE: ROLE,
232 MEDIA_SOURCE: MEDIA_SOURCE,
233 BAN_DURATION: BAN_DURATION,
234 BAN_REASON: BAN_REASON,
235 MUTE_DURATION: MUTE_DURATION,
236 MUTE_REASON: MUTE_REASON,
237 WAITLIST_BAN_DURATION: WAITLIST_BAN_DURATION,
238 WAITLIST_BAN_REASON: WAITLIST_BAN_REASON
239});
240
241var captureStackTrace = Error.captureStackTrace || function (error) {
242 var container = new Error();
243
244 Object.defineProperty(error, 'stack', {
245 configurable: true,
246 get: function getStack() {
247 var stack = container.stack;
248
249 Object.defineProperty(this, 'stack', {
250 value: stack
251 });
252
253 return stack;
254 }
255 });
256};
257
258function inherits(ctor, superCtor) {
259 ctor.super_ = superCtor;
260 ctor.prototype = Object.create(superCtor.prototype, {
261 constructor: {
262 value: ctor,
263 enumerable: false,
264 writable: true,
265 configurable: true
266 }
267 });
268}
269
270var createErrorClass = function createErrorClass(className, setup) {
271 if (typeof className !== 'string') {
272 throw new TypeError('Expected className to be a string');
273 }
274
275 if (/[^0-9a-zA-Z_$]/.test(className)) {
276 throw new Error('className contains invalid characters');
277 }
278
279 setup = setup || function (message) {
280 this.message = message;
281 };
282
283 var ErrorClass = function () {
284 Object.defineProperty(this, 'name', {
285 configurable: true,
286 value: className,
287 writable: true
288 });
289
290 captureStackTrace(this, this.constructor);
291
292 setup.apply(this, arguments);
293 };
294
295 inherits(ErrorClass, Error);
296
297 return ErrorClass;
298};
299
300function setup (response, cause) {
301 this.message = (response.body.data && response.body.data[0]) || cause.message;
302 this.status = response.body.status;
303 if (!this.response) { this.response = response; }
304 this.cause = cause;
305}
306
307function setupFundsPP (response, cause) {
308 setup.call(this, response, cause);
309 this.message = 'You don\'t have enough Plug Points to unlock this.';
310}
311
312var RequestError = createErrorClass('RequestError', setup);
313var BadLoginError = createErrorClass('BadLoginError', setup);
314var NotAuthorizedError = createErrorClass('NotAuthorizedError', setup);
315var NotFoundError = createErrorClass('NotFoundError', setup);
316var NoValidPlaylistError = createErrorClass('NoValidPlaylistError', setup);
317var NotEnoughPPError = createErrorClass('NotEnoughPPError', setupFundsPP);
318var MaintenanceError = createErrorClass('MaintenanceError', setup);
319
320var statusToError = {
321 requestError: RequestError,
322 badLogin: BadLoginError,
323 notAuthorized: NotAuthorizedError,
324 notFound: NotFoundError,
325 noValidPlaylist: NoValidPlaylistError,
326 maintenanceMode: MaintenanceError
327};
328
329var errorClasses = {
330 RequestError,
331 BadLoginError,
332 NotAuthorizedError,
333 NotFoundError,
334 NoValidPlaylistError,
335 NotEnoughPPError,
336 MaintenanceError
337};
338
339function wrapResponseError (response, cause) {
340 var ref = response.body;
341 var status = ref.status;
342 var data = ref.data;
343 // Special cases
344 if (status === 'requestError' && data && data[0] === 'fundsPP') {
345 return new NotEnoughPPError(response, cause)
346 }
347
348 var ErrorClass = statusToError[status];
349 if (ErrorClass) {
350 return new ErrorClass(response, cause)
351 }
352 setup.call(cause, response, cause);
353 return cause
354}
355
356/**
357 * Reverse plug.dj's chat message escaping.
358 */
359function unescape (message) {
360 return message
361 .replace(/&#34;/g, '"')
362 .replace(/&#39;/g, '\'')
363 .replace(/&lt;/g, '<')
364 .replace(/&gt;/g, '>')
365 .replace(/&amp;/g, '&')
366}
367
368/**
369 * Add methods to an object's prototype.
370 */
371function makeProto (obj, methods) {
372 var proto = copyProperties({
373 toJSON: () => obj
374 }, methods);
375 var instance = Object.create(proto);
376 copyProperties(instance, obj);
377 return instance
378}
379
380function wrapMessage (mp, message) {
381 if (message.un) {
382 message.un = unescape(message.un);
383 }
384
385 message.id = message.cid;
386 message.message = unescape(message.message);
387 message.user = mp.user(message.uid) ||
388 mp.wrapUser({ id: message.uid, username: message.un });
389
390 return makeProto(message, {
391 own: () => mp.me().id === message.uid,
392
393 chat: mp.chat,
394 reply: partial(mp.chat, `@${message.un}`),
395 emote: partial(mp.chat, `/me @${message.un}`),
396
397 delete: partial(mp.deleteChat, message.cid),
398
399 getUser () {
400 var user = mp.user && mp.user(message.uid);
401 return user ? Promise.resolve(user) : mp.getUser(message.uid)
402 }
403 })
404}
405
406function wrapHistoryEntry (mp, rawEntry) {
407 var timestamp = parseDate(rawEntry.timestamp);
408
409 rawEntry.timestamp = timestamp;
410 // wrapMedia expects a playlist ID, but we don't know it--pass null instead.
411 rawEntry.media = mp.wrapMedia(null, rawEntry.media);
412 rawEntry.user = mp.wrapUser(rawEntry.user);
413
414 if (rawEntry.room) {
415 rawEntry.room = mp.wrapRoom(rawEntry.room);
416 }
417
418 return makeProto(rawEntry, {
419 get time () { throw new Error('miniplug: \'HistoryEntry.time\' was renamed to \'HistoryEntry.timestamp\' in v1.8.0. Please update your code.') },
420 get dj () { throw new Error('miniplug: \'HistoryEntry.dj\' does not work, use \'HistoryEntry.user\' instead. Please update your code.') },
421 getUser: partial(mp.getUser, rawEntry.user.id),
422 skip: partial(mp.skipDJ, rawEntry.user.id, rawEntry.id)
423 })
424}
425
426function wrapInventoryProduct (mp, product) {
427 // Rename the product.id property. The `id` property on inventory products
428 // corresponds to the `name` property on store products in the plug.dj API.
429 // Miniplug uses `name` for both for consistency.
430 product.name = product.id;
431 delete product.id;
432
433 return makeProto(product, {})
434}
435
436function wrapMedia (mp, playlistId, media) {
437 return makeProto(media, {
438 update: partial(mp.updateMedia, playlistId, media.id),
439 delete: () =>
440 mp.deleteMedia(playlistId, [ media.id ])
441 })
442}
443
444function wrapNotification (mp, notif) {
445 notif.id = Number(notif.id);
446 notif.timestamp = parseDate(notif.timestamp);
447
448 if (notif.action === 'levelUp') {
449 notif.level = Number(notif.value);
450 }
451
452 if (notif.action === 'gift') {
453 var parts = notif.value.split('\u2800');
454 notif.from = parts[0];
455 notif.amount = Number(parts[1]);
456 }
457
458 if (notif.action === 'custom') {
459 notif.message = notif.value;
460 }
461
462 return makeProto(notif, {
463 acknowledge: partial(mp.acknowledgeNotification, notif.id)
464 })
465}
466
467function wrapPlaylist (mp, playlist) {
468 return makeProto(playlist, {
469 delete: partial(mp.deletePlaylist, playlist.id),
470 activate: partial(mp.activatePlaylist, playlist.id),
471 rename: partial(mp.renamePlaylist, playlist.id),
472 shuffle: partial(mp.shufflePlaylist, playlist.id),
473
474 getMedia: () =>
475 mp.getMedia(playlist.id)
476 .map(partial(mp.wrapMedia, playlist.id))
477 .tap(media => {
478 // cachedMedia = media
479 }),
480 insert: (media, append) =>
481 {
482 if ( append === void 0 ) append = true;
483
484 return mp.insertMedia(playlist.id, media, append);
485 },
486 move: (media, before) =>
487 mp.moveMedia(playlist.id, media, before)
488 })
489}
490
491function wrapRoom (mp, room) {
492 if (room.welcome) {
493 room.welcome = unescape(room.welcome);
494 }
495 if (room.description) {
496 room.description = unescape(room.description);
497 }
498
499 room.isFavorite = room.favorite;
500 delete room.favorite;
501
502 return makeProto(room, {
503 getHost: partial(mp.getUser, room.hostID),
504
505 join: partial(mp.join, room.slug),
506
507 favorite: partial(mp.favoriteRoom, room.id),
508 unfavorite: partial(mp.unfavoriteRoom, room.id)
509 })
510}
511
512function wrapStoreProduct (mp, product) {
513 return makeProto(product, {
514 purchase: partial(mp.purchase, product.id)
515 })
516}
517
518function wrapUser (mp, user) {
519 user.username = user.guest ? null : unescape(user.username || '');
520
521 // Chat mention string for this user.
522 var mention = `@${user.username}`;
523
524 return makeProto(user, {
525 hasPermission: (role) =>
526 (user.role >= role || user.gRole >= role),
527 hasGlobalPermission: (role) =>
528 user.gRole >= role,
529
530 chat: partial(mp.chat, mention),
531 emote: partial(mp.chat, `/me ${mention}`),
532
533 add: partial(mp.addDJ, user.id),
534 move: partial(mp.moveDJ, user.id),
535 remove: partial(mp.removeDJ, user.id),
536 skip: partial(mp.skipDJ, user.id),
537
538 befriend: partial(mp.befriend, user.id),
539 rejectRequest: partial(mp.rejectFriendRequest, user.id),
540
541 ignore: partial(mp.ignore, user.id),
542 unignore: partial(mp.unignore, user.id),
543
544 mute: partial(mp.mute, user.id),
545 unmute: partial(mp.unmute, user.id),
546
547 ban: partial(mp.ban, user.id),
548 unban: partial(mp.unban, user.id),
549
550 waitlistBan: partial(mp.waitlistBan, user.id),
551 waitlistUnban: partial(mp.waitlistUnban, user.id),
552
553 getHistory: partial(mp.getUserHistory, user.id),
554
555 mention: () => mention,
556 // Allows mentioning users by doing `Hello ${user}` in chat messages.
557 toString: () => mention
558 })
559}
560
561// Have to use a real class to get working inheritance from Array. Methods are
562// still added inside wrapWaitlist, as usual.
563class Waitlist extends Array {}
564
565function wrapWaitlist (mp, waitlist) {
566 var wrapped = Object.assign(new Waitlist(), {
567 contains: (user) =>
568 wrapped.some((waiting) => waiting.id === getId(user)),
569 positionOf: (user) =>
570 wrapped.findIndex((waiting) => waiting.id === getId(user)),
571 toJSON: () => waitlist
572 });
573
574 waitlist.forEach((id) => {
575 wrapped.push(mp.user(id) || mp.wrapUser({ id: id }));
576 });
577
578 return wrapped
579}
580
581function wrapWaitlistBan (mp, data) {
582 data.username = unescape(data.username);
583 data.moderator = unescape(data.moderator);
584
585 var ban = {
586 user: mp.user(data.id) || mp.wrapUser({ id: data.id, username: data.username }),
587 moderator: mp.user(data.moderatorID) || mp.userByName(data.moderator) ||
588 mp.wrapUser({ id: data.moderatorID, username: data.moderator }),
589 moderatorName: data.moderator,
590 reason: data.reason,
591 duration: data.duration,
592 timestamp: typeof data.timestamp === 'string'
593 ? parseDate(data.timestamp)
594 : data.timestamp
595 };
596 return makeProto(ban, {
597 getUser: partial(mp.getUser, data.id),
598 remove: partial(mp.waitlistUnban, data.id)
599 })
600}
601
602function dataModelPlugin () {
603 return (mp) => {
604 Object.assign(mp, {
605 wrapMessage: partial(wrapMessage, mp),
606 wrapHistoryEntry: partial(wrapHistoryEntry, mp),
607 wrapInventoryProduct: partial(wrapInventoryProduct, mp),
608 wrapMedia: partial(wrapMedia, mp),
609 wrapNotification: partial(wrapNotification, mp),
610 wrapPlaylist: partial(wrapPlaylist, mp),
611 wrapRoom: partial(wrapRoom, mp),
612 wrapStoreProduct: partial(wrapStoreProduct, mp),
613 wrapUser: partial(wrapUser, mp),
614 wrapWaitlist: partial(wrapWaitlist, mp),
615 wrapWaitlistBan: partial(wrapWaitlistBan, mp)
616 });
617 }
618}
619
620var Agent = https.Agent;
621var debug = createDebug('miniplug:http');
622
623function httpPlugin (httpOpts) {
624 httpOpts = Object.assign({}, {backoff: (fn) => fn,
625 agent: httpOpts.agent || new Agent({ keepAlive: true })},
626 httpOpts);
627
628 return (mp) => {
629 var request = httpOpts.backoff((url, opts) => {
630 if (!mp.connected) {
631 return Promise.reject(new Error('No connection available. Use mp.connect() before calling other functions.'))
632 }
633
634 // wait until connections are complete before sending off requests
635 return mp.connected
636 .tap(() => debug(opts.method, url, opts.body || opts.query))
637 .then((session) =>
638 fetch(`${httpOpts.host}/_/${url}`, Object.assign({}, {agent: httpOpts.agent,
639 headers: {
640 cookie: session.cookie,
641 'content-type': 'application/json'
642 }},
643 opts,
644 {body: opts.body ? JSON.stringify(opts.body) : undefined}))
645 )
646 .then((response) => {
647 return response.json().then((body) => {
648 if (!response.ok || body.status !== 'ok') {
649 throw Object.assign(new Error(
650 body.data && body.data.length
651 ? body.data[0]
652 : body.status
653 ), { response })
654 }
655 return body.data
656 })
657 })
658 .catch((err) => {
659 if (err && err.response) {
660 throw wrapResponseError(err.response, err)
661 }
662 throw err
663 })
664 });
665
666 var post = (url, data) => request(url, { method: 'post', body: data });
667 var get = (url, data) => request(url, { method: 'get', query: data });
668 var put = (url, data) => request(url, { method: 'put', body: data });
669 var del = (url, data) => request(url, { method: 'delete', body: data });
670
671 mp.request = request;
672 mp.post = post;
673 mp.get = get;
674 mp.put = put;
675 mp.del = del;
676 }
677}
678
679var debug$1 = createDebug('miniplug:connect');
680var debugWs = createDebug('miniplug:ws');
681var MaintenanceError$1 = errorClasses.MaintenanceError;
682
683function connectPlugin (options) {
684 if ( options === void 0 ) options = {};
685
686 return (mp) => {
687 var loginOpts = {
688 agent: options.agent,
689 host: options.host,
690 authToken: true
691 };
692
693 function connect (opts) {
694 if ( opts === void 0 ) opts = {};
695
696 debug$1('connecting', opts.email);
697
698 var loginPromise = Promise.resolve(
699 opts.email
700 ? login.user(opts.email, opts.password, loginOpts)
701 : login.guest(loginOpts)
702 ).catch((err) => {
703 if (err && err.status === 'maintenanceMode') {
704 throw new MaintenanceError$1(err.response, err)
705 }
706 throw err
707 });
708
709 var ws = socket();
710 ws.setMaxListeners(100);
711 ws.on('action', (type, payload) => {
712 debugWs(type, payload);
713 });
714 ws.on('close', () => {
715 mp.emit('disconnected');
716 });
717
718 var connected = loginPromise
719 .then((res) => new Promise((resolve, reject) => {
720 ws.auth(res.token);
721 ws.once('error', reject);
722
723 mp.isConnected = true;
724 mp.emit('login');
725
726 var me = mp.getMe();
727 ws.once('ack', () => {
728 resolve({ cookie: res.cookie });
729 ws.removeListener('error', reject);
730
731 me.then((user) => mp.emit('connected', user));
732 });
733 }))
734 .catch((err) => {
735 mp.emit('error', err);
736 throw err
737 });
738
739 mp.ws = ws;
740 mp.connected = connected;
741
742 // Return `mp` so you can do
743 // mp = await miniplug().connect()
744 return connected.then(() => mp)
745 }
746
747 mp.connect = connect;
748 }
749}
750
751var debug$2 = createDebug('miniplug:users');
752
753var GUEST_ID = 0;
754
755var kGuestsCount = Symbol('Guests count');
756var kUsers = Symbol('Users');
757var kCurrentUser = Symbol('Me');
758
759function usersPlugin () {
760 return (mp) => {
761 // local user cache/collection API
762 mp[kGuestsCount] = 0;
763 mp[kUsers] = [];
764
765 function onUserJoin (user) {
766 debug$2('join', user.id);
767 if (user.guest) {
768 mp[kGuestsCount] += 1;
769 mp.emit('guestJoin');
770 } else {
771 user = mp.wrapUser(user);
772 mp[kUsers].push(user);
773 mp.emit('userJoin', user);
774 }
775 }
776
777 function onUserLeave (id) {
778 debug$2('leave', id);
779 if (id === GUEST_ID) {
780 if (mp[kGuestsCount] > 0) {
781 mp[kGuestsCount] -= 1;
782 }
783 mp.emit('guestLeave');
784 } else {
785 var i = mp[kUsers].findIndex((user) => user.id === id);
786 if (i !== -1) {
787 var user = mp[kUsers][i];
788 mp[kUsers].splice(i, 1);
789 mp.emit('userLeave', user);
790 }
791 }
792 }
793
794 function onUserUpdate (props) {
795 var user = mp.user(props.i);
796 if (!user) { return }
797
798 delete props.i;
799 var oldProps = Object.keys(props).reduce((o, name) => {
800 o[name] = user[name];
801 return o
802 }, {});
803
804 Object.assign(user, props);
805
806 mp.emit('userUpdate', user, oldProps);
807 }
808
809 // Emit `userUpdate` events for `role` updates.
810 function onStaffUpdate (props) {
811 props.u.forEach((ref) => {
812 onUserUpdate({ i: ref.i, role: ref.p });
813
814 mp.emit('modStaff', {
815 moderator: mp.user(props.mi) || mp.wrapUser({ id: props.mi, username: props.m }),
816 user: mp.user(ref.i) || mp.wrapUser({ id: ref.i, username: ref.n }),
817 role: ref.p
818 });
819 });
820 }
821
822 mp.on('connected', (user) => {
823 mp[kCurrentUser] = mp.wrapUser(user);
824
825 mp.ws.on('userJoin', onUserJoin);
826 mp.ws.on('userLeave', onUserLeave);
827 mp.ws.on('userUpdate', onUserUpdate);
828 mp.ws.on('modStaff', onStaffUpdate);
829 });
830
831 // keeping things in sync
832 mp.on('roomState', (ref) => {
833 var users = ref.users;
834 var guests = ref.meta.guests;
835
836 mp[kUsers] = users.map(mp.wrapUser);
837 mp[kGuestsCount] = guests;
838 });
839
840 var me = () => mp[kCurrentUser];
841 var userByProp = (prop) => (value) => {
842 if (mp[kCurrentUser] && value === mp[kCurrentUser][prop]) {
843 return mp[kCurrentUser]
844 }
845 return mp[kUsers].find((user) => user[prop] === value)
846 };
847 var user = userByProp('id');
848 var userByName = userByProp('username');
849 // Maybe include `me()` in the `users()` list in v3.0.0? I'm not
850 // sure which is more expected.
851 var users = () => mp[kUsers];
852 var guests = () => mp[kGuestsCount];
853
854 // REST API
855 var getMe = () => mp.get('users/me').get(0).then(mp.wrapUser);
856 var getUser = uid => mp.get(`users/${uid}`).get(0).then(mp.wrapUser);
857 // pass IDs or arrays of IDs
858 var getUsers = function () {
859 var uids = [], len = arguments.length;
860 while ( len-- ) uids[ len ] = arguments[ len ];
861
862 return mp.post('users/bulk', {
863 ids: flatten(uids)
864 }).map(mp.wrapUser);
865 };
866
867 // current user & profile
868 var saveSettings = partial(mp.put, 'users/settings');
869 var setAvatar = (avatar) =>
870 mp.put('users/avatar', { id: avatar });
871 var setBadge = (badge) =>
872 mp.put('users/badge', { id: badge });
873 var setBlurb = (blurb) =>
874 mp.put('profile/blurb', { blurb: blurb });
875 var setLanguage = (lang) =>
876 mp.put('users/language', { language: lang });
877 var getTransactions = partial(mp.get, 'users/me/transactions');
878
879 var validateUsername = (name) =>
880 mp.get(`users/validate/${encodeURIComponent(name)}`)
881 .get(0)
882 .catch(() => Promise.reject(new Error('Username unavailable.')));
883
884 // Public API
885 Object.assign(mp, {
886 // local
887 me,
888 user,
889 userByName,
890 users,
891 guests,
892 // remote
893 getMe,
894 getUser,
895 getUsers,
896 // setters
897 saveSettings,
898 setAvatar,
899 setBadge,
900 setBlurb,
901 setLanguage,
902
903 getTransactions,
904 validateUsername
905 });
906 }
907}
908
909function ignoresPlugin () {
910 return (mp) => {
911 var getIgnoredUsers = partial(mp.get, 'ignores');
912 var ignore = (uid) =>
913 mp.post('ignores', { id: uid }).get(0);
914 var unignore = (uid) =>
915 mp.del(`ignores/${uid}`);
916
917 Object.assign(mp, {
918 getIgnoredUsers,
919 ignore,
920 unignore
921 });
922 }
923}
924
925function mutesPlugin () {
926 return (mp) => {
927 var getMutes = partial(mp.get, 'mutes');
928 var mute = (uid, duration, reason) =>
929 {
930 if ( duration === void 0 ) duration = MUTE_DURATION.SHORT;
931 if ( reason === void 0 ) reason = MUTE_REASON.VIOLATING_RULES;
932
933 return mp.post('mutes', {
934 userID: uid,
935 duration: duration,
936 reason: reason
937 });
938 };
939 var unmute = (uid) =>
940 mp.del(`mutes/${uid}`);
941
942 Object.assign(mp, {
943 getMutes,
944 mute,
945 unmute
946 });
947
948 function onModMute (ref) {
949 mp.emit('modMute', {
950 moderator: mp.user(ref.mi) || mp.wrapUser({ id: ref.mi, username: ref.m }),
951 user: mp.user(ref.i) || mp.wrapUser({ id: ref.i, username: ref.t }),
952 reason: ref.r,
953 duration: ref.d
954 });
955 }
956
957 mp.on('connected', (user) => {
958 mp.ws.on('modMute', onModMute);
959 });
960 }
961}
962
963function bansPlugin () {
964 return (mp) => {
965 var getBans = partial(mp.get, 'bans');
966 var ban = (uid, duration, reason) =>
967 {
968 if ( duration === void 0 ) duration = BAN_DURATION.HOUR;
969 if ( reason === void 0 ) reason = BAN_REASON.SPAMMING;
970
971 return mp.post('bans/add', {
972 userID: uid,
973 reason: reason,
974 duration: duration
975 }).get(0);
976 };
977 var unban = (uid) =>
978 mp.del(`bans/${uid}`);
979
980 Object.assign(mp, {
981 getBans,
982 ban,
983 unban
984 });
985
986 function onBan (ref) {
987 mp.emit('ban', {
988 duration: ref.d,
989 reason: ref.r
990 });
991 }
992
993 function onModBan (ref) {
994 mp.emit('modBan', {
995 moderator: mp.user(ref.mi) || mp.wrapUser({ id: ref.mi }),
996 username: ref.t,
997 duration: ref.d
998 });
999 }
1000
1001 mp.on('connected', (user) => {
1002 mp.ws.on('ban', onBan);
1003 mp.ws.on('modBan', onModBan);
1004 });
1005 }
1006}
1007
1008var kNotifications = Symbol('Notifications');
1009
1010function notificationsPlugin () {
1011 return (mp) => {
1012 mp[kNotifications] = [];
1013
1014 function onNotify (notif) {
1015 mp[kNotifications].push(notif);
1016 mp.emit('notify', mp.wrapNotification(notif));
1017 }
1018
1019 function onEarn (ref) {
1020 var me = mp.me();
1021 if (me) { Object.assign(me, ref); }
1022 mp.emit('earn', {
1023 xp: ref.xp,
1024 pp: ref.pp,
1025 level: ref.level
1026 });
1027 }
1028
1029 function onSub (sub) {
1030 var me = mp.me();
1031 if (me) { me.sub = sub; }
1032 mp.emit('sub', sub);
1033 }
1034
1035 function onGift (pp) {
1036 var me = mp.me();
1037 if (me) { Object.assign(me, { pp }); }
1038 mp.emit('gift', { pp });
1039 }
1040
1041 mp.on('connected', (user) => {
1042 mp[kNotifications] = (user && user.notifications) || [];
1043
1044 mp.ws.on('notify', onNotify);
1045 mp.ws.on('earn', onEarn);
1046 mp.ws.on('sub', onSub);
1047 mp.ws.on('gift', onGift);
1048 });
1049
1050 Object.assign(mp, {
1051 notifications: () => mp[kNotifications].map(mp.wrapNotification),
1052
1053 getNotifications: () =>
1054 mp.getMe()
1055 .then((me) => me.notifications || [])
1056 .tap((notifs) => {
1057 mp[kNotifications] = notifs;
1058 })
1059 .map(mp.wrapNotification),
1060
1061 acknowledgeNotification: (id) =>
1062 mp.del(`notifications/${id}`).tap(() => {
1063 // Remove the notification from the local notifications list.
1064 mp[kNotifications] = mp[kNotifications]
1065 .filter((notif) => notif.id !== Number(id));
1066 })
1067 });
1068 }
1069}
1070
1071var kCurrentHistoryEntry = Symbol('History entry');
1072
1073function boothPlugin (opts) {
1074 if ( opts === void 0 ) opts = {};
1075
1076 return (mp) => {
1077 // Local state API
1078 var historyEntry = () => mp[kCurrentHistoryEntry];
1079 var dj = () => mp[kCurrentHistoryEntry] ? mp[kCurrentHistoryEntry].user : null;
1080 var media = () => mp[kCurrentHistoryEntry] ? mp[kCurrentHistoryEntry].media : null;
1081
1082 mp.on('roomState', (state) => {
1083 if (!state.playback.media) {
1084 mp[kCurrentHistoryEntry] = null;
1085 return
1086 }
1087
1088 var timestamp = parseDate(state.playback.startTime);
1089
1090 mp[kCurrentHistoryEntry] = mp.wrapHistoryEntry({
1091 id: state.playback.historyID,
1092 user: mp.user(state.booth.currentDJ),
1093 media: state.playback.media,
1094 timestamp: timestamp
1095 });
1096
1097 // only for the current historyEntry
1098 mp[kCurrentHistoryEntry].playlistId = state.playback.playlistID;
1099 });
1100
1101 // Socket API
1102 function onAdvance (event) {
1103 var previous = historyEntry();
1104 if (!event || !event.m) {
1105 mp[kCurrentHistoryEntry] = null;
1106 mp.emit('advance', null, previous);
1107 return
1108 }
1109
1110 var historyId = event.h;
1111 var media = event.m;
1112 var djId = event.c;
1113 var playlistId = event.p;
1114 var timestamp = event.t;
1115
1116 mp[kCurrentHistoryEntry] = mp.wrapHistoryEntry({
1117 id: historyId,
1118 user: mp.user(djId) || mp.wrapUser({ id: djId }),
1119 media: media,
1120 timestamp: timestamp
1121 });
1122
1123 // only for the current historyEntry
1124 mp[kCurrentHistoryEntry].playlistId = playlistId;
1125
1126 mp.emit('advance', historyEntry(), previous);
1127 }
1128
1129 mp.on('connected', () => {
1130 mp.ws.on('advance', onAdvance);
1131 });
1132
1133 // Rest API
1134 Object.assign(mp, {
1135 skipDJ: (uid, hid) =>
1136 mp.post('booth/skip', { userID: uid, historyID: hid }),
1137 skipMe: partial(mp.post, 'booth/skip/me')
1138 });
1139
1140 Object.assign(mp, {
1141 historyEntry,
1142 dj,
1143 media
1144 });
1145 }
1146}
1147
1148var debug$3 = createDebug('miniplug:waitlist');
1149var kWaitlist = Symbol('Waitlist');
1150var kLocked = Symbol('Waitlist locked');
1151var kCycles = Symbol('Waitlist cycles');
1152
1153function waitlistPlugin () {
1154 return (mp) => {
1155 mp[kLocked] = false;
1156 mp[kCycles] = true;
1157 mp[kWaitlist] = mp.wrapWaitlist([]);
1158
1159 mp.on('roomState', (state) => {
1160 mp[kWaitlist] = mp.wrapWaitlist(state.booth.waitingDJs);
1161 mp[kLocked] = state.booth.isLocked;
1162 mp[kCycles] = state.booth.shouldCycle;
1163 mp.emit('waitlistUpdate', mp.waitlist(), []);
1164 });
1165
1166 function onDjListUpdate (ids) {
1167 debug$3('update', ids);
1168
1169 var previous = mp.waitlist();
1170
1171 mp[kWaitlist] = mp.wrapWaitlist(ids);
1172 mp.emit('waitlistUpdate', mp.waitlist(), previous);
1173 }
1174
1175 function onAdvance (event) {
1176 if (event && event.d) {
1177 onDjListUpdate(event.d);
1178 } else {
1179 onDjListUpdate([]);
1180 }
1181 }
1182
1183 function onModAddDj (ref) {
1184 mp.emit('modAddDj', {
1185 moderator: mp.user(ref.mi) || mp.wrapUser({ id: ref.mi, username: ref.m }),
1186 username: ref.t
1187 });
1188 }
1189
1190 function onModMoveDj (ref) {
1191 mp.emit('modMoveDj', {
1192 moderator: mp.user(ref.mi) || mp.wrapUser({ id: ref.mi }),
1193 username: ref.u,
1194 movedFrom: ref.o,
1195 movedTo: ref.n
1196 });
1197 }
1198
1199 function onModRemoveDj (ref) {
1200 mp.emit('modRemoveDj', {
1201 moderator: mp.user(ref.mi) || mp.wrapUser({ id: ref.mi }),
1202 username: ref.t,
1203 inBooth: ref.d
1204 });
1205 }
1206
1207 function onModSkip (ref) {
1208 mp.emit('modSkip', mp.user(ref.mi) || mp.wrapUser({ id: ref.mi, username: ref.m }));
1209 }
1210
1211 function onDjListCycle (ref) {
1212 var f = ref.f;
1213 var mi = ref.mi;
1214
1215 mp[kCycles] = f;
1216
1217 mp.emit('waitlistCycle', {
1218 shouldCycle: f,
1219 user: mp.user(mi) || mp.wrapUser({ id: mi })
1220 });
1221 }
1222
1223 function onDjListLocked (ref) {
1224 var f = ref.f;
1225 var c = ref.c;
1226 var mi = ref.mi;
1227
1228 mp[kLocked] = f;
1229
1230 var user = mp.user(mi) || mp.wrapUser({ id: mi });
1231 mp.emit('waitlistLock', {
1232 locked: f,
1233 cleared: !!c,
1234 user
1235 });
1236
1237 if (c) {
1238 mp.emit('waitlistClear', { user });
1239 }
1240 }
1241
1242 function onSkip (ref) {
1243 mp.emit('skip', mp.user(ref) || mp.wrapUser({ id: ref }));
1244 }
1245
1246 mp.on('connected', () => {
1247 mp.ws.on('djListUpdate', onDjListUpdate);
1248 mp.ws.on('advance', onAdvance);
1249 mp.ws.on('modAddDJ', onModAddDj);
1250 mp.ws.on('modMoveDJ', onModMoveDj);
1251 mp.ws.on('modRemoveDJ', onModRemoveDj);
1252 mp.ws.on('modSkip', onModSkip);
1253 mp.ws.on('djListCycle', onDjListCycle);
1254 mp.ws.on('djListLocked', onDjListLocked);
1255 mp.ws.on('skip', onSkip);
1256 });
1257
1258 var setCycle = (val) =>
1259 {
1260 if ( val === void 0 ) val = true;
1261
1262 return mp.put('booth/cycle', { shouldCycle: val });
1263 };
1264
1265 Object.assign(mp, {
1266 waitlist: () => mp[kWaitlist],
1267
1268 joinWaitlist: partial(mp.post, 'booth'),
1269 leaveWaitlist: partial(mp.del, 'booth'),
1270
1271 isCycling: () => mp[kCycles],
1272 setCycle,
1273 enableCycle: partial(setCycle, true),
1274 disableCycle: partial(setCycle, false),
1275
1276 isLocked: () => mp[kLocked],
1277 setLock: (locked, clear) =>
1278 {
1279 if ( locked === void 0 ) locked = true;
1280 if ( clear === void 0 ) clear = false;
1281
1282 return mp.put('booth/lock', { isLocked: locked, removeAllDJs: clear });
1283 },
1284 lockWaitlist: (clear) =>
1285 {
1286 if ( clear === void 0 ) clear = false;
1287
1288 return mp.setLock(true, clear);
1289 },
1290 unlockWaitlist: () =>
1291 mp.setLock(false, false),
1292
1293 addDJ: (uid) =>
1294 mp.post('booth/add', { id: uid }),
1295 moveDJ: (uid, pos) =>
1296 mp.post('booth/move', { userID: uid, position: pos }),
1297 removeDJ: (uid) =>
1298 mp.del(`booth/remove/${uid}`)
1299 });
1300 }
1301}
1302
1303function waitlistBansPlugin () {
1304 return (mp) => {
1305 var getWaitlistBans = () =>
1306 mp.get('booth/waitlistban').map(mp.wrapWaitlistBan);
1307 var waitlistBan = (id, duration, reason) =>
1308 {
1309 if ( duration === void 0 ) duration = WAITLIST_BAN_DURATION.SHORT;
1310 if ( reason === void 0 ) reason = WAITLIST_BAN_REASON.SPAMMING;
1311
1312 return mp.post('booth/waitlistban', {
1313 userID: id,
1314 duration,
1315 reason
1316 });
1317 };
1318 var waitlistUnban = (id) =>
1319 mp.del(`booth/waitlistban/${id}`);
1320
1321 mp.on('connected', () => {
1322 mp.ws.on('modWaitlistBan', (ban) => {
1323 var wrappedBan = mp.wrapWaitlistBan({
1324 moderator: ban.m,
1325 moderatorID: ban.mi,
1326 username: ban.t,
1327 id: ban.ti,
1328 duration: ban.d,
1329 reason: null,
1330 timestamp: new Date()
1331 });
1332 mp.emit('modWaitlistBan', wrappedBan);
1333 });
1334 });
1335
1336 // Deprecations
1337 mp.on('newListener', (name) => {
1338 if (name === 'waitlistBan') {
1339 throw new Error('miniplug: the \'waitlistBan\' event was renamed to \'modWaitlistBan\' in 1.11.0. Please update your code.')
1340 }
1341 });
1342
1343 Object.assign(mp, {
1344 getWaitlistBans,
1345 waitlistBan,
1346 waitlistUnban
1347 });
1348 }
1349}
1350
1351function historyPlugin () {
1352 return (mp) => {
1353 var getRoomHistory = () =>
1354 mp.get('rooms/history').map(mp.wrapHistoryEntry);
1355
1356 // Get the history for the logged-in user.
1357 var getOwnHistory = () =>
1358 mp.get('users/me/history').map(mp.wrapHistoryEntry);
1359
1360 var getProfile = (id) =>
1361 mp.get(`profile/${id}`).get(0);
1362
1363 // Get the history for another user.
1364 var getOtherHistory = (id) =>
1365 getProfile(id).get('history').map(mp.wrapHistoryEntry);
1366
1367 // Get the history for a user. Defaults to the current user.
1368 var getUserHistory = (id) =>
1369 {
1370 if ( id === void 0 ) id = mp.me().id;
1371
1372 return mp.me().id === id ? getOwnHistory() : getOtherHistory(id);
1373 };
1374
1375 Object.assign(mp, {
1376 getRoomHistory,
1377 getUserHistory
1378 });
1379 }
1380}
1381
1382var debug$4 = createDebug('miniplug:chat');
1383
1384function chatPlugin (opts) {
1385 opts = Object.assign({}, {timeout: 7000,
1386 // allow users to pass their own rate limiting function,
1387 // this will get a sensible default at some point
1388 backoff: (fn) => fn},
1389 opts);
1390
1391 return (mp) => {
1392 // translate raw socket events to wrapped miniplug events
1393 function onChat (msg) {
1394 debug$4('chat', msg.uid, msg.un, msg.message);
1395 mp.emit('chat', mp.wrapMessage(msg));
1396 }
1397
1398 function onChatDelete (ref) {
1399 var c = ref.c;
1400 var mi = ref.mi;
1401
1402 debug$4('chatDelete', mi, c);
1403 mp.emit('chatDelete', {
1404 cid: c,
1405 user: mp.user(mi) || mp.wrapUser({ id: mi })
1406 });
1407 }
1408
1409 function onGifted (ref) {
1410 mp.emit('gifted', {
1411 sender: ref.s,
1412 recipient: ref.r
1413 });
1414 }
1415
1416 mp.on('connected', () => {
1417 mp.ws.on('chat', onChat);
1418 mp.ws.on('chatDelete', onChatDelete);
1419 mp.ws.on('gifted', onGifted);
1420 });
1421
1422 // REST API
1423 var deleteChat = (cid) =>
1424 mp.del(`chat/${cid}`);
1425 var getChatHistory = () =>
1426 mp.get(`chat/history`)
1427 .then((d) => d[0].history.map((msg) => Object.assign(
1428 mp.wrapMessage(msg),
1429 { user: mp.wrapUser(d[0].users.find((u) => u.id === msg.uid)) }
1430 )));
1431
1432 // Socket API
1433 var chat = opts.backoff(function () {
1434 var args = [], len = arguments.length;
1435 while ( len-- ) args[ len ] = arguments[ len ];
1436
1437 var ws = mp.ws;
1438 var message = args.join(' ');
1439 debug$4('send', message);
1440 ws.chat(message);
1441 // attempt to resolve when the message comes back
1442 return new Promise((resolve, reject) => {
1443 var intv = setTimeout(() => {
1444 mp.removeListener('chat', onChat);
1445 reject(new Error('Waiting for chat message to come back timed out.'));
1446 }, opts.timeout);
1447
1448 var onChat = (chat) => {
1449 debug$4('is same?', chat.own(), message, chat.message);
1450 // TODO ensure that chat messages are unescaped enough for this
1451 // (they aren't)
1452 if (chat.own() && chat.message === message) {
1453 mp.removeListener('chat', onChat);
1454 clearTimeout(intv);
1455 resolve(chat);
1456 }
1457 };
1458
1459 mp.on('chat', onChat);
1460 })
1461 });
1462 var emote = partial(chat, '/me');
1463
1464 // Public API
1465 Object.assign(mp, {
1466 deleteChat,
1467 getChatHistory,
1468 chat,
1469 emote
1470 });
1471 }
1472}
1473
1474function friendsPlugin () {
1475 return (mp) => {
1476 // Friendship events only include the requesting/accepting user's name.
1477 // Plug.dj refreshes its client-side friends or invites list when these
1478 // events come in. Miniplug requests the friends or invites list and emits
1479 // the events with a user object, so it's easy to respond to the events
1480 // using the `user.befriend` and `user.rejectRequest` methods.
1481 function onFriendRequest (name) {
1482 getFriendRequests().each((request) => {
1483 if (unescape(request.username) === unescape(name)) {
1484 mp.emit('friendRequest', mp.wrapUser(request));
1485 }
1486 });
1487 }
1488
1489 function onFriendAccept (name) {
1490 getFriends().each((friend) => {
1491 if (friend.username === unescape(name)) {
1492 mp.emit('friendAccept', friend);
1493 }
1494 });
1495 }
1496
1497 mp.on('connected', () => {
1498 mp.ws.on('friendRequest', onFriendRequest);
1499 mp.ws.on('friendAccept', onFriendAccept);
1500 });
1501
1502 // REST Friend API
1503 var getFriends = () =>
1504 mp.get('friends').map(mp.wrapUser);
1505 var befriend = (uid) =>
1506 mp.post('friends', { id: uid });
1507 var unfriend = (uid) =>
1508 mp.del(`friends/${uid}`);
1509 var getFriendRequests = partial(mp.get, 'friends/invites');
1510 var rejectFriendRequest = (uid) =>
1511 mp.put('friends/ignore', { id: uid });
1512
1513 // Public API
1514 Object.assign(mp, {
1515 getFriends,
1516 befriend,
1517 unfriend,
1518 getFriendRequests,
1519 rejectFriendRequest
1520 });
1521 }
1522}
1523
1524var debug$5 = createDebug('miniplug:rooms');
1525var kCurrentRoom = Symbol('Current room');
1526
1527function roomsPlugin () {
1528 return (mp) => {
1529 mp[kCurrentRoom] = null;
1530
1531 mp.on('roomState', (state) => {
1532 mp[kCurrentRoom] = mp.wrapRoom(state.meta);
1533 });
1534
1535 // Add a handler to process a room property update.
1536 //
1537 // * `eventName` - Plug.dj socket event name for the property.
1538 // * `sockProp` - Property name of the new value on the plug.dj socket
1539 // event parameter.
1540 // * `roomProp` - Property name for the value on the miniplug room object.
1541 function addUpdateHandler (eventName, sockProp, roomProp) {
1542 mp.ws.on(eventName, (data, targetSlug) => {
1543 var user = mp.user(data.u) || mp.wrapUser({ id: data.u });
1544 var value = data[sockProp];
1545
1546 debug$5(eventName, user && user.id, value);
1547
1548 if (mp[kCurrentRoom] && mp[kCurrentRoom].slug === targetSlug) {
1549 mp[kCurrentRoom][roomProp] = value;
1550 }
1551
1552 mp.emit(eventName, value, user);
1553 mp.emit('roomUpdate', {
1554 [roomProp]: value
1555 }, user);
1556 });
1557 }
1558
1559 mp.on('connected', () => {
1560 addUpdateHandler('roomNameUpdate', 'n', 'name');
1561 addUpdateHandler('roomDescriptionUpdate', 'd', 'description');
1562 addUpdateHandler('roomWelcomeUpdate', 'w', 'welcome');
1563 addUpdateHandler('roomMinChatLevelUpdate', 'm', 'minChatLevel');
1564 });
1565
1566 var room = () => mp[kCurrentRoom];
1567
1568 var getRooms = (query, page, limit) =>
1569 {
1570 if ( query === void 0 ) query = '';
1571 if ( page === void 0 ) page = 0;
1572 if ( limit === void 0 ) limit = 50;
1573
1574 return mp.get('rooms', { q: query, page, limit })
1575 .map(mp.wrapRoom);
1576 };
1577 var getFavorites = (query, page, limit) =>
1578 {
1579 if ( query === void 0 ) query = '';
1580 if ( page === void 0 ) page = 0;
1581 if ( limit === void 0 ) limit = 50;
1582
1583 return mp.get('rooms/favorites', { q: query, page, limit })
1584 .map(mp.wrapRoom);
1585 };
1586 var createRoom = (name, isPrivate) =>
1587 {
1588 if ( isPrivate === void 0 ) isPrivate = false;
1589
1590 return mp.post('rooms', { name: name, private: isPrivate }).get(0);
1591 };
1592 var getMyRooms = () =>
1593 mp.get('rooms/me').map(mp.wrapRoom);
1594
1595 var favoriteRoom = (rid) =>
1596 mp.post('rooms/favorites', { id: rid });
1597 var unfavoriteRoom = (rid) =>
1598 mp.del(`rooms/favorites/${rid}`);
1599 var join = (slug) =>
1600 mp.post('rooms/join', { slug: slug })
1601 .then(getRoomState)
1602 .then(room); // Return the current Room instance.
1603 var getRoomState = () =>
1604 mp.get('rooms/state').get(0)
1605 .tap(mp.emit.bind(mp, 'roomState'));
1606
1607 var updateRoom = (patch) => {
1608 if (!room()) {
1609 return Promise.reject(new Error('You are not currently in a room.'))
1610 }
1611 return mp.post('rooms/update', patch)
1612 };
1613
1614 var setRoomWelcome = (welcome) => updateRoom({ welcome });
1615 var setRoomDescription = (description) => updateRoom({ description });
1616 var setRoomName = (name) => updateRoom({ name });
1617 var setRoomMinChatLevel = (minChatLevel) => updateRoom({ minChatLevel });
1618
1619 var validateRoomName = (name) =>
1620 mp.get(`rooms/validate/${encodeURIComponent(name)}`)
1621 .get(0)
1622 .catch(() => Promise.reject(new Error('Room name unavailable.')));
1623
1624 Object.assign(mp, {
1625 // local
1626 room,
1627 // remote
1628 getRooms,
1629 getFavorites,
1630 getMyRooms,
1631 createRoom,
1632 validateRoomName,
1633 setRoomWelcome,
1634 setRoomDescription,
1635 setRoomName,
1636 setRoomMinChatLevel,
1637 // favorites
1638 favoriteRoom,
1639 unfavoriteRoom,
1640 // joining
1641 join,
1642 getRoomState
1643 });
1644 }
1645}
1646
1647function playlistsPlugin () {
1648 return (mp) => {
1649 mp.on('connected', () => {
1650 mp.ws.on('playlistCycle', (id) => {
1651 mp.emit('playlistCycle', id);
1652 });
1653 });
1654
1655 Object.assign(mp, {
1656 getPlaylists: () =>
1657 mp.get('playlists').map(mp.wrapPlaylist),
1658 getActivePlaylist: () =>
1659 mp.get('playlists')
1660 .filter((playlist) => playlist.active)
1661 .get(0).then(mp.wrapPlaylist),
1662 createPlaylist: (name/*, initialMedia */) =>
1663 mp.post('playlists', { name: name }),
1664 deletePlaylist: (pid) =>
1665 mp.del(`playlists/${pid}`),
1666 activatePlaylist: (pid) =>
1667 mp.put(`playlists/${pid}/activate`),
1668 renamePlaylist: (pid, name) =>
1669 mp.put(`playlists/${pid}/rename`, { name: name }),
1670 shufflePlaylist: (pid) =>
1671 mp.put(`playlists/${pid}/shuffle`),
1672
1673 getMedia: (pid) =>
1674 mp.get(`playlists/${pid}/media`).map(partial(mp.wrapMedia, pid)),
1675 updateMedia: (pid, mid, author, title) =>
1676 mp.put(`playlists/${pid}/media/update`, {
1677 id: mid,
1678 author: author,
1679 title: title
1680 }).get(0),
1681 moveMedia: (pid, mids, before) =>
1682 mp.put(`playlists/${pid}/media/move`, {
1683 ids: getIds(mids),
1684 beforeID: getId(before)
1685 }),
1686 insertMedia: (pid, media, append) =>
1687 {
1688 if ( append === void 0 ) append = true;
1689
1690 return mp.post(`playlists/${pid}/media/insert`, {
1691 media: media,
1692 append: append
1693 });
1694 },
1695 deleteMedia: (pid, mids) =>
1696 mp.post(`playlists/${pid}/media/delete`, {
1697 ids: getIds(mids)
1698 })
1699 });
1700 }
1701}
1702
1703function storePlugin () {
1704 return (mp) => {
1705 var getProducts = (type, category) =>
1706 {
1707 if ( category === void 0 ) category = 'all';
1708
1709 return mp.get(`store/products/${type}/${category}`).map(mp.wrapStoreProduct);
1710 };
1711 var getStoreAvatars = partial(getProducts, 'avatars');
1712 var getStoreBadges = partial(getProducts, 'badges');
1713 var getStoreMisc = partial(getProducts, 'misc');
1714
1715 var getInventory = (type) =>
1716 mp.get(`store/inventory/${type}`).map(mp.wrapInventoryProduct);
1717 var getOwnedAvatars = partial(getInventory, 'avatars');
1718 var getOwnedBadges = partial(getInventory, 'badges');
1719
1720 var purchase = (product) =>
1721 mp.post('store/purchase', { id: getId(product) }).get(0);
1722
1723 var getNameChangeProductId = () =>
1724 getStoreMisc('username').get(0).get('id');
1725 var purchaseNameChange = (username) =>
1726 Promise.props({
1727 id: getNameChangeProductId(),
1728 username
1729 }).then(
1730 partial(mp.post, 'store/purchase/username')
1731 );
1732
1733 Object.assign(mp, {
1734 getProducts,
1735 getStoreAvatars,
1736 getStoreBadges,
1737 getStoreMisc,
1738 getInventory,
1739 getOwnedAvatars,
1740 getOwnedBadges,
1741 purchase,
1742 purchaseNameChange
1743 });
1744 }
1745}
1746
1747function systemPlugin () {
1748 return (mp) => {
1749 function onMessage (text) {
1750 mp.emit('systemMessage', text);
1751 }
1752
1753 function onMaintenanceAlert (minutesLeft) {
1754 mp.emit('maintenanceAlert', minutesLeft);
1755 }
1756
1757 function onMaintenance () {
1758 mp.emit('maintenance');
1759 }
1760
1761 function onUpdate () {
1762 mp.emit('systemUpdate');
1763 }
1764
1765 mp.on('connected', () => {
1766 mp.ws.on('plugMessage', onMessage);
1767 mp.ws.on('plugMaintenanceAlert', onMaintenanceAlert);
1768 mp.ws.on('plugMaintenance', onMaintenance);
1769 mp.ws.on('plugUpdate', onUpdate);
1770 });
1771 }
1772}
1773
1774var kVoteStats = Symbol('Votes');
1775var kGrabs = Symbol('Grabs');
1776
1777function votePlugin (opts) {
1778 if ( opts === void 0 ) opts = {};
1779
1780 return function (mp) {
1781 mp[kVoteStats] = [];
1782 mp[kGrabs] = [];
1783
1784 mp.on('advance', () => {
1785 mp[kVoteStats] = [];
1786 mp[kGrabs] = [];
1787 });
1788
1789 function onVote (ref) {
1790 var i = ref.i;
1791 var v = ref.v;
1792
1793 mp[kVoteStats].push({
1794 uid: i,
1795 vote: v
1796 });
1797 var user = mp.user(i) || mp.wrapUser({ id: i });
1798 mp.emit('vote', {
1799 user,
1800 vote: v
1801 });
1802 }
1803
1804 function onGrab (uid) {
1805 mp[kGrabs].push(uid);
1806 var user = mp.user(uid) || mp.wrapUser({ id: uid });
1807 mp.emit('grab', user);
1808 }
1809
1810 mp.on('connected', () => {
1811 mp.ws.on('vote', onVote);
1812 mp.ws.on('grab', onGrab);
1813 });
1814
1815 mp.on('roomState', (state) => {
1816 mp[kGrabs] = Object.keys(state.grabs);
1817 mp[kVoteStats] = Object.keys(state.votes).map((uid) => ({
1818 uid,
1819 vote: state.votes[uid]
1820 }));
1821 });
1822
1823 mp.score = () => {
1824 var map = mp[kVoteStats].reduce(
1825 (map, v) => Object.assign(map, { [v.uid]: v.vote }),
1826 {}
1827 );
1828 var positive = 0;
1829 var negative = 0;
1830 Object.keys(map).forEach((uid) => {
1831 if (map[uid] === 1) {
1832 positive++;
1833 } else if (map[uid] === -1) {
1834 negative++;
1835 }
1836 });
1837 return {
1838 positive,
1839 negative,
1840 grabs: mp[kGrabs].length,
1841 listeners: mp.users().length + mp.guests()
1842 }
1843 };
1844
1845 var vote = (direction) =>
1846 mp.post('votes', {
1847 historyID: mp.historyEntry().id,
1848 direction: direction
1849 });
1850
1851 Object.assign(mp, {
1852 grab: (targetPlaylist, hid) =>
1853 mp.post('grabs', { playlistID: targetPlaylist, historyID: hid }).get(0),
1854
1855 vote,
1856
1857 woot: partial(vote, 1),
1858 meh: partial(vote, -1)
1859 });
1860 }
1861}
1862
1863// Exports
1864
1865Object.assign(miniplug, {
1866 httpPlugin,
1867 connectPlugin,
1868 usersPlugin,
1869 ignoresPlugin,
1870 mutesPlugin,
1871 bansPlugin,
1872 notificationsPlugin,
1873 boothPlugin,
1874 waitlistPlugin,
1875 waitlistBansPlugin,
1876 historyPlugin,
1877 chatPlugin,
1878 friendsPlugin,
1879 roomsPlugin,
1880 playlistsPlugin,
1881 storePlugin,
1882 systemPlugin,
1883 votePlugin
1884}, constants, errorClasses);
1885
1886// Implementation
1887
1888var Agent$1 = https.Agent;
1889var debug$6 = createDebug('miniplug:miniplug');
1890var defaultOptions = {
1891 host: 'https://plug.dj'
1892};
1893
1894function miniplug (opts) {
1895 if ( opts === void 0 ) opts = {};
1896
1897 // Faux-inherit from EventEmitter.
1898 var emitter = new EventEmitter();
1899 emitter.setMaxListeners(100);
1900 var mp = Object.create(emitter);
1901
1902 opts = Object.assign({}, defaultOptions,
1903 opts);
1904
1905 if (typeof opts.agent === 'undefined') {
1906 opts.agent = new Agent$1({ keepAlive: true });
1907 }
1908
1909 // trim trailing slashes
1910 var plugHost = opts.host.replace(/\/+$/, '');
1911
1912 mp.on('login', () => debug$6('authenticated'));
1913 mp.on('connected', () => debug$6('connected'));
1914
1915 // Super-Duper Advanced Plugin API
1916 function use (plugin) {
1917 plugin(mp);
1918 return mp
1919 }
1920
1921 // make miniplug!
1922 mp.use = use;
1923
1924 use(dataModelPlugin());
1925 use(httpPlugin({
1926 host: plugHost,
1927 agent: opts.agent,
1928 // This is the same backoff as used in Sooyou/plugged:
1929 // https://github.com/SooYou/plugged/blob/856bd0ef47307491c0ad95cba7006cd4721828fd/query.js#L4
1930 // And that seems pretty robust.
1931 backoff: linearPromiseBackoffQueue({ increment: 200, max: 2200, Promise })
1932 }));
1933 use(connectPlugin({
1934 host: plugHost,
1935 agent: opts.agent
1936 }));
1937 use(usersPlugin());
1938 use(ignoresPlugin());
1939 use(mutesPlugin());
1940 use(bansPlugin());
1941 use(notificationsPlugin());
1942 use(boothPlugin());
1943 use(waitlistPlugin());
1944 use(waitlistBansPlugin());
1945 use(historyPlugin());
1946 use(chatPlugin({
1947 backoff: linearPromiseBackoffQueue({ increment: 70, max: 700, Promise })
1948 }));
1949 use(friendsPlugin());
1950 use(roomsPlugin());
1951 use(playlistsPlugin());
1952 use(storePlugin());
1953 use(systemPlugin());
1954 use(votePlugin());
1955
1956 // Misc
1957 Object.assign(mp, {
1958 // REST: News APIs
1959 getNews: partial(mp.get, 'news')
1960 });
1961
1962 return mp
1963}
1964
1965module.exports = miniplug;
1966//# sourceMappingURL=index.js.map