karaoke-eternal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/README.md +49 -0
- package/assets/app.ico +0 -0
- package/assets/app.png +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/mic-blackTemplate.png +0 -0
- package/assets/mic-blackTemplate@2x.png +0 -0
- package/assets/mic-white.png +0 -0
- package/assets/mic-white@2x.png +0 -0
- package/assets/robots.txt +2 -0
- package/build/267.4be526e3a94d53aeceae.js +1 -0
- package/build/591.4be526e3a94d53aeceae.js +1 -0
- package/build/598.4be526e3a94d53aeceae.css +5 -0
- package/build/598.4be526e3a94d53aeceae.js +1 -0
- package/build/799.4be526e3a94d53aeceae.js +1 -0
- package/build/7ce9eb3fe454f54745a4.woff2 +0 -0
- package/build/845.4be526e3a94d53aeceae.css +132 -0
- package/build/845.4be526e3a94d53aeceae.js +1 -0
- package/build/a35814dd9eb496e3d7cc.woff2 +0 -0
- package/build/e419b95dccb58b362811.woff2 +0 -0
- package/build/index.html +1 -0
- package/build/licenses.txt +1400 -0
- package/build/main.4be526e3a94d53aeceae.css +2034 -0
- package/build/main.4be526e3a94d53aeceae.js +1 -0
- package/package.json +144 -0
- package/server/Library/Library.js +340 -0
- package/server/Library/index.js +3 -0
- package/server/Library/ipc.js +18 -0
- package/server/Library/router.js +27 -0
- package/server/Library/socket.js +47 -0
- package/server/Media/Media.js +207 -0
- package/server/Media/index.js +3 -0
- package/server/Media/ipc.js +19 -0
- package/server/Media/router.js +99 -0
- package/server/Player/socket.js +78 -0
- package/server/Prefs/Prefs.js +165 -0
- package/server/Prefs/index.js +3 -0
- package/server/Prefs/router.js +124 -0
- package/server/Prefs/socket.js +68 -0
- package/server/Queue/Queue.js +208 -0
- package/server/Queue/index.js +3 -0
- package/server/Queue/socket.js +99 -0
- package/server/Rooms/Rooms.js +114 -0
- package/server/Rooms/index.js +3 -0
- package/server/Rooms/router.js +146 -0
- package/server/Scanner/FileScanner/FileScanner.js +225 -0
- package/server/Scanner/FileScanner/getConfig.js +35 -0
- package/server/Scanner/FileScanner/getFiles.js +63 -0
- package/server/Scanner/FileScanner/index.js +3 -0
- package/server/Scanner/MetaParser/MetaParser.js +49 -0
- package/server/Scanner/MetaParser/defaultMiddleware.js +197 -0
- package/server/Scanner/MetaParser/index.js +3 -0
- package/server/Scanner/Scanner.js +33 -0
- package/server/User/User.js +139 -0
- package/server/User/index.js +3 -0
- package/server/User/router.js +442 -0
- package/server/lib/Database.js +55 -0
- package/server/lib/IPCBridge.js +115 -0
- package/server/lib/Log.js +71 -0
- package/server/lib/bcrypt.js +24 -0
- package/server/lib/cli.js +136 -0
- package/server/lib/electron.js +81 -0
- package/server/lib/getCdgName.js +20 -0
- package/server/lib/getDevMiddleware.js +51 -0
- package/server/lib/getFolders.js +10 -0
- package/server/lib/getHotMiddleware.js +27 -0
- package/server/lib/getIPAddress.js +16 -0
- package/server/lib/getPermutations.js +21 -0
- package/server/lib/getWindowsDrives.js +30 -0
- package/server/lib/parseCookie.js +12 -0
- package/server/lib/pushQueuesAndLibrary.js +29 -0
- package/server/lib/schemas/001-initial-schema.sql +98 -0
- package/server/lib/schemas/002-replaygain.sql +9 -0
- package/server/lib/schemas/003-queue-linked-list.sql +16 -0
- package/server/main.js +135 -0
- package/server/scannerWorker.js +58 -0
- package/server/serverWorker.js +242 -0
- package/server/socket.js +173 -0
- package/shared/actionTypes.js +103 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const db = require('../lib/Database').db
|
|
3
|
+
const sql = require('sqlate')
|
|
4
|
+
|
|
5
|
+
class Queue {
|
|
6
|
+
/**
|
|
7
|
+
* Add a songId to a room's queue
|
|
8
|
+
*
|
|
9
|
+
* @param {object} roomId, songId, userId
|
|
10
|
+
* @return {Promise}
|
|
11
|
+
*/
|
|
12
|
+
static async add ({ roomId, songId, userId }) {
|
|
13
|
+
const fields = new Map()
|
|
14
|
+
fields.set('roomId', roomId)
|
|
15
|
+
fields.set('songId', songId)
|
|
16
|
+
fields.set('userId', userId)
|
|
17
|
+
fields.set('prevQueueId', sql`(
|
|
18
|
+
SELECT queueId
|
|
19
|
+
FROM queue
|
|
20
|
+
WHERE roomId = ${roomId} AND queueId NOT IN (
|
|
21
|
+
SELECT prevQueueId
|
|
22
|
+
FROM queue
|
|
23
|
+
WHERE prevQueueId IS NOT NULL
|
|
24
|
+
)
|
|
25
|
+
)`)
|
|
26
|
+
|
|
27
|
+
const query = sql`
|
|
28
|
+
INSERT INTO queue ${sql.tuple(Array.from(fields.keys()).map(sql.column))}
|
|
29
|
+
VALUES ${sql.tuple(Array.from(fields.values()))}
|
|
30
|
+
`
|
|
31
|
+
const res = await db.run(String(query), query.parameters)
|
|
32
|
+
|
|
33
|
+
if (res.changes !== 1) {
|
|
34
|
+
throw new Error('Could not add song to queue')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get queued items for a given room
|
|
40
|
+
*
|
|
41
|
+
* @param {Number} roomId
|
|
42
|
+
* @return {Promise}
|
|
43
|
+
*/
|
|
44
|
+
static async get (roomId) {
|
|
45
|
+
const result = []
|
|
46
|
+
const entities = {}
|
|
47
|
+
const map = new Map()
|
|
48
|
+
let curQueueId = null
|
|
49
|
+
|
|
50
|
+
const query = sql`
|
|
51
|
+
SELECT queueId, songId, userId, prevQueueId,
|
|
52
|
+
media.mediaId, media.relPath, media.rgTrackGain, media.rgTrackPeak,
|
|
53
|
+
users.name AS userDisplayName, users.dateUpdated,
|
|
54
|
+
MAX(isPreferred) AS isPreferred
|
|
55
|
+
FROM queue
|
|
56
|
+
INNER JOIN users USING(userId)
|
|
57
|
+
INNER JOIN media USING(songId)
|
|
58
|
+
INNER JOIN paths USING(pathId)
|
|
59
|
+
WHERE roomId = ${roomId}
|
|
60
|
+
GROUP BY queueId
|
|
61
|
+
ORDER BY queueId, paths.priority ASC
|
|
62
|
+
`
|
|
63
|
+
const rows = await db.all(String(query), query.parameters)
|
|
64
|
+
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
entities[row.queueId] = row
|
|
67
|
+
entities[row.queueId].mediaType = this.getType(row.relPath)
|
|
68
|
+
|
|
69
|
+
// don't send over the wire
|
|
70
|
+
delete entities[row.queueId].relPath
|
|
71
|
+
delete entities[row.queueId].isPreferred
|
|
72
|
+
|
|
73
|
+
if (row.prevQueueId === null) {
|
|
74
|
+
// found the first item
|
|
75
|
+
result.push(row.queueId)
|
|
76
|
+
curQueueId = row.queueId
|
|
77
|
+
} else {
|
|
78
|
+
// map indexed by prevQueueId
|
|
79
|
+
map.set(row.prevQueueId, row.queueId)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
while (result.length < rows.length) {
|
|
84
|
+
// get the item whose prevQueueId references the current one
|
|
85
|
+
const nextQueueId = entities[map.get(curQueueId)].queueId
|
|
86
|
+
result.push(nextQueueId)
|
|
87
|
+
curQueueId = nextQueueId
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { result, entities }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Move a queue item
|
|
95
|
+
* @param {object} prevQueueId, queueId, roomId
|
|
96
|
+
* @return {Promise} undefined
|
|
97
|
+
*/
|
|
98
|
+
static async move ({ prevQueueId, queueId, roomId }) {
|
|
99
|
+
if (queueId === prevQueueId) {
|
|
100
|
+
throw new Error('Invalid prevQueueId')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (prevQueueId === -1) prevQueueId = null
|
|
104
|
+
|
|
105
|
+
const query = sql`
|
|
106
|
+
UPDATE queue
|
|
107
|
+
SET prevQueueId = CASE
|
|
108
|
+
WHEN queueId = newChild THEN ${queueId}
|
|
109
|
+
WHEN queueId = curChild AND curParent IS NOT NULL AND newChild IS NOT NULL THEN curParent
|
|
110
|
+
WHEN queueId = ${queueId} THEN ${prevQueueId}
|
|
111
|
+
ELSE queue.prevQueueId
|
|
112
|
+
END
|
|
113
|
+
FROM (SELECT
|
|
114
|
+
(
|
|
115
|
+
SELECT prevQueueId
|
|
116
|
+
FROM queue
|
|
117
|
+
WHERE queueId = ${queueId}
|
|
118
|
+
) AS curParent,
|
|
119
|
+
(
|
|
120
|
+
SELECT queueId
|
|
121
|
+
FROM queue
|
|
122
|
+
WHERE prevQueueId = ${queueId}
|
|
123
|
+
) AS curChild,
|
|
124
|
+
(
|
|
125
|
+
SELECT queueId
|
|
126
|
+
FROM queue
|
|
127
|
+
WHERE queueId != ${queueId}
|
|
128
|
+
AND prevQueueId ${prevQueueId === null ? sql`IS NULL` : sql`= ${prevQueueId}`}
|
|
129
|
+
AND roomId = ${roomId}
|
|
130
|
+
) AS newChild
|
|
131
|
+
)
|
|
132
|
+
WHERE roomId = ${roomId}
|
|
133
|
+
`
|
|
134
|
+
await db.run(String(query), query.parameters)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete a queue item
|
|
139
|
+
*
|
|
140
|
+
* We could DELETE first and get the deleted item's prevQueueId using
|
|
141
|
+
* RETURNING, but the DELETE and UPDATE need to be wrapped in a transaction
|
|
142
|
+
* (so the prevQueueId foreign key check is deferred). Also, v0.9 betas didn't
|
|
143
|
+
* have prevQueueId DEFFERABLE, and so will still error at DELETE (do we care?)
|
|
144
|
+
*
|
|
145
|
+
* @param {object} queueId, userId
|
|
146
|
+
* @return {Promise} undefined
|
|
147
|
+
*/
|
|
148
|
+
static async remove (queueId) {
|
|
149
|
+
// close the soon-to-be gap first
|
|
150
|
+
const updateQuery = sql`
|
|
151
|
+
UPDATE queue
|
|
152
|
+
SET prevQueueId = curParent
|
|
153
|
+
FROM (
|
|
154
|
+
SELECT
|
|
155
|
+
(
|
|
156
|
+
SELECT prevQueueId
|
|
157
|
+
FROM queue
|
|
158
|
+
WHERE queueId = ${queueId}
|
|
159
|
+
) AS curParent,
|
|
160
|
+
(
|
|
161
|
+
SELECT queueId
|
|
162
|
+
FROM queue
|
|
163
|
+
WHERE prevQueueId = ${queueId}
|
|
164
|
+
) AS curChild
|
|
165
|
+
)
|
|
166
|
+
WHERE queueId = curChild
|
|
167
|
+
`
|
|
168
|
+
await db.run(String(updateQuery), updateQuery.parameters)
|
|
169
|
+
|
|
170
|
+
// delete item
|
|
171
|
+
const deleteQuery = sql`
|
|
172
|
+
DELETE FROM queue
|
|
173
|
+
WHERE queueId = ${queueId}
|
|
174
|
+
`
|
|
175
|
+
const deleteRes = await db.run(String(deleteQuery), deleteQuery.parameters)
|
|
176
|
+
|
|
177
|
+
if (!deleteRes.changes) {
|
|
178
|
+
throw new Error(`Could not remove queueId: ${queueId}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if a user owns a queue item
|
|
184
|
+
* @param {number} userId
|
|
185
|
+
* @param {number} queueId
|
|
186
|
+
* @return {boolean}
|
|
187
|
+
*/
|
|
188
|
+
static async isOwner (userId, queueId) {
|
|
189
|
+
const query = sql`
|
|
190
|
+
SELECT COUNT(*) AS count
|
|
191
|
+
FROM queue
|
|
192
|
+
WHERE userId = ${userId} AND queueId = ${queueId}
|
|
193
|
+
`
|
|
194
|
+
const res = await db.get(String(query), query.parameters)
|
|
195
|
+
return res.count === 1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get media type from file extension
|
|
200
|
+
* @param {string} file filename
|
|
201
|
+
* @return {string} player component
|
|
202
|
+
*/
|
|
203
|
+
static getType (file) {
|
|
204
|
+
return /\.mp4/i.test(path.extname(file)) ? 'mp4' : 'cdg'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = Queue
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const Queue = require('./Queue')
|
|
2
|
+
const Rooms = require('../Rooms')
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
QUEUE_ADD,
|
|
6
|
+
QUEUE_MOVE,
|
|
7
|
+
QUEUE_REMOVE,
|
|
8
|
+
QUEUE_PUSH,
|
|
9
|
+
} = require('../../shared/actionTypes')
|
|
10
|
+
|
|
11
|
+
// ------------------------------------
|
|
12
|
+
// Action Handlers
|
|
13
|
+
// ------------------------------------
|
|
14
|
+
const ACTION_HANDLERS = {
|
|
15
|
+
[QUEUE_ADD]: async (sock, { payload }, acknowledge) => {
|
|
16
|
+
const { songId } = payload
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await Rooms.validate(sock.user.roomId, null, { validatePassword: false })
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return acknowledge({
|
|
22
|
+
type: QUEUE_ADD + '_ERROR',
|
|
23
|
+
error: err.message,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await Queue.add({
|
|
28
|
+
roomId: sock.user.roomId,
|
|
29
|
+
songId,
|
|
30
|
+
userId: sock.user.userId,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// success
|
|
34
|
+
acknowledge({ type: QUEUE_ADD + '_SUCCESS' })
|
|
35
|
+
|
|
36
|
+
// to all in room
|
|
37
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
38
|
+
type: QUEUE_PUSH,
|
|
39
|
+
payload: await Queue.get(sock.user.roomId)
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
[QUEUE_MOVE]: async (sock, { payload }, acknowledge) => {
|
|
43
|
+
const { queueId, prevQueueId } = payload
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await Rooms.validate(sock.user.roomId, null, { validatePassword: false })
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return acknowledge({
|
|
49
|
+
type: QUEUE_MOVE + '_ERROR',
|
|
50
|
+
error: err.message,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!sock.user.isAdmin && !await Queue.isOwner(sock.user.userId, queueId)) {
|
|
55
|
+
return acknowledge({
|
|
56
|
+
type: QUEUE_MOVE + '_ERROR',
|
|
57
|
+
error: 'Cannot move another user\'s song',
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await Queue.move({
|
|
62
|
+
prevQueueId,
|
|
63
|
+
queueId,
|
|
64
|
+
roomId: sock.user.roomId,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// success
|
|
68
|
+
acknowledge({ type: QUEUE_MOVE + '_SUCCESS' })
|
|
69
|
+
|
|
70
|
+
// tell room
|
|
71
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
72
|
+
type: QUEUE_PUSH,
|
|
73
|
+
payload: await Queue.get(sock.user.roomId)
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
[QUEUE_REMOVE]: async (sock, { payload }, acknowledge) => {
|
|
77
|
+
const { queueId } = payload
|
|
78
|
+
|
|
79
|
+
if (!sock.user.isAdmin && !await Queue.isOwner(sock.user.userId, queueId)) {
|
|
80
|
+
return acknowledge({
|
|
81
|
+
type: QUEUE_REMOVE + '_ERROR',
|
|
82
|
+
error: 'Cannot remove another user\'s song',
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await Queue.remove(queueId)
|
|
87
|
+
|
|
88
|
+
// success
|
|
89
|
+
acknowledge({ type: QUEUE_REMOVE + '_SUCCESS' })
|
|
90
|
+
|
|
91
|
+
// tell room
|
|
92
|
+
sock.server.to(Rooms.prefix(sock.user.roomId)).emit('action', {
|
|
93
|
+
type: QUEUE_PUSH,
|
|
94
|
+
payload: await Queue.get(sock.user.roomId)
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = ACTION_HANDLERS
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const bcrypt = require('../lib/bcrypt')
|
|
2
|
+
const db = require('../lib/Database').db
|
|
3
|
+
const sql = require('sqlate')
|
|
4
|
+
|
|
5
|
+
class Rooms {
|
|
6
|
+
/**
|
|
7
|
+
* Get all rooms
|
|
8
|
+
*
|
|
9
|
+
* @param {Boolean} closed Whether to include rooms with status "closed"
|
|
10
|
+
* @return {Promise}
|
|
11
|
+
*/
|
|
12
|
+
static async get (closed = false) {
|
|
13
|
+
const result = []
|
|
14
|
+
const entities = {}
|
|
15
|
+
const whereClause = closed ? sql`true` : sql`status = "open"`
|
|
16
|
+
|
|
17
|
+
const query = sql`
|
|
18
|
+
SELECT * FROM rooms
|
|
19
|
+
WHERE ${whereClause}
|
|
20
|
+
ORDER BY dateCreated DESC
|
|
21
|
+
`
|
|
22
|
+
const res = await db.all(String(query), query.parameters)
|
|
23
|
+
|
|
24
|
+
res.forEach(row => {
|
|
25
|
+
row.dateCreated = row.dateCreated.substring(0, 10)
|
|
26
|
+
row.hasPassword = !!row.password
|
|
27
|
+
delete row.password
|
|
28
|
+
|
|
29
|
+
result.push(row.roomId)
|
|
30
|
+
entities[row.roomId] = row
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return { result, entities }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate a room against optional criteria
|
|
38
|
+
*
|
|
39
|
+
* @param {Number} roomId
|
|
40
|
+
* @param {[String]} password Room password
|
|
41
|
+
* @param {[Object]} opts (bool) isOpen, (bool) validatePassword
|
|
42
|
+
* @return {Promise} True if validated, otherwise throws an error
|
|
43
|
+
*/
|
|
44
|
+
static async validate (roomId, password, { isOpen = true, validatePassword = true } = {}) {
|
|
45
|
+
const query = sql`
|
|
46
|
+
SELECT * FROM rooms
|
|
47
|
+
WHERE roomId = ${roomId}
|
|
48
|
+
`
|
|
49
|
+
const room = await db.get(String(query), query.parameters)
|
|
50
|
+
|
|
51
|
+
if (!room) {
|
|
52
|
+
throw new Error('Room not found')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isOpen && room.status !== 'open') {
|
|
56
|
+
throw new Error('Room is no longer open')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (validatePassword && room.password) {
|
|
60
|
+
if (!password) {
|
|
61
|
+
throw new Error('Room password is required')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!await bcrypt.compare(password, room.password)) {
|
|
65
|
+
throw new Error('Incorrect room password')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static prefix (roomId = '') {
|
|
73
|
+
return `ROOM_ID_${roomId}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Utility method to list active rooms on a socket.io instance
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} io The socket.io instance
|
|
80
|
+
* @return {Array} Array of objects as { room, roomId }
|
|
81
|
+
*/
|
|
82
|
+
static getActive (io) {
|
|
83
|
+
const rooms = []
|
|
84
|
+
|
|
85
|
+
for (const room of io.sockets.adapter.rooms.keys()) {
|
|
86
|
+
// ignore auto-generated per-user rooms
|
|
87
|
+
if (room.startsWith(Rooms.prefix())) {
|
|
88
|
+
const roomId = parseInt(room.substring(Rooms.prefix().length), 10)
|
|
89
|
+
rooms.push({ room, roomId })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return rooms
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Utility method to determine if a player is in a room
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} io The socket.io instance
|
|
100
|
+
* @param {Object} roomId Room to check
|
|
101
|
+
* @return {Boolean}
|
|
102
|
+
*/
|
|
103
|
+
static isPlayerPresent (io, roomId) {
|
|
104
|
+
for (const sock of io.of('/').sockets.values()) {
|
|
105
|
+
if (sock.user && sock.user.roomId === roomId && sock._lastPlayerStatus) {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = Rooms
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const bcrypt = require('../lib/bcrypt')
|
|
2
|
+
const db = require('../lib/Database').db
|
|
3
|
+
const sql = require('sqlate')
|
|
4
|
+
const KoaRouter = require('koa-router')
|
|
5
|
+
const router = KoaRouter({ prefix: '/api' })
|
|
6
|
+
const log = require('../lib/Log')('Rooms')
|
|
7
|
+
const Rooms = require('../Rooms')
|
|
8
|
+
|
|
9
|
+
const BCRYPT_ROUNDS = 12
|
|
10
|
+
const NAME_MIN_LENGTH = 1
|
|
11
|
+
const NAME_MAX_LENGTH = 50
|
|
12
|
+
const PASSWORD_MIN_LENGTH = 5
|
|
13
|
+
const STATUSES = ['open', 'closed']
|
|
14
|
+
|
|
15
|
+
// list rooms
|
|
16
|
+
router.get('/rooms', async (ctx, next) => {
|
|
17
|
+
// non-admins can only see open rooms
|
|
18
|
+
const res = await Rooms.get(ctx.user.isAdmin)
|
|
19
|
+
|
|
20
|
+
res.result.forEach(roomId => {
|
|
21
|
+
const room = ctx.io.sockets.adapter.rooms.get(Rooms.prefix(roomId))
|
|
22
|
+
res.entities[roomId].numUsers = room ? room.size : 0
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
ctx.body = res
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// create room
|
|
29
|
+
router.post('/rooms', async (ctx, next) => {
|
|
30
|
+
if (!ctx.user.isAdmin) {
|
|
31
|
+
ctx.throw(401)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { name, password, status } = ctx.request.body
|
|
35
|
+
|
|
36
|
+
if (!name || !name.trim() || name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
|
|
37
|
+
ctx.throw(400, `Room name must have ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (password && password.length < PASSWORD_MIN_LENGTH) {
|
|
41
|
+
ctx.throw(400, `Room password must have at least ${PASSWORD_MIN_LENGTH} characters`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!status || !STATUSES.includes(status)) {
|
|
45
|
+
ctx.throw(400, 'Invalid room status')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fields = new Map()
|
|
49
|
+
fields.set('name', name.trim())
|
|
50
|
+
fields.set('password', password ? await bcrypt.hash(password, BCRYPT_ROUNDS) : null)
|
|
51
|
+
fields.set('status', status)
|
|
52
|
+
fields.set('dateCreated', Math.floor(Date.now() / 1000))
|
|
53
|
+
|
|
54
|
+
const query = sql`
|
|
55
|
+
INSERT INTO rooms ${sql.tuple(Array.from(fields.keys()).map(sql.column))}
|
|
56
|
+
VALUES ${sql.tuple(Array.from(fields.values()))}
|
|
57
|
+
`
|
|
58
|
+
const res = await db.run(String(query), query.parameters)
|
|
59
|
+
|
|
60
|
+
if (res.changes) {
|
|
61
|
+
log.verbose('%s created room "%s" (roomId: %s)', ctx.user.name, name, res.lastID)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// send updated room list
|
|
65
|
+
ctx.body = await Rooms.get(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// update room
|
|
69
|
+
router.put('/rooms/:roomId', async (ctx, next) => {
|
|
70
|
+
if (!ctx.user.isAdmin) {
|
|
71
|
+
ctx.throw(401)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { name, password, status } = ctx.request.body
|
|
75
|
+
const roomId = parseInt(ctx.params.roomId, 10)
|
|
76
|
+
|
|
77
|
+
if (!name || !name.trim() || name.length < NAME_MIN_LENGTH || name.length > NAME_MAX_LENGTH) {
|
|
78
|
+
ctx.throw(400, `Room name must have ${NAME_MIN_LENGTH}-${NAME_MAX_LENGTH} characters`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (password && password.length < PASSWORD_MIN_LENGTH) {
|
|
82
|
+
ctx.throw(400, `Room password must have at least ${PASSWORD_MIN_LENGTH} characters`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!status || !STATUSES.includes(status)) {
|
|
86
|
+
ctx.throw(400, 'Invalid room status')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fields = new Map()
|
|
90
|
+
fields.set('name', name.trim())
|
|
91
|
+
fields.set('status', status)
|
|
92
|
+
fields.set('roomId', roomId)
|
|
93
|
+
|
|
94
|
+
// falsey value will unset password
|
|
95
|
+
if (typeof password !== 'undefined') {
|
|
96
|
+
fields.set('password', password ? await bcrypt.hash(password, BCRYPT_ROUNDS) : null)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const query = sql`
|
|
100
|
+
UPDATE rooms
|
|
101
|
+
SET ${sql.tuple(Array.from(fields.keys()).map(sql.column))} = ${sql.tuple(Array.from(fields.values()))}
|
|
102
|
+
WHERE roomId = ${roomId}
|
|
103
|
+
`
|
|
104
|
+
const res = await db.run(String(query), query.parameters)
|
|
105
|
+
|
|
106
|
+
if (res.changes) {
|
|
107
|
+
log.verbose('%s updated roomId %s', ctx.user.name, ctx.params.roomId)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// send updated room list
|
|
111
|
+
ctx.body = await Rooms.get(true)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// remove room
|
|
115
|
+
router.delete('/rooms/:roomId', async (ctx, next) => {
|
|
116
|
+
if (!ctx.user.isAdmin) {
|
|
117
|
+
ctx.throw(401)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const roomId = parseInt(ctx.params.roomId, 10)
|
|
121
|
+
|
|
122
|
+
if (typeof roomId !== 'number') {
|
|
123
|
+
ctx.throw(422, 'Invalid roomId')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// remove room's queue first
|
|
127
|
+
const queueQuery = sql`
|
|
128
|
+
DELETE FROM queue
|
|
129
|
+
WHERE roomId = ${roomId}
|
|
130
|
+
`
|
|
131
|
+
await db.run(String(queueQuery), queueQuery.parameters)
|
|
132
|
+
|
|
133
|
+
// remove room
|
|
134
|
+
const roomQuery = sql`
|
|
135
|
+
DELETE FROM rooms
|
|
136
|
+
WHERE roomId = ${roomId}
|
|
137
|
+
`
|
|
138
|
+
await db.run(String(roomQuery), roomQuery.parameters)
|
|
139
|
+
|
|
140
|
+
log.verbose('%s deleted roomId %s', ctx.user.name, roomId)
|
|
141
|
+
|
|
142
|
+
// send updated room list
|
|
143
|
+
ctx.body = await Rooms.get(true)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
module.exports = router
|