• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

u-wave / core / 12018248150

25 Nov 2024 08:27PM UTC coverage: 81.786% (-0.2%) from 81.949%
12018248150

Pull #667

github

goto-bus-stop
Remove remnants of replace booth

This was probably a nice idea we had back when üWave was first started.
A moderator would be able to instantly put a user in the booth, skipping
the waitlist.

In practice we never implemented it, and it would require client-side
support. I doubt it's all that useful and it is just dead code right
now.
Pull Request #667: Remove remnants of replace booth

826 of 998 branches covered (82.77%)

Branch coverage included in aggregate %.

9057 of 11086 relevant lines covered (81.7%)

85.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.93
/src/plugins/waitlist.js
1
import fs from 'node:fs';
1✔
2
import {
1✔
3
  PermissionError,
1✔
4
  UserNotFoundError,
1✔
5
  EmptyPlaylistError,
1✔
6
  WaitlistLockedError,
1✔
7
  AlreadyInWaitlistError,
1✔
8
  UserNotInWaitlistError,
1✔
9
  UserIsPlayingError,
1✔
10
} from '../errors/index.js';
1✔
11
import routes from '../routes/waitlist.js';
1✔
12
import { Permissions } from './acl.js';
1✔
13

1✔
14
const schema = JSON.parse(
1✔
15
  fs.readFileSync(new URL('../schemas/waitlist.json', import.meta.url), 'utf8'),
1✔
16
);
1✔
17

1✔
18
/**
1✔
19
 * @typedef {import('../schema.js').UserID} UserID
1✔
20
 * @typedef {import('../schema.js').User} User
1✔
21
 * @typedef {{ cycle: boolean, locked: boolean }} WaitlistSettings
1✔
22
 */
1✔
23

1✔
24
const ADD_TO_WAITLIST_SCRIPT = {
1✔
25
  keys: ['waitlist', 'booth:currentDJ'],
1✔
26
  lua: `
1✔
27
    local k_waitlist = KEYS[1]
1✔
28
    local k_dj = KEYS[2]
1✔
29
    local user_id = ARGV[1]
1✔
30
    local position = ARGV[2]
1✔
31
    local is_in_waitlist = redis.call('LPOS', k_waitlist, user_id)
1✔
32
    local current_dj = redis.call('GET', k_dj)
1✔
33
    if is_in_waitlist or current_dj == user_id then
1✔
34
      return { err = '${AlreadyInWaitlistError.code}' }
1✔
35
    end
1✔
36

1✔
37
    local before_id = nil
1✔
38
    if position then
1✔
39
      before_id = redis.call('LINDEX', k_waitlist, position)
1✔
40
    end
1✔
41

1✔
42
    if before_id then
1✔
43
      redis.call('LINSERT', k_waitlist, 'BEFORE', before_id)
1✔
44
    else
1✔
45
      redis.call('RPUSH', k_waitlist, user_id)
1✔
46
    end
1✔
47

1✔
48
    return redis.call('LRANGE', k_waitlist, 0, -1)
1✔
49
  `,
1✔
50
};
1✔
51

1✔
52
const MOVE_WAITLIST_SCRIPT = {
1✔
53
  keys: ['waitlist', 'booth:currentDJ'],
1✔
54
  lua: `
1✔
55
    local k_waitlist = KEYS[1]
1✔
56
    local k_dj = KEYS[2]
1✔
57
    local user_id = ARGV[1]
1✔
58
    local position = ARGV[2]
1✔
59
    local is_in_waitlist = redis.call('LPOS', k_waitlist, user_id)
1✔
60
    if not is_in_waitlist then
1✔
61
      return { err = '${UserNotInWaitlistError.code}' }
1✔
62
    end
1✔
63
    local current_dj = redis.call('GET', k_dj)
1✔
64
    if current_dj == user_id then
1✔
65
      return { err = '${UserIsPlayingError.code}' }
1✔
66
    end
1✔
67

1✔
68
    local before_id = redis.call('LINDEX', k_waitlist, position)
1✔
69

1✔
70
    redis.call('LREM', k_waitlist, 0, user_id);
1✔
71
    if before_id then
1✔
72
      redis.call('LINSERT', k_waitlist, 'BEFORE', before_id, user_id);
1✔
73
    else
1✔
74
      redis.call('RPUSH', k_waitlist, user_id)
1✔
75
    end
1✔
76

1✔
77
    return redis.call('LRANGE', k_waitlist, 0, -1)
1✔
78
  `,
1✔
79
};
1✔
80

1✔
81
class Waitlist {
126✔
82
  #uw;
126✔
83

126✔
84
  /**
126✔
85
   * @param {import('../Uwave.js').Boot} uw
126✔
86
   */
126✔
87
  constructor(uw) {
126✔
88
    this.#uw = uw;
126✔
89

126✔
90
    uw.config.register(schema['uw:key'], schema);
126✔
91
    uw.redis.defineCommand('uw:addToWaitlist', {
126✔
92
      numberOfKeys: ADD_TO_WAITLIST_SCRIPT.keys.length,
126✔
93
      lua: ADD_TO_WAITLIST_SCRIPT.lua,
126✔
94
    });
126✔
95
    uw.redis.defineCommand('uw:moveWaitlist', {
126✔
96
      numberOfKeys: MOVE_WAITLIST_SCRIPT.keys.length,
126✔
97
      lua: MOVE_WAITLIST_SCRIPT.lua,
126✔
98
    });
126✔
99

126✔
100
    const unsubscribe = uw.config.subscribe(
126✔
101
      schema['uw:key'],
126✔
102
      /**
126✔
103
       * @param {WaitlistSettings} _settings
126✔
104
       * @param {UserID|null} userID
126✔
105
       * @param {Partial<WaitlistSettings>} patch
126✔
106
       */
126✔
107
      (_settings, userID, patch) => {
126✔
108
        // TODO This userID != null check is wrong. It should always pass as
×
109
        // long as all the cases where waitlist settings can be updated provide
×
110
        // the moderator's user ID. There's no type level guarantee of that happening
×
111
        // though and if it doesn't, clients will get out of sync because of this check.
×
112
        if ('locked' in patch && patch.locked != null && userID != null) {
×
113
          this.#uw.publish('waitlist:lock', {
×
114
            moderatorID: userID,
×
115
            locked: patch.locked,
×
116
          });
×
117
        }
×
118
      },
126✔
119
    );
126✔
120
    uw.onClose(unsubscribe);
126✔
121
  }
126✔
122

126✔
123
  async #isBoothEmpty() {
126✔
124
    return !(await this.#uw.redis.get('booth:historyID'));
11✔
125
  }
11✔
126

126✔
127
  /**
126✔
128
   * @param {User} user
126✔
129
   * @returns {Promise<boolean>}
126✔
130
   */
126✔
131
  async #hasPlayablePlaylist(user) {
126✔
132
    const { playlists } = this.#uw;
14✔
133
    if (!user.activePlaylistID) {
14✔
134
      return false;
1✔
135
    }
1✔
136

13✔
137
    const playlist = await playlists.getUserPlaylist(user, user.activePlaylistID);
13✔
138
    return playlist && playlist.size > 0;
14✔
139
  }
14✔
140

126✔
141
  /**
126✔
142
   * @returns {Promise<WaitlistSettings>}
126✔
143
   */
126✔
144
  async #getSettings() {
126✔
145
    const { config } = this.#uw;
26✔
146

26✔
147
    const settings = /** @type {WaitlistSettings} */ (await config.get(schema['uw:key']));
26✔
148
    return settings;
26✔
149
  }
26✔
150

126✔
151
  /**
126✔
152
   * @returns {Promise<boolean>}
126✔
153
   */
126✔
154
  async isLocked() {
126✔
155
    const settings = await this.#getSettings();
18✔
156
    return settings.locked;
18✔
157
  }
18✔
158

126✔
159
  /**
126✔
160
   * @returns {Promise<boolean>}
126✔
161
   */
126✔
162
  async isCycleEnabled() {
126✔
163
    const settings = await this.#getSettings();
8✔
164
    return settings.cycle;
8✔
165
  }
8✔
166

126✔
167
  /**
126✔
168
   * @returns {Promise<UserID[]>}
126✔
169
   */
126✔
170
  getUserIDs() {
126✔
171
    return /** @type {Promise<UserID[]>} */ (this.#uw.redis.lrange('waitlist', 0, -1));
22✔
172
  }
22✔
173

126✔
174
  /**
126✔
175
   * used both for joining the waitlist, and for
126✔
176
   * adding someone else to the waitlist.
126✔
177
   * TODO maybe split this up and let http-api handle the difference
126✔
178
   *
126✔
179
   * @param {UserID} userID
126✔
180
   * @param {{moderator?: User}} [options]
126✔
181
   */
126✔
182
  async addUser(userID, options = {}) {
126✔
183
    const { moderator } = options;
15✔
184
    const { acl, users } = this.#uw;
15✔
185

15✔
186
    const user = await users.getUser(userID);
15✔
187
    if (!user) throw new UserNotFoundError({ id: userID });
15!
188

15✔
189
    const isAddingOtherUser = moderator && user.id !== moderator.id;
15✔
190
    if (isAddingOtherUser) {
15✔
191
      if (!(await acl.isAllowed(moderator, Permissions.WaitlistAdd))) {
2✔
192
        throw new PermissionError({
1✔
193
          requiredRole: 'waitlist.add',
1✔
194
        });
1✔
195
      }
1✔
196
    }
2✔
197

14✔
198
    const canForceJoin = await acl.isAllowed(user, Permissions.WaitlistJoinLocked);
14✔
199
    if (!isAddingOtherUser && !canForceJoin && await this.isLocked()) {
15!
200
      throw new WaitlistLockedError();
×
201
    }
×
202

14✔
203
    if (!(await this.#hasPlayablePlaylist(user))) {
15✔
204
      throw new EmptyPlaylistError();
2✔
205
    }
2✔
206

12✔
207
    try {
12✔
208
      const waitlist = /** @type {UserID[]} */ (await this.#uw.redis['uw:addToWaitlist'](...ADD_TO_WAITLIST_SCRIPT.keys, user.id));
15✔
209

11✔
210
      if (isAddingOtherUser) {
15✔
211
        this.#uw.publish('waitlist:add', {
1✔
212
          userID: user.id,
1✔
213
          moderatorID: moderator.id,
1✔
214
          position: waitlist.indexOf(user.id),
1✔
215
          waitlist,
1✔
216
        });
1✔
217
      } else {
15✔
218
        this.#uw.publish('waitlist:join', {
10✔
219
          userID: user.id,
10✔
220
          waitlist,
10✔
221
        });
10✔
222
      }
10✔
223
    } catch (error) {
15✔
224
      if (error.message === AlreadyInWaitlistError.code) {
1✔
225
        throw new AlreadyInWaitlistError();
1✔
226
      }
1✔
227
      throw error;
×
228
    }
×
229

11✔
230
    if (await this.#isBoothEmpty()) {
15✔
231
      await this.#uw.booth.advance();
8✔
232
    }
8✔
233
  }
15✔
234

126✔
235
  /**
126✔
236
   * @param {UserID} userID
126✔
237
   * @param {number} position
126✔
238
   * @param {{moderator: User}} options
126✔
239
   * @returns {Promise<void>}
126✔
240
   */
126✔
241
  async moveUser(userID, position, { moderator }) {
126✔
242
    const { users } = this.#uw;
×
243

×
244
    const user = await users.getUser(userID);
×
245
    if (!user) {
×
246
      throw new UserNotFoundError({ id: userID });
×
247
    }
×
248

×
249
    if (!(await this.#hasPlayablePlaylist(user))) {
×
250
      throw new EmptyPlaylistError();
×
251
    }
×
252

×
253
    try {
×
254
      const waitlist = /** @type {UserID[]} */ (await this.#uw.redis['uw:moveWaitlist'](...MOVE_WAITLIST_SCRIPT.keys, user.id, position));
×
255

×
256
      this.#uw.publish('waitlist:move', {
×
257
        userID: user.id,
×
258
        moderatorID: moderator.id,
×
259
        position: waitlist.indexOf(user.id),
×
260
        waitlist,
×
261
      });
×
262
    } catch (error) {
×
263
      if (error.message === UserNotInWaitlistError.code) {
×
264
        throw new UserNotInWaitlistError({ id: user.id });
×
265
      }
×
266
      if (error.message === UserIsPlayingError.code) {
×
267
        throw new UserIsPlayingError({ id: user.id });
×
268
      }
×
269
      throw error;
×
270
    }
×
271
  }
×
272

126✔
273
  /**
126✔
274
   * @param {UserID} userID
126✔
275
   * @param {{moderator: User}} options
126✔
276
   * @returns {Promise<void>}
126✔
277
   */
126✔
278
  async removeUser(userID, { moderator }) {
126✔
279
    const { acl, users } = this.#uw;
×
280
    const user = await users.getUser(userID);
×
281
    if (!user) {
×
282
      throw new UserNotFoundError({ id: userID });
×
283
    }
×
284

×
285
    const isRemoving = moderator && user.id !== moderator.id;
×
286
    if (isRemoving && !(await acl.isAllowed(moderator, Permissions.WaitlistRemove))) {
×
287
      throw new PermissionError({
×
288
        requiredRole: 'waitlist.remove',
×
289
      });
×
290
    }
×
291

×
292
    const removedCount = await this.#uw.redis.lrem('waitlist', 0, user.id);
×
293
    if (removedCount === 0) {
×
294
      throw new UserNotInWaitlistError({ id: user.id });
×
295
    }
×
296

×
297
    const waitlist = await this.getUserIDs();
×
298
    if (isRemoving) {
×
299
      this.#uw.publish('waitlist:remove', {
×
300
        userID: user.id,
×
301
        moderatorID: moderator.id,
×
302
        waitlist,
×
303
      });
×
304
    } else {
×
305
      this.#uw.publish('waitlist:leave', {
×
306
        userID: user.id,
×
307
        waitlist,
×
308
      });
×
309
    }
×
310
  }
×
311

126✔
312
  /**
126✔
313
   * @param {{moderator: User}} options
126✔
314
   * @returns {Promise<void>}
126✔
315
   */
126✔
316
  async clear({ moderator }) {
126✔
317
    await this.#uw.redis.del('waitlist');
×
318

×
319
    const waitlist = await this.getUserIDs();
×
320
    if (waitlist.length !== 0) {
×
321
      throw new Error('Could not clear the waitlist. Please try again.');
×
322
    }
×
323

×
324
    this.#uw.publish('waitlist:clear', {
×
325
      moderatorID: moderator.id,
×
326
    });
×
327
  }
×
328

126✔
329
  /**
126✔
330
   * @param {boolean} lock
126✔
331
   * @param {User} moderator
126✔
332
   * @returns {Promise<void>}
126✔
333
   */
126✔
334
  async #setWaitlistLocked(lock, moderator) {
126✔
335
    const settings = await this.#getSettings();
×
336
    await this.#uw.config.set(schema['uw:key'], { ...settings, locked: lock }, { user: moderator });
×
337
  }
×
338

126✔
339
  /**
126✔
340
   * Lock the waitlist. Only users with the `waitlist.join.locked` permission
126✔
341
   * will be able to join.
126✔
342
   *
126✔
343
   * @param {{moderator: User}} options
126✔
344
   * @returns {Promise<void>}
126✔
345
   */
126✔
346
  lock({ moderator }) {
126✔
347
    return this.#setWaitlistLocked(true, moderator);
×
348
  }
×
349

126✔
350
  /**
126✔
351
   * Unlock the waitlist. All users with the `waitlist.join` permission
126✔
352
   * will be able to join.
126✔
353
   *
126✔
354
   * @param {{moderator: User}} options
126✔
355
   * @returns {Promise<void>}
126✔
356
   */
126✔
357
  unlock({ moderator }) {
126✔
358
    return this.#setWaitlistLocked(false, moderator);
×
359
  }
×
360
}
126✔
361

1✔
362
/**
1✔
363
 * @param {import('../Uwave.js').Boot} uw
1✔
364
 * @returns {Promise<void>}
1✔
365
 */
1✔
366
async function waitlistPlugin(uw) {
126✔
367
  uw.waitlist = new Waitlist(uw);
126✔
368
  uw.httpApi.use('/waitlist', routes());
126✔
369
}
126✔
370

1✔
371
export default waitlistPlugin;
1✔
372
export { Waitlist };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc