1 var db
= require("../database");
2 var valid
= require("../utilities").isValidChannelName
;
3 var Flags
= require("../flags");
4 var util
= require("../utilities");
5 import { createMySQLDuplicateKeyUpdate
} from '../util/on-duplicate-key-update';
6 import Config
from '../config';
8 const LOGGER
= require('@calzoneman/jsli')('database/channels');
10 var blackHole = function () { };
17 * Checks if the given channel name is registered
19 isChannelTaken: function (name
, callback
) {
20 if (typeof callback
!== "function") {
25 callback("Invalid channel name", null);
29 db
.query("SELECT name FROM `channels` WHERE name=?",
31 function (err
, rows
) {
36 callback(null, rows
.length
> 0);
43 lookup: function (name
, callback
) {
44 if (typeof callback
!== "function") {
49 callback("Invalid channel name", null);
53 db
.query("SELECT * FROM `channels` WHERE name=?",
55 function (err
, rows
) {
61 if (rows
.length
=== 0) {
62 callback("No such channel", null);
64 callback(null, rows
[0]);
70 * Searches for a channel
72 search: function (name
, callback
) {
73 if (typeof callback
!== "function") {
77 db
.query("SELECT * FROM `channels` WHERE name LIKE ?",
79 function (err
, rows
) {
89 * Searches for a channel by owner
91 searchOwner: function (name
, callback
) {
92 if (typeof callback
!== "function") {
96 db
.query("SELECT * FROM `channels` WHERE owner LIKE ?",
98 function (err
, rows
) {
103 callback(null, rows
);
108 * Validates and registers a new channel
110 register: function (name
, owner
, callback
) {
111 if (typeof callback
!== "function") {
112 callback
= blackHole
;
115 if (typeof name
!== "string" || typeof owner
!== "string") {
116 callback("Name and owner are required for channel registration", null);
121 callback("Invalid channel name. Channel names may consist of 1-30 " +
122 "characters a-z, A-Z, 0-9, -, and _", null);
126 module
.exports
.isChannelTaken(name
, function (err
, taken
) {
133 callback("Channel name " + name
+ " is already taken", null);
137 db
.query("INSERT INTO `channels` " +
138 "(`name`, `owner`, `time`, `last_loaded`) VALUES (?, ?, ?, ?)",
139 [name
, owner
, Date
.now(), new Date()],
140 function (err
, _res
) {
146 db
.users
.getGlobalRank(owner
, function (err
, rank
) {
152 rank
= Math
.max(rank
, 5);
154 module
.exports
.setRank(name
, owner
, rank
, function (err
) {
160 callback(null, { name
: name
});
168 * Unregisters a channel
170 drop: function (name
, callback
) {
171 if (typeof callback
!== "function") {
172 callback
= blackHole
;
176 callback("Invalid channel name", null);
180 db
.query("DELETE FROM `channels` WHERE name=?", [name
], function (err
) {
182 module
.exports
.deleteBans(name
, function (err
) {
184 LOGGER
.error("Failed to delete bans for " + name
+ ": " + err
);
188 module
.exports
.deleteLibrary(name
, function (err
) {
190 LOGGER
.error("Failed to delete library for " + name
+ ": " + err
);
194 module
.exports
.deleteAllRanks(name
, function (err
) {
196 LOGGER
.error("Failed to delete ranks for " + name
+ ": " + err
);
205 * Looks up channels registered by a given user
207 listUserChannels: function (owner
, callback
) {
208 if (typeof callback
!== "function") {
212 db
.query("SELECT * FROM `channels` WHERE owner=?", [owner
],
213 function (err
, res
) {
223 listUserChannelsAsync
: owner
=> {
224 return new Promise((resolve
, reject
) => {
225 module
.exports
.listUserChannels(owner
, (error
, rows
) => {
236 * Loads the channel from the database
238 load: function (chan
, callback
) {
239 if (typeof callback
!== "function") {
240 callback
= blackHole
;
243 if (!valid(chan
.name
)) {
244 callback("Invalid channel name", null);
248 db
.query("SELECT * FROM `channels` WHERE name=?", chan
.name
, function (err
, res
) {
254 if (res
.length
=== 0) {
255 callback("Channel is not registered", null);
260 callback("Channel is dead", null);
264 // Note that before this line, chan.name might have a different capitalization
265 // than the database has stored. Update accordingly.
266 chan
.name
= res
[0].name
;
267 chan
.uniqueName
= chan
.name
.toLowerCase();
269 chan
.ownerName
= typeof res
[0].owner
=== 'string' ? res
[0].owner
.toLowerCase() : null;
270 chan
.setFlag(Flags
.C_REGISTERED
);
271 chan
.logger
.log("[init] Loaded channel from database");
272 callback(null, true);
277 * Looks up a user's rank
279 getRank: function (chan
, name
, callback
) {
280 if (typeof callback
!== "function") {
285 callback("Invalid channel name", null);
289 db
.query("SELECT * FROM `channel_ranks` WHERE name=? AND channel=?",
291 function (err
, rows
) {
297 if (rows
.length
=== 0) {
302 callback(null, rows
[0].rank
);
307 * Looks up multiple users' ranks at once
309 getRanks: function (chan
, names
, callback
) {
310 if (typeof callback
!== "function") {
315 callback("Invalid channel name", null);
319 var replace
= "(" + names
.map(function () { return "?"; }).join(",") + ")";
321 /* Last substitution is the channel to select ranks for */
322 const sub
= names
.concat([chan
]);
324 db
.query("SELECT * FROM `channel_ranks` WHERE name IN " +
325 replace
+ " AND channel=?", sub
,
326 function (err
, rows
) {
332 callback(null, rows
.map(function (r
) { return r
.rank
; }));
337 * Query all user ranks at once
339 allRanks: function (chan
, callback
) {
340 if (typeof callback
!== "function") {
345 callback("Invalid channel name", null);
349 db
.query("SELECT * FROM `channel_ranks` WHERE channel=?", [chan
], callback
);
353 * Updates a user's rank
355 setRank: function (chan
, name
, rank
, callback
) {
356 if (typeof callback
!== "function") {
357 callback
= blackHole
;
361 module
.exports
.deleteRank(chan
, name
, callback
);
366 callback("Invalid channel name", null);
370 db
.query("INSERT INTO `channel_ranks` VALUES (?, ?, ?) " +
371 "ON DUPLICATE KEY UPDATE `rank`=?",
372 [name
, rank
, chan
, rank
], callback
);
376 * Removes a user's rank entry
378 deleteRank: function (chan
, name
, callback
) {
379 if (typeof callback
!== "function") {
380 callback
= blackHole
;
384 callback("Invalid channel name", null);
388 db
.query("DELETE FROM `channel_ranks` WHERE name=? AND channel=?", [name
, chan
],
393 * Removes all ranks for a channel
395 deleteAllRanks: function (chan
, callback
) {
396 if (typeof callback
!== "function") {
397 callback
= blackHole
;
401 callback("Invalid channel name", null);
405 db
.query("DELETE FROM `channel_ranks` WHERE channel=?", [chan
], callback
);
409 * Adds a media item to the library
411 addToLibrary: function (chan
, media
, callback
) {
412 if (typeof callback
!== "function") {
413 callback
= blackHole
;
417 callback("Invalid channel name", null);
421 var meta
= JSON
.stringify({
422 bitrate
: media
.meta
.bitrate
,
423 codec
: media
.meta
.codec
,
424 scuri
: media
.meta
.scuri
,
425 embed
: media
.meta
.embed
,
426 direct
: media
.meta
.direct
429 db
.query("INSERT INTO `channel_libraries` " +
430 "(id, title, seconds, type, meta, channel) " +
431 "VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE id=id",
432 [media
.id
, media
.title
, media
.seconds
, media
.type
, meta
, chan
], callback
);
436 * Adds a list of media items to the library
438 addListToLibrary
: async
function addListToLibrary(chan
, list
) {
440 throw new Error("Invalid channel name");
443 if (list
.length
> Config
.get("playlist.max-items")) {
444 throw new Error("Cannot save list to library: exceeds max-items");
447 const items
= list
.map(item
=> ({
450 seconds
: item
.seconds
,
452 meta
: JSON
.stringify({
453 bitrate
: item
.meta
.bitrate
,
454 codec
: item
.meta
.codec
,
455 scuri
: item
.meta
.scuri
,
456 embed
: item
.meta
.embed
,
457 direct
: item
.meta
.direct
462 await db
.getDB().runTransaction(tx
=> {
463 const insert
= tx
.table('channel_libraries')
466 const update
= tx
.raw(createMySQLDuplicateKeyUpdate(
467 ['title', 'seconds', 'meta']
470 return tx
.raw(insert
.toString() + update
.toString());
475 * Retrieves a media item from the library by id
477 getLibraryItem: function (chan
, id
, callback
) {
478 if (typeof callback
!== "function") {
483 callback("Invalid channel name", null);
487 db
.query("SELECT * FROM `channel_libraries` WHERE id=? AND channel=?", [id
, chan
],
488 function (err
, rows
) {
494 if (rows
.length
=== 0) {
495 callback("Item not in library", null);
497 callback(null, rows
[0]);
503 * Search the library by title
505 searchLibrary: function (chan
, search
, callback
) {
506 if (typeof callback
!== "function") {
510 db
.query("SELECT * FROM `channel_libraries` WHERE title LIKE ? AND channel=?",
511 ["%" + search
+ "%", chan
], callback
);
515 * Deletes a media item from the library
517 deleteFromLibrary: function (chan
, id
, callback
) {
518 if (typeof callback
!== "function") {
519 callback
= blackHole
;
523 callback("Invalid channel name", null);
527 db
.query("DELETE FROM `channel_libraries` WHERE id=? AND channel=?",
528 [id
, chan
], callback
);
532 * Deletes all library entries for a channel
534 deleteLibrary: function (chan
, callback
) {
535 if (typeof callback
!== "function") {
536 callback
= blackHole
;
540 callback("Invalid channel name", null);
544 db
.query("DELETE FROM `channel_libraries` WHERE channel=?", [chan
], callback
);
548 * Add a ban to the banlist
550 ban: function (chan
, ip
, name
, note
, bannedby
, callback
) {
551 if (typeof callback
!== "function") {
552 callback
= blackHole
;
556 callback("Invalid channel name", null);
560 db
.query("INSERT INTO `channel_bans` (ip, name, reason, bannedby, channel) " +
561 "VALUES (?, ?, ?, ?, ?)",
562 [ip
, name
, note
, bannedby
, chan
], callback
);
566 * Check if an IP address or range is banned
568 isIPBanned: function (chan
, ip
, callback
) {
569 if (typeof callback
!== "function") {
574 callback("Invalid channel name", null);
578 var range
= util
.getIPRange(ip
);
579 var wrange
= util
.getWideIPRange(ip
);
581 db
.query("SELECT * FROM `channel_bans` WHERE ip IN (?, ?, ?) AND channel=?",
582 [ip
, range
, wrange
, chan
],
583 function (err
, rows
) {
584 callback(err
, err
? false : rows
.length
> 0);
589 * Check if a username is banned
591 isNameBanned: function (chan
, name
, callback
) {
592 if (typeof callback
!== "function") {
597 callback("Invalid channel name", null);
601 db
.query("SELECT * FROM `channel_bans` WHERE name=? AND channel=?", [name
, chan
],
602 function (err
, rows
) {
603 callback(err
, err
? false : rows
.length
> 0);
608 * Check if a user's name or IP is banned
610 isBanned: function (chan
, ip
, name
, callback
) {
611 if (typeof callback
!== "function") {
616 callback("Invalid channel name", null);
620 var range
= util
.getIPRange(ip
);
621 var wrange
= util
.getWideIPRange(ip
);
623 db
.query("SELECT COUNT(1) AS count FROM `channel_bans` WHERE (ip IN (?, ?, ?) OR name=?) AND channel=?",
624 [ip
, range
, wrange
, name
, chan
],
625 function (err
, rows
) {
626 callback(err
, err
? false : rows
.length
> 0 && rows
[0].count
> 0);
633 listBans: function (chan
, callback
) {
634 if (typeof callback
!== "function") {
639 callback("Invalid channel name", null);
643 db
.query("SELECT * FROM `channel_bans` WHERE channel=?", [chan
], callback
);
647 * Removes a ban from the banlist
649 unbanId: function (chan
, id
, callback
) {
650 if (typeof callback
!== "function") {
651 callback
= blackHole
;
655 callback("Invalid channel name", null);
659 db
.query("DELETE FROM `channel_bans` WHERE id=? AND channel=?",
660 [id
, chan
], callback
);
664 * Removes all bans from a channel
666 deleteBans: function (chan
, id
, callback
) {
667 if (typeof callback
!== "function") {
668 callback
= blackHole
;
672 callback("Invalid channel name", null);
676 db
.query("DELETE FROM `channel_bans` WHERE channel=?", [chan
], callback
);
680 * Updates the `last_loaded` column to be the current timestamp
682 updateLastLoaded
: function updateLastLoaded(channelId
) {
683 if (channelId
<= 0) {
687 db
.query("UPDATE channels SET last_loaded = ? WHERE id = ?", [new Date(), channelId
], error
=> {
689 LOGGER
.error(`Failed to update last_loaded column for channel ID ${channelId}: ${error}`);
695 * Updates the `owner_last_seen` column to be the current timestamp
697 updateOwnerLastSeen
: function updateOwnerLastSeen(channelId
) {
698 if (channelId
<= 0) {
702 db
.query("UPDATE channels SET owner_last_seen = ? WHERE id = ?", [new Date(), channelId
], error
=> {
704 LOGGER
.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`);