UNPKG

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