From 83c41c05f72f350e20d6988590f6dc7c4815d241 Mon Sep 17 00:00:00 2001 From: in1tiate Date: Fri, 28 Jan 2022 19:51:30 -0600 Subject: [PATCH] Squashed commit of the following: commit e946bf124602f224ce0e371ba1374f0355b537eb Merge: d6a4e64 4505909 Author: Rosemary Witchaven <32779090+in1tiate@users.noreply.github.com> Date: Fri Jan 28 19:43:36 2022 -0600 Merge pull request #225 from Salanto/Dynamic-Area-Musiclist-Take2 Allow users to add custom songs to the music list on a per-area basis commit 45059092d2888b60912f721e43a380910d10ccd8 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 24 22:05:27 2022 +0100 TIL what a typedef is commit 02584db9640fff0a1969a7f516c4bccfae9b5388 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 24 21:39:17 2022 +0100 Not all OR are equal. Explain weird command splitting Remove hardcoded URLs commit d00ebd5692296cd0c29dd377113b53fe0e7b99c0 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 24 21:28:08 2022 +0100 Salanto PR Language Update by in1tiate As usual, my English is absolutely unreadable. Co-authored-by: Rosemary Witchaven <32779090+in1tiate@users.noreply.github.com> commit d3842106e06350dc02d8864bb28232fdc5643f00 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 24 20:35:32 2022 +0100 Add missing config file + document commands for help commit ac64360e1c1741023b01052977de77a7d5ea4f8c Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 24 19:52:13 2022 +0100 Minor improvements to command usage and addition of clear command commit c614578e78ce9afa0c8e22aa36bdf46a70a97169 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 23 22:19:54 2022 +0100 Purge last traces of old songInformation system commit 07618761f044a13d75587b28a9c994342a5980e2 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 23 22:10:54 2022 +0100 Working version, needs some refinement and debugging in AOClient commit 33c0358c98c0fd2de805356a9aa3ac7bbed204e1 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 23 21:48:05 2022 +0100 Almost functional implementation Now only need to determine why I can't play the customs yet commit b0acbace78b3f16f2fe4f3c6f65a422e3343f992 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 23 15:26:42 2022 +0100 Fix build error, expand validation test slightly commit a48c4f503998ce8e42f0bb409c5a3c7dc5e40329 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 23 01:03:27 2022 +0100 Add commands commit 88ab0b473953873166e291e5009b97df31547b3f Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sat Jan 22 15:10:26 2022 +0100 Float sucks, int has to be good enough + add retrival of song information commit e924e1340be1a0909eba84072f1646fe9770bd02 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Thu Jan 20 22:28:35 2022 +0100 Fix removing moving Add necessary tests commit 3df088f8d07ce7e0d8fe08b6a97608a623e6ef97 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Wed Jan 19 19:34:16 2022 +0100 Start work on adding this shit into commands commit c293ecfa99d1b2bd1e0b34cb8752d69b2eca057c Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Tue Jan 18 19:07:11 2022 +0100 Fix typo and add singal for incremental upgrades commit 10a42322e1e23af5795278a40b2ac59f3ab952ef Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Tue Jan 18 06:17:51 2022 +0100 Hookup packet sending to music manager This might sound like a bad idea on first glance, but otherwise it breaks the AreaData tests and I am NOT gonna try to fix those without even understanding why they break. commit 319836296374162b0b847432e8a626778317b869 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Tue Jan 18 06:17:51 2022 +0100 Enraged comments, make area send FM packet Revert "Enraged comments, make area send FM packet" This reverts commit ec7a1a25646b2c2acc8a3a748b853851cc47d205. commit 224a0d7efe989a5f336167c3f716061813f93ee3 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Tue Jan 18 05:10:18 2022 +0100 Change packet communication from area to client First steps to hookup the custom musiclist. commit 65aa8f7855a36f2c668b1399a5ed22fefeaf186d Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 17 00:48:38 2022 +0100 Add test for custom list sanitisation. + Fix intentionally broken tests commit 7c00ab437a6ff12033742d029ce49037f5bb1ebe Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Mon Jan 17 00:29:51 2022 +0100 Sanitise the custom list opposed to deleting it This will fail tests intentionally to test the CI. commit 80ad401267068e75707b2517a0bf836763141f8b Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 16 17:17:12 2022 +0100 Add custom category capabilities commit 08d8f5f8f683816ceba532f9c47cd0d5ab34389a Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sun Jan 16 03:58:18 2022 +0100 Fix music addition and move relevant tests commit 6ebf0d03b5da61a9c287115009d28038710ba7af Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sat Jan 15 08:08:20 2022 +0100 Hookup music_manager into server, change default musiclist source for new clients + More tests :cool: commit bd50c62376f131e2508ecdd3e272209894ecaec1 Author: Salanto <62221668+Salanto@users.noreply.github.com> Date: Sat Jan 15 03:13:42 2022 +0100 Add central song validator for other classes Also added applicable test cases to ensure proper operation. --- bin/config_sample/text/cdns.txt | 1 + bin/config_sample/text/commandhelp.json | 27 +- core/core.pro | 6 +- core/include/aoclient.h | 47 ++- core/include/area_data.h | 14 +- core/include/config_manager.h | 34 +- core/include/music_manager.h | 214 ++++++++++++ core/include/server.h | 18 +- core/src/aoclient.cpp | 30 +- core/src/area_data.cpp | 16 +- core/src/commands/music.cpp | 76 +++++ core/src/config_manager.cpp | 35 +- core/src/music_manager.cpp | 237 +++++++++++++ core/src/packets.cpp | 5 +- core/src/server.cpp | 33 +- tests/tests.pro | 3 +- tests/unittest_area/tst_unittest_area.cpp | 2 +- .../tst_unittest_music_manager.cpp | 322 ++++++++++++++++++ .../unittest_music_manager.pro | 5 + 19 files changed, 1063 insertions(+), 62 deletions(-) create mode 100644 bin/config_sample/text/cdns.txt create mode 100644 core/include/music_manager.h create mode 100644 core/src/music_manager.cpp create mode 100644 tests/unittest_music_manager/tst_unittest_music_manager.cpp create mode 100644 tests/unittest_music_manager/unittest_music_manager.pro diff --git a/bin/config_sample/text/cdns.txt b/bin/config_sample/text/cdns.txt new file mode 100644 index 0000000..2d09b6c --- /dev/null +++ b/bin/config_sample/text/cdns.txt @@ -0,0 +1 @@ +cdn.discord.com diff --git a/bin/config_sample/text/commandhelp.json b/bin/config_sample/text/commandhelp.json index 4bd2859..299b66f 100644 --- a/bin/config_sample/text/commandhelp.json +++ b/bin/config_sample/text/commandhelp.json @@ -646,7 +646,32 @@ }, { "name":"areamessage", - "usage":"/areamessage", + "usage":"/areamessage 'Message'", "text":"Returns the area message in OOC. If text is entered it updates the current area message." + }, + { + "name":"addsong", + "usage":"/addsong ,'TrueName','Duration'", + "text":"Adds a song to the custom musiclist. Optionally can be an aliased name to hide a streamlink. Set the duration for the jukebox." + }, + { + "name":"addcategory", + "usage":"/addsong ", + "text":"Adds a category to the custom musiclist." + }, + { + "name":"removeentry", + "usage":"/removeentry 'Name'", + "text":"Removes an entry from the custom musiclist." + }, + { + "name":"toggleglobal", + "usage":"/toggleglobal", + "text":"Changes the behaviour of prepending the server root musiclist to the custom lists of the area." + }, + { + "name":"clearcustom", + "usage":"/clearcustom", + "text":"Removes all custom songs from the area." } ] \ No newline at end of file diff --git a/core/core.pro b/core/core.pro index 44cd5fc..1dfddf8 100644 --- a/core/core.pro +++ b/core/core.pro @@ -47,7 +47,8 @@ SOURCES += \ src/advertiser.cpp \ src/logger/u_logger.cpp \ src/logger/writer_modcall.cpp \ - src/logger/writer_full.cpp + src/logger/writer_full.cpp \ + src/music_manager.cpp HEADERS += include/aoclient.h \ include/aopacket.h \ @@ -62,4 +63,5 @@ HEADERS += include/aoclient.h \ include/advertiser.h \ include/logger/u_logger.h \ include/logger/writer_modcall.h \ - include/logger/writer_full.h + include/logger/writer_full.h \ + include/music_manager.h diff --git a/core/include/aoclient.h b/core/include/aoclient.h index c6ded93..6a9faca 100644 --- a/core/include/aoclient.h +++ b/core/include/aoclient.h @@ -22,6 +22,7 @@ #include "include/server.h" #include "include/area_data.h" #include "include/db_manager.h" +#include "include/music_manager.h" #include @@ -50,14 +51,7 @@ class AOClient : public QObject { * @param user_id The user ID of the client. * @param parent Qt-based parent, passed along to inherited constructor from QObject. */ - AOClient(Server* p_server, QTcpSocket* p_socket, QObject* parent = nullptr, int user_id = 0) - : QObject(parent), m_id(user_id), m_remote_ip(p_socket->peerAddress()), m_password(""), - m_joined(false), m_current_area(0), m_current_char(""), m_socket(p_socket), server(p_server), - is_partial(false), m_last_wtce_time(0) { - m_afk_timer = new QTimer; - m_afk_timer->setSingleShot(true); - connect(m_afk_timer, SIGNAL(timeout()), this, SLOT(onAfkTimeout())); - }; + AOClient(Server* p_server, QTcpSocket* p_socket, QObject* parent = nullptr, int user_id = 0, MusicManager* p_manager = nullptr);; /** * @brief Destructor for the AOClient instance. @@ -1868,6 +1862,31 @@ class AOClient : public QObject { */ void cmdToggleJukebox(int argc, QStringList argv); + /** + * @brief Adds a song to the custom list. + */ + void cmdAddSong(int argc, QStringList argv); + + /** + * @brief Adds a category to the areas custom music list. + */ + void cmdAddCategory(int argc, QStringList argv); + + /** + * @brief Removes any matching song or category from the custom area. + */ + void cmdRemoveCategorySong(int argc, QStringList argv); + + /** + * @brief Toggles the prepending behaviour of the servers root musiclist. + */ + void cmdToggleRootlist(int argc, QStringList argv); + + /** + * @brief Clears the entire custom list of this area. + */ + void cmdClearCustom(int argc, QStringList argv); + ///@} /** @@ -2185,7 +2204,12 @@ class AOClient : public QObject { {"clearcm", {ACLFlags.value("KICK"), 0, &AOClient::cmdClearCM}}, {"togglemessage", {ACLFlags.value("CM"), 0, &AOClient::cmdToggleAreaMessageOnJoin}}, {"clearmessage", {ACLFlags.value("CM"), 0, &AOClient::cmdClearAreaMessage}}, - {"areamessage", {ACLFlags.value("CM"), 0, &AOClient::cmdAreaMessage}} + {"areamessage", {ACLFlags.value("CM"), 0, &AOClient::cmdAreaMessage}}, + {"addsong", {ACLFlags.value("CM"), 1, &AOClient::cmdAddSong}}, + {"addcategory", {ACLFlags.value("CM"), 1, &AOClient::cmdAddCategory}}, + {"removeentry", {ACLFlags.value("CM"), 1, &AOClient::cmdRemoveCategorySong}}, + {"toggleroot", {ACLFlags.value("CM"), 0, &AOClient::cmdToggleRootlist}}, + {"clearcustom", {ACLFlags.value("CM"), 0, &AOClient::cmdClearCustom}} }; /** @@ -2234,6 +2258,11 @@ class AOClient : public QObject { */ QString m_last_message; + /** + * @brief Pointer to the servers music manager instance. + */ + MusicManager* m_music_manager; + /** * @brief A helper function to add recorded packets to an area's judgelog. * diff --git a/core/include/area_data.h b/core/include/area_data.h index e869650..a8512ba 100644 --- a/core/include/area_data.h +++ b/core/include/area_data.h @@ -20,6 +20,7 @@ #include "aopacket.h" #include "config_manager.h" +#include "music_manager.h" #include #include @@ -44,7 +45,7 @@ class AreaData : public QObject { * and `YYYYYY` is the actual name of the area. * @param p_index The index of the area in the area list. */ - AreaData(QString p_name, int p_index); + AreaData(QString p_name, int p_index, MusicManager* p_music_manager); /** * @brief The data for evidence in the area. @@ -865,9 +866,11 @@ public slots: signals: /** - * @brief Changes the song in the current area when the jukebox timer expires. + * @brief Sends a packet to every client inside the area. */ - void playJukeboxSong(AOPacket f_packet, int f_area_index); + void sendAreaPacket(AOPacket f_packet, int f_area_index); + + void userJoinedArea(int f_area_index, int f_user_id); private: /** @@ -885,6 +888,11 @@ private: */ int m_index; + /** + * @brief Pointer to the global music manager. + */ + MusicManager* m_music_manager; + /** * @brief A list of the character IDs of all characters taken. */ diff --git a/core/include/config_manager.h b/core/include/config_manager.h index f58e352..263087d 100644 --- a/core/include/config_manager.h +++ b/core/include/config_manager.h @@ -37,6 +37,8 @@ #include #include +typedef QMap> MusicList; + /** * @brief The config file handler class. */ @@ -76,7 +78,14 @@ class ConfigManager { * * @return See short description. */ - static QStringList musiclist(); + static MusicList musiclist(); + + /** + * @brief Returns an ordered QList of all basesongs of this server. + * + * @return See short description. + */ + static QStringList ordered_songs(); /** * @brief Loads help information into m_help_information. @@ -85,13 +94,6 @@ class ConfigManager { */ static void loadCommandHelp(); - /** - * @brief Returns the duration of a song in the songlist. - * @param The name of the song where duration is requested - * @return The duration of the song - */ - static QPair songInformation(const QString& f_songName); - /** * @brief Returns the content of * @@ -412,6 +414,13 @@ class ConfigManager { */ static QStringList gimpList(); + /** + * @brief Returns the server approved domain list. + * + * @return See short description. + */ + static QStringList cdnList(); + /** * @brief Returns if the advertiser is enabled to advertise on ms3. */ @@ -473,6 +482,7 @@ class ConfigManager { */ static void reloadSettings(); + private: /** * @brief Checks if a file exists and is valid. @@ -500,6 +510,7 @@ private: QStringList praises; //!< Contains command praises, found in config/text/praises.txt QStringList reprimands; //!< Contains command reprimands, found in config/text/reprimands.txt QStringList gimps; //!< Contains phrases for /gimp, found in config/text/gimp.txt + QStringList cdns; // !< Contains domains for custom song validation, found in config/text/cdns.txt }; /** @@ -535,7 +546,12 @@ private: /** * @brief Contains the musiclist with time durations. */ - static QHash>* m_musicList; + static MusicList* m_musicList; + + /** + * @brief Contains an ordered list for the musiclist. + */ + static QStringList* m_ordered_list; /** * @brief QHash containing the help information for all commands registered to the server. diff --git a/core/include/music_manager.h b/core/include/music_manager.h new file mode 100644 index 0000000..2c7fd40 --- /dev/null +++ b/core/include/music_manager.h @@ -0,0 +1,214 @@ +////////////////////////////////////////////////////////////////////////////////////// +// akashi - a server for Attorney Online 2 // +// Copyright (C) 2020 scatterflower // +// // +// This program is free software: you can redistribute it and/or modify // +// it under the terms of the GNU Affero General Public License as // +// published by the Free Software Foundation, either version 3 of the // +// License, or (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU Affero General Public License for more details. // +// // +// You should have received a copy of the GNU Affero General Public License // +// along with this program. If not, see . // +////////////////////////////////////////////////////////////////////////////////////// +#ifndef MUSIC_MANAGER_H +#define MUSIC_MANAGER_H + +#include +#include +#include +#include + +#include +#include + +class MusicManager : public QObject +{ + Q_OBJECT +public: + + /** + * @brief Constructor for the server-wide musiclist manager. + * + * @param Copy of the server musiclist generated by ConfigManager::musiclist(); + */ + MusicManager(QObject* parent = nullptr, QStringList f_root_ordered = {}, QStringList f_cdns = {"cdn.discord.com"}, + QMap > f_root_list = ConfigManager::musiclist()); + + /** + * @brief Destructor for the server-wide musiclist manager. + */ + virtual ~MusicManager(); + + /** + * @brief Returns a musiclist with aliased names. + * + * @return See short description. + */ + QStringList musiclist(int f_area_id); + + /** + * @brief Returns only the root musiclist with aliased names. + * + */ + QStringList rootMusiclist(); + + /** + * @brief Adds a new area to the music_manager. + * + * @param f_area_id ID of the new area being added. + * + * @return Returns false if registering the area fails. + */ + bool registerArea(int f_area_id); + + /** + * @brief Validates the song candidate to be played. If validation fails, false is returned. + * + * @param f_song_name The song to be played. Can be an http/https stream or a local file. + * + * @param f_approved_cdns A list of approved remote content sources. + * + * @return Wether or not the song can be played or added. + */ + bool validateSong(QString f_song_name, QStringList f_approved_cdns); + + /** + * @brief Attempts to add the new song to the custom musiclist. + * + * @param f_song_name Friendly name shown in the clients musiclist. + * + * @param f_real_name Real name/url of the file. + * + * @param f_duration Playtime of the musicfile in seconds. + * + * @param f_area_id Area id of the clients current area. + * + * @return Returns true on success, false on fail. + */ + bool addCustomSong(QString f_song_name, QString f_real_name, int f_duration, int f_area_id); + + /** + * @brief Attempts to add the new category to the custom musiclist. + * + * @param f_category_name Category name candidate. + * + * @return Returns true on saccess, false on fail. + */ + bool addCustomCategory(QString f_category_name, int f_area_id); + + /** + * @brief Removes either a song or a category from the custom list. + * + * @param Name of the category or song to remove + * + * @return True on success, false on failure. + */ + bool removeCategorySong(QString f_songcategory_name, int f_area_id); + + /** + * @brief Toggles wether the root list is included for this area. + * This also delets the custom llist if it was enabled prior. + * + * @return Current state of the music list. + */ + bool toggleRootEnabled(int f_area_id); + + /** + * @brief Removes conflicting songnames from the custom list. + * + * @param f_area_id Id of the area this is invoked in. + */ + void sanitiseCustomList(int f_area_id); + + /** + * @brief Removes all entries from the custom list. + * @param f_area_id Id of the area custom list. + */ + void clearCustomList(int f_area_id); + + /** + * @brief Returns song information necessary for the operation of the jukebox. + * @param Alias name of the song. + * @param Area of the jukebox checking for information. + * @return Returns a QPair with the true name and duration of the invoked song. + */ + QPair songInformation(QString f_song_name, int f_area_id); + + /** + * @brief Checks if a song is part of the clients current area custom list. + * + * @return Returns true if the song exists as a custom song. + */ + bool isCustom(int f_area_id, QString f_song_name); + +public slots: + + /** + * @brief Updates the root musiclist and CDN list. + */ + void reloadRequest(); + + /** + * @brief Triggers sending of FM packet to client joining a new area. + */ + void userJoinedArea(int f_area_index, int f_user_id); + +signals: + + /** + * @brief Sends the FM packet with the musiclist of the area when a client enters. + * + * @param f_packet FM packet with the full musiclist, when enabled, and custom list. + * + * @param f_user_id temporary userid of the incoming client. + */ + void sendFMPacket(AOPacket f_packet, int f_user_id); + + /** + * @brief Sends the FM packet with the musiclist of the area when changes are made. + * + * @param f_packet FM packet with the full musiclist, when enabled, and eventual custom list. + * + * @param f_area_index Index of the current area the edit is made in. + */ + void sendAreaFMPacket(AOPacket f_packet, int f_area_index); + +private: + + /** + * @brief Contains all custom lists of all areas in the server. + */ + QHash* m_custom_lists; + + /** + * @brief Server musiclist shared among all areas. + */ + MusicList m_root_list; + + /** + * @brief QList with the ordered musiclist. + */ + QStringList m_root_ordered; + + /** + * @brief Contains all custom songs ordered in a per-area buffer. + */ + QMap m_customs_ordered; + + /** + * @brief Wether the global musiclist is prepend and validation when adding custom music. + */ + QHash m_global_enabled; + + /** + * @brief Contains all server approved content sources. + */ + QStringList m_cdns; +}; + +#endif // MUSIC_MANAGER_H diff --git a/core/include/server.h b/core/include/server.h index c20600b..cb6d535 100644 --- a/core/include/server.h +++ b/core/include/server.h @@ -27,6 +27,7 @@ #include "include/config_manager.h" #include "include/advertiser.h" #include "include/logger/u_logger.h" +#include "include/music_manager.h" #include #include @@ -126,6 +127,7 @@ class Server : public QObject { * @brief Sends a packet to all clients in a given area. * * @param packet The packet to send to the clients. + * * @param area_index The index of the area to look for clients in. * * @note Does nothing if an area by the given index does not exist. @@ -140,7 +142,7 @@ class Server : public QObject { void broadcast(AOPacket packet); /** - * @brief Sends a packet to clients, sends an altered packet to a specific usergroup. + * @brief Sends a packet to a specific usergroup.. * * @param The packet to send to the clients. * @@ -159,6 +161,15 @@ class Server : public QObject { */ void broadcast(AOPacket packet, AOPacket other_packet, enum TARGET_TYPE target); + /** + * @brief Sends a packet to a single client. + * + * @param The packet send to the client. + * + * @param The temporary userID of the client. + */ + void unicast(AOPacket f_packet, int f_client_id); + /** * @brief Returns the character's character ID (= their index in the character list). * @@ -385,6 +396,11 @@ class Server : public QObject { */ ULogger* logger; + /** + * @brief Handles all musiclists. + */ + MusicManager* music_manager; + /** * @brief The port through which the server will accept TCP connections. */ diff --git a/core/src/aoclient.cpp b/core/src/aoclient.cpp index 39cbaa1..a57ecf6 100644 --- a/core/src/aoclient.cpp +++ b/core/src/aoclient.cpp @@ -347,9 +347,15 @@ bool AOClient::checkAuth(unsigned long long acl_mask) } -QString AOClient::getIpid() const { return m_ipid; } +QString AOClient::getIpid() const +{ + return m_ipid; +} -QString AOClient::getHwid() const { return m_hwid; } +QString AOClient::getHwid() const +{ + return m_hwid; +} Server* AOClient::getServer() { return server; } @@ -360,6 +366,26 @@ void AOClient::onAfkTimeout() m_is_afk = true; } +AOClient::AOClient(Server *p_server, QTcpSocket *p_socket, QObject *parent, int user_id, MusicManager *p_manager) + : QObject(parent), + m_id(user_id), + m_remote_ip(p_socket->peerAddress()), + m_password(""), + m_joined(false), + m_current_area(0), + m_current_char(""), + m_socket(p_socket), + server(p_server), + is_partial(false), + m_last_wtce_time(0), + m_music_manager(p_manager) +{ + m_afk_timer = new QTimer; + m_afk_timer->setSingleShot(true); + connect(m_afk_timer, &QTimer::timeout, + this, &AOClient::onAfkTimeout); +} + AOClient::~AOClient() { m_socket->deleteLater(); } diff --git a/core/src/area_data.cpp b/core/src/area_data.cpp index 6c153bb..ea0b04b 100644 --- a/core/src/area_data.cpp +++ b/core/src/area_data.cpp @@ -20,8 +20,9 @@ #include "include/area_data.h" -AreaData::AreaData(QString p_name, int p_index) : +AreaData::AreaData(QString p_name, int p_index, MusicManager* p_music_manager = nullptr) : m_index(p_index), + m_music_manager(p_music_manager), m_playerCount(0), m_status(IDLE), m_locked(FREE), @@ -93,6 +94,7 @@ void AreaData::clientJoinedArea(int f_charId, int f_userId) m_charactersTaken.append(f_charId); } m_joined_ids.append(f_userId); + emit userJoinedArea(m_index, f_userId); } QList AreaData::owners() const @@ -564,12 +566,12 @@ QString AreaData::addJukeboxSong(QString f_song) { if(!m_jukebox_queue.contains(f_song)) { //Retrieve song information. - QPair l_song = ConfigManager::songInformation(f_song); + QPair l_song = m_music_manager->songInformation(f_song, index()); if (l_song.second > 0) { if (m_jukebox_queue.size() == 0) { - emit playJukeboxSong(AOPacket("MC",{l_song.first,QString::number(-1)}), index()); + emit sendAreaPacket(AOPacket("MC",{l_song.first,QString::number(-1)}), index()); m_jukebox_timer->start(l_song.second * 1000); setCurrentMusic(f_song); setMusicPlayedBy("Jukebox"); @@ -594,16 +596,16 @@ void AreaData::switchJukeboxSong() QString l_song_name; if(m_jukebox_queue.size() == 1) { l_song_name = m_jukebox_queue[0]; - QPair l_song = ConfigManager::songInformation(l_song_name); - emit playJukeboxSong(AOPacket("MC",{l_song.first,"-1"}), m_index); + QPair l_song = m_music_manager->songInformation(l_song_name, index()); + emit sendAreaPacket(AOPacket("MC",{l_song.first,"-1"}), m_index); m_jukebox_timer->start(l_song.second * 1000); } else { int l_random_index = QRandomGenerator::system()->bounded(m_jukebox_queue.size() -1); l_song_name = m_jukebox_queue[l_random_index]; - QPair l_song = ConfigManager::songInformation(l_song_name); - emit playJukeboxSong(AOPacket("MC",{l_song.first,"-1"}), m_index); + QPair l_song = m_music_manager->songInformation(l_song_name, index()); + emit sendAreaPacket(AOPacket("MC",{l_song.first,"-1"}), m_index); m_jukebox_timer->start(l_song.second * 1000); m_jukebox_queue.remove(l_random_index); diff --git a/core/src/commands/music.cpp b/core/src/commands/music.cpp index 9504b50..0fdc1f3 100644 --- a/core/src/commands/music.cpp +++ b/core/src/commands/music.cpp @@ -127,3 +127,79 @@ void AOClient::cmdToggleJukebox(int argc, QStringList argv) sendServerMessage("You do not have permission to change the jukebox status."); } } + +void AOClient::cmdAddSong(int argc, QStringList argv) +{ + Q_UNUSED(argc); + + //This needs some explanation. + //Akashi has no concept of argument count,so any space is interpreted as a new element + //in the QStringList. This works fine until someone enters something with a space. + //Since we can't preencode those elements, we join all as a string and use a delimiter + //that does not exist in file and URL paths. I decided on the ol' reliable ','. + QString l_argv_string = argv.join(" "); + QStringList l_argv = l_argv_string.split(","); + + bool l_success = false; + if (l_argv.size() == 1) { + QString l_song_name = l_argv.value(0); + l_success = m_music_manager->addCustomSong(l_song_name, l_song_name, 0, m_current_area); + } + + if (l_argv.size() == 2) { + QString l_song_name = l_argv.value(0); + QString l_true_name = l_argv.value(1); + l_success = m_music_manager->addCustomSong(l_song_name, l_true_name, 0, m_current_area); + } + + if (l_argv.size() == 3) { + QString l_song_name = l_argv.value(0); + QString l_true_name = l_argv.value(1); + bool ok; + int l_song_duration = l_argv.value(2).toInt(&ok); + if (!ok) + l_song_duration = 0; + l_success = m_music_manager->addCustomSong(l_song_name, l_true_name, l_song_duration, m_current_area); + } + + if (l_argv.size() >= 4) { + sendServerMessage("Too many arguments. Addition of song has failed."); + return; + } + + QString l_message = l_success ? "succeeded." : "failed."; + sendServerMessage("The addition of the song has " + l_message); +} + +void AOClient::cmdAddCategory(int argc, QStringList argv) +{ + Q_UNUSED(argc); + bool l_success = m_music_manager->addCustomCategory(argv.join(" "), m_current_area); + QString l_message = l_success ? "succeeded." : "failed."; + sendServerMessage("The addition of the category has " + l_message); +} + +void AOClient::cmdRemoveCategorySong(int argc, QStringList argv) +{ + Q_UNUSED(argc); + bool l_success = m_music_manager->removeCategorySong(argv.join(" "), m_current_area); + QString l_message = l_success ? "succeeded." : "failed."; + sendServerMessage("The removal of the entry has " + l_message); +} + +void AOClient::cmdToggleRootlist(int argc, QStringList argv) +{ + Q_UNUSED(argc); + Q_UNUSED(argv); + bool l_status = m_music_manager->toggleRootEnabled(m_current_area); + QString l_message = (l_status) ? "enabled." : "disabled."; + sendServerMessage("Global musiclist has been " + l_message); +} + +void AOClient::cmdClearCustom(int argc, QStringList argv) +{ + Q_UNUSED(argc); + Q_UNUSED(argv); + m_music_manager->clearCustomList(m_current_area); + sendServerMessage("Custom songs have been cleared."); +} diff --git a/core/src/config_manager.cpp b/core/src/config_manager.cpp index c82d719..56c0780 100644 --- a/core/src/config_manager.cpp +++ b/core/src/config_manager.cpp @@ -25,8 +25,9 @@ QSettings* ConfigManager::m_areas = new QSettings("config/areas.ini", QSettings: QSettings* ConfigManager::m_logtext = new QSettings("config/text/logtext.ini", QSettings::IniFormat); ConfigManager::CommandSettings* ConfigManager::m_commands = new CommandSettings(); QElapsedTimer* ConfigManager::m_uptimeTimer = new QElapsedTimer; -QHash>* ConfigManager::m_musicList = new QHash>; +MusicList* ConfigManager::m_musicList = new MusicList; QHash* ConfigManager::m_commands_help = new QHash; +QStringList* ConfigManager::m_ordered_list = new QStringList; bool ConfigManager::verifyServerConfig() { @@ -42,7 +43,7 @@ bool ConfigManager::verifyServerConfig() // Verify config files QStringList l_config_files{"config/config.ini", "config/areas.ini", "config/backgrounds.txt", "config/characters.txt", "config/music.json", "config/discord.ini", "config/text/8ball.txt", "config/text/gimp.txt", "config/text/praise.txt", - "config/text/reprimands.txt","config/text/commandhelp.json"}; + "config/text/reprimands.txt","config/text/commandhelp.json","config/text/cdns.txt"}; for (const QString &l_file : l_config_files) { if (!fileExists(QFileInfo(l_file))) { qCritical() << l_file + " does not exist!"; @@ -92,6 +93,7 @@ bool ConfigManager::verifyServerConfig() m_commands->praises = (loadConfigFile("praise")); m_commands->reprimands = (loadConfigFile("reprimands")); m_commands->gimps = (loadConfigFile("gimp")); + m_commands->cdns = (loadConfigFile("cdns")); m_uptimeTimer->start(); @@ -129,7 +131,7 @@ QStringList ConfigManager::backgrounds() return l_backgrounds; } -QStringList ConfigManager::musiclist() +MusicList ConfigManager::musiclist() { QFile l_music_json("config/music.json"); l_music_json.open(QIODevice::ReadOnly | QIODevice::Text); @@ -138,14 +140,13 @@ QStringList ConfigManager::musiclist() QJsonDocument l_music_list_json = QJsonDocument::fromJson(l_music_json.readAll(), &l_error); if (!(l_error.error == QJsonParseError::NoError)) { //Non-Terminating error. qWarning() << "Unable to load musiclist. The following error was encounted : " + l_error.errorString(); - return QStringList {}; //Server can still run without music. + return QMap>{}; //Server can still run without music. } // Akashi expects the musiclist to be contained in a JSON array, even if its only a single category. QJsonArray l_Json_root_array = l_music_list_json.array(); QJsonObject l_child_obj; QJsonArray l_child_array; - QStringList l_musiclist; for (int i = 0; i <= l_Json_root_array.size() -1; i++){ //Iterate trough entire JSON file to assemble musiclist l_child_obj = l_Json_root_array.at(i).toObject(); @@ -153,7 +154,7 @@ QStringList ConfigManager::musiclist() QString l_category_name = l_child_obj["category"].toString(); if (!l_category_name.isEmpty()) { m_musicList->insert(l_category_name,{l_category_name,0}); - l_musiclist.append(l_category_name); + m_ordered_list->append(l_category_name); } else { qWarning() << "Category name not set. This may cause the musiclist to be displayed incorrectly."; @@ -167,17 +168,19 @@ QStringList ConfigManager::musiclist() if (l_real_name.isEmpty()) { l_real_name = l_song_name; } - float l_song_duration = l_song_obj["length"].toVariant().toFloat(); + int l_song_duration = l_song_obj["length"].toVariant().toInt(); m_musicList->insert(l_song_name,{l_real_name,l_song_duration}); - l_musiclist.append(l_song_name); + m_ordered_list->append(l_song_name); } } l_music_json.close(); - if(!l_musiclist[0].contains("==")) // Add a default category if none exists - l_musiclist.insert(0,"==Music=="); + return *m_musicList; +} - return l_musiclist; +QStringList ConfigManager::ordered_songs() +{ + return *m_ordered_list; } void ConfigManager::loadCommandHelp() @@ -211,11 +214,6 @@ void ConfigManager::loadCommandHelp() } } -QPair ConfigManager::songInformation(const QString &f_songName) -{ - return m_musicList->value(f_songName); -} - QSettings* ConfigManager::areaData() { return m_areas; @@ -566,6 +564,11 @@ QStringList ConfigManager::gimpList() return m_commands->gimps; } +QStringList ConfigManager::cdnList() +{ + return m_commands->cdns; +} + bool ConfigManager::advertiseServer() { return m_settings->value("Advertiser/advertise","true").toBool(); diff --git a/core/src/music_manager.cpp b/core/src/music_manager.cpp new file mode 100644 index 0000000..6bb649f --- /dev/null +++ b/core/src/music_manager.cpp @@ -0,0 +1,237 @@ +#include "include/music_manager.h" + +MusicManager::MusicManager(QObject *parent, QStringList f_root_ordered, QStringList f_cdns, QMap> f_root_list) : + QObject(parent), + m_root_list(f_root_list), + m_root_ordered(f_root_ordered) +{ + m_custom_lists = new QHash>>; + if (!f_cdns.isEmpty()) { + m_cdns = f_cdns; + } +} + +MusicManager::~MusicManager() +{ + +} + +QStringList MusicManager::musiclist(int f_area_id) +{ + if (m_global_enabled.value(f_area_id)) { + QStringList l_combined_list = m_root_ordered; + l_combined_list.append(m_customs_ordered.value(f_area_id)); + return l_combined_list; + } + return m_custom_lists->value(f_area_id).keys(); +} + +QStringList MusicManager::rootMusiclist() +{ + return m_root_ordered; +} + +bool MusicManager::registerArea(int f_area_id) +{ + if(m_custom_lists->contains(f_area_id)) { + //This area is already registered. We can't add it. + return false; + } + m_custom_lists->insert(f_area_id,{}); + m_global_enabled.insert(f_area_id,true); + return true; +} + +bool MusicManager::validateSong(QString f_song_name, QStringList f_approved_cdns) +{ + QStringList l_extensions = {".opus", ".ogg", ".mp3", ".wav" }; + + bool l_cdn_approved = false; + //Check if URL formatted. + if (f_song_name.contains("/")) { + //Only allow HTTPS/HTTP sources. + if (f_song_name.startsWith("https://") || f_song_name.startsWith("http://")) { + for (const QString &l_cdn : qAsConst(f_approved_cdns)) { + //Iterate trough all available CDNs to find an approved match + if (f_song_name.startsWith("https://" + l_cdn + "/", Qt::CaseInsensitive) + || f_song_name.startsWith("http://" + l_cdn + "/", Qt::CaseInsensitive)) { + l_cdn_approved = true; + break; + } + } + if (!l_cdn_approved) { + return false; + } + } + else { + return false; + } + } + + bool l_suffix_found = false;; + for (const QString &suffix : qAsConst(l_extensions)) { + if (f_song_name.endsWith(suffix)){ + l_suffix_found = true; + break; + } + } + + if (!l_suffix_found) { + return false; + } + + return true; +} + +bool MusicManager::addCustomSong(QString f_song_name, QString f_real_name, int f_duration, int f_area_id) +{ + //Validate if simple name. + QString l_song_name = f_song_name; + if (f_song_name.split(".").size() == 1) { + l_song_name = l_song_name + ".opus"; + } + + QString l_real_name = f_real_name; + if (f_real_name.split(".").size() == 1) { + l_real_name = l_real_name + ".opus"; + } + + if (!(validateSong(l_song_name, m_cdns) && validateSong(l_real_name, m_cdns))) { + return false; + } + + // Avoid conflicts by checking if it exists. + if (m_root_list.contains(l_song_name) && m_global_enabled[f_area_id]) { + return false; + } + + if (m_custom_lists->value(f_area_id).contains(f_song_name)) { + return false; + } + + if (m_customs_ordered.value(f_area_id).contains(l_song_name)){ + return false; + } + + // There should be a way to directly insert into the QMap. Too bad! + MusicList l_custom_list = m_custom_lists->value(f_area_id); + l_custom_list.insert(l_song_name,{l_real_name,f_duration}); + m_custom_lists->insert(f_area_id,l_custom_list); + m_customs_ordered.insert(f_area_id,(QStringList {m_customs_ordered.value(f_area_id)} << l_song_name)); + emit sendAreaFMPacket(AOPacket("FM",musiclist(f_area_id)), f_area_id); + return true; +} + +bool MusicManager::addCustomCategory(QString f_category_name, int f_area_id) +{ + if (f_category_name.split(".").size() > 1) { + return false; + } + + QString l_category_name = f_category_name; + if (!f_category_name.startsWith("==")) { + l_category_name = "==" + l_category_name + "=="; + } + + // Avoid conflicts by checking if it exists. + if (m_root_list.contains(l_category_name) && m_global_enabled.value(f_area_id)) { + return false; + } + + if (m_custom_lists->value(f_area_id).contains(l_category_name)) { + return false; + } + + QMap> l_custom_list = m_custom_lists->value(f_area_id); + l_custom_list.insert(l_category_name,{l_category_name,0}); + m_custom_lists->insert(f_area_id,l_custom_list); + m_customs_ordered.insert(f_area_id,(QStringList {m_customs_ordered.value(f_area_id)} << l_category_name)); + emit sendAreaFMPacket(AOPacket("FM",musiclist(f_area_id)), f_area_id); + return true; +} + +bool MusicManager::removeCategorySong(QString f_songcategory_name, int f_area_id) +{ + if (!m_root_list.contains(f_songcategory_name)){ + MusicList l_custom_list = m_custom_lists->value(f_area_id); + if (l_custom_list.contains(f_songcategory_name)) { + l_custom_list.remove(f_songcategory_name); + m_custom_lists->insert(f_area_id,l_custom_list); + + //Updating the list alias too. + QStringList l_customs_ordered = m_customs_ordered.value(f_area_id); + l_customs_ordered.removeAll(f_songcategory_name); + m_customs_ordered.insert(f_area_id, l_customs_ordered); + + emit sendAreaFMPacket(AOPacket("FM",musiclist(f_area_id)), f_area_id); + return true; + } // Fallthrough + } + return false; +} + +bool MusicManager::toggleRootEnabled(int f_area_id) +{ + m_global_enabled.insert(f_area_id, !m_global_enabled.value(f_area_id)); + if (m_global_enabled.value(f_area_id)) { + sanitiseCustomList(f_area_id); + } + emit sendAreaFMPacket(AOPacket("FM",musiclist(f_area_id)), f_area_id); + return m_global_enabled.value(f_area_id); +} + +void MusicManager::sanitiseCustomList(int f_area_id) +{ + MusicList l_sanitised_list; + QStringList l_sanitised_ordered = m_customs_ordered.value(f_area_id); + for (auto iterator = m_custom_lists->value(f_area_id).keyBegin(), + end = m_custom_lists->value(f_area_id).keyEnd(); iterator != end; ++iterator) + { + QString l_key = iterator.operator*(); + if (!m_root_list.contains(l_key)) { + l_sanitised_list.insert(l_key, m_custom_lists->value(f_area_id).value(l_key)); + } + else { + l_sanitised_ordered.removeAll(l_key); + } + + } + m_custom_lists->insert(f_area_id, l_sanitised_list); + m_customs_ordered.insert(f_area_id, l_sanitised_ordered); +} + +void MusicManager::clearCustomList(int f_area_id) +{ + m_custom_lists->remove(f_area_id); + m_custom_lists->insert(f_area_id,{}); + m_customs_ordered.remove(f_area_id); + m_customs_ordered.insert(f_area_id, {}); +} + +QPair MusicManager::songInformation(QString f_song_name, int f_area_id) +{ + if (m_root_list.contains(f_song_name)) { + return m_root_list.value(f_song_name); + } + return m_custom_lists->value(f_area_id).value(f_song_name); +} + +bool MusicManager::isCustom(int f_area_id, QString f_song_name) +{ + if (m_customs_ordered.value(f_area_id).contains(f_song_name, Qt::CaseInsensitive)) { + return true; + } + return false; +} + +void MusicManager::reloadRequest() +{ + m_root_list = ConfigManager::musiclist(); + m_root_ordered = ConfigManager::ordered_songs(); + m_cdns = ConfigManager::cdnList(); +} + +void MusicManager::userJoinedArea(int f_area_index, int f_user_id) +{ + emit sendFMPacket(AOPacket("FM", musiclist(f_area_index)), f_user_id); +} diff --git a/core/src/packets.cpp b/core/src/packets.cpp index 2f4c90a..d1f3d6c 100644 --- a/core/src/packets.cpp +++ b/core/src/packets.cpp @@ -299,7 +299,7 @@ void AOClient::pktChangeMusic(AreaData* area, int argc, QStringList argv, AOPack // argument is a valid song QString l_argument = argv[0]; - if (server->m_music_list.contains(l_argument) || l_argument == "~stop.mp3") { // ~stop.mp3 is a dummy track used by 2.9+ + if (server->m_music_list.contains(l_argument) || m_music_manager->isCustom(m_current_area, l_argument) || l_argument == "~stop.mp3") { // ~stop.mp3 is a dummy track used by 2.9+ // We have a song here if (m_is_dj_blocked) { sendServerMessage("You are blocked from changing the music."); @@ -327,9 +327,8 @@ void AOClient::pktChangeMusic(AreaData* area, int argc, QStringList argv, AOPack return; } - QPair l_song = ConfigManager::songInformation(l_final_song); + QPair l_song = m_music_manager->songInformation(l_final_song, m_current_area); QString l_real_name = l_song.first; - qDebug() << l_real_name; AOPacket l_music_change("MC", {l_real_name, argv[1], m_showname, "1", "0", l_effects}); area->setCurrentMusic(l_final_song); area->setMusicPlayedBy(m_showname); diff --git a/core/src/server.cpp b/core/src/server.cpp index 3be931f..590c0cd 100644 --- a/core/src/server.cpp +++ b/core/src/server.cpp @@ -81,21 +81,32 @@ void Server::start() //Get characters from config file m_characters = ConfigManager::charlist(); - //Get musiclist from config file - m_music_list = ConfigManager::musiclist(); - //Get backgrounds from config file m_backgrounds = ConfigManager::backgrounds(); + //Build our music manager. + ConfigManager::musiclist(); + music_manager = new MusicManager(this, ConfigManager::ordered_songs(), ConfigManager::cdnList()); + connect(music_manager, &MusicManager::sendFMPacket, + this, &Server::unicast); + connect(music_manager, &MusicManager::sendAreaFMPacket, + this, QOverload::of(&Server::broadcast)); + + //Get musiclist from config file + m_music_list = music_manager->rootMusiclist(); + //Assembles the area list m_area_names = ConfigManager::sanitizedAreaNames(); QStringList raw_area_names = ConfigManager::rawAreaNames(); for (int i = 0; i < raw_area_names.length(); i++) { QString area_name = raw_area_names[i]; - AreaData* l_area = new AreaData(area_name, i); + AreaData* l_area = new AreaData(area_name, i, music_manager); m_areas.insert(i, l_area); - connect(l_area, &AreaData::playJukeboxSong, + connect(l_area, &AreaData::sendAreaPacket, this, QOverload::of(&Server::broadcast)); + connect(l_area, &AreaData::userJoinedArea, + music_manager, &MusicManager::userJoinedArea); + music_manager->registerArea(i); } //Loads the command help information. This is not stored inside the server. @@ -130,7 +141,7 @@ void Server::clientConnected() } int user_id = m_available_ids.dequeue(); - AOClient* client = new AOClient(this, socket, this, user_id); + AOClient* client = new AOClient(this, socket, this, user_id, music_manager); m_clients_ids.insert(user_id, client); int multiclient_count = 1; @@ -247,7 +258,6 @@ void Server::reloadSettings() emit updateHTTPConfiguration(); handleDiscordIntegration(); logger->loadLogtext(); - m_music_list = ConfigManager::musiclist(); m_ipban_list = ConfigManager::iprangeBans(); } @@ -304,6 +314,15 @@ void Server::broadcast(AOPacket packet, AOPacket other_packet, TARGET_TYPE targe } } +void Server::unicast(AOPacket f_packet, int f_client_id) +{ + AOClient* l_client = getClientByID(f_client_id); + if (l_client != nullptr) { // This should never happen, but safety first. + l_client->sendPacket(f_packet); + return; + } +} + QList Server::getClientsByIpid(QString ipid) { QList return_clients; diff --git a/tests/tests.pro b/tests/tests.pro index 8bc046a..6219044 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -1,4 +1,5 @@ TEMPLATE = subdirs SUBDIRS += \ - unittest_area + unittest_area \ + unittest_music_manager diff --git a/tests/unittest_area/tst_unittest_area.cpp b/tests/unittest_area/tst_unittest_area.cpp index 8a07ed8..5ec7151 100644 --- a/tests/unittest_area/tst_unittest_area.cpp +++ b/tests/unittest_area/tst_unittest_area.cpp @@ -66,7 +66,7 @@ private slots: void Area::init() { - m_area = new AreaData("Test Area", 0); + m_area = new AreaData("Test Area", 0, nullptr); } void Area::cleanup() diff --git a/tests/unittest_music_manager/tst_unittest_music_manager.cpp b/tests/unittest_music_manager/tst_unittest_music_manager.cpp new file mode 100644 index 0000000..6c091a5 --- /dev/null +++ b/tests/unittest_music_manager/tst_unittest_music_manager.cpp @@ -0,0 +1,322 @@ +#include + +#include + +namespace tests { +namespace unittests { + +/** + * @brief Unit Tester class for the musiclist-related functions. + */ +class MusicListManager : public QObject +{ + Q_OBJECT +public : + + MusicManager* m_music_manager; + +private slots: + /** + * @brief Initialises every tests with creating a new MusicManager with a small sample root list. + */ + void init(); + + /** + * @brief Tests the registration of areas in the music manager. + */ + void registerArea(); + + /** + * @brief Tests toggling the enabling/disabling of the prepend behaviour of our root list. + */ + void toggleRootEnabled(); + + /** + * @brief The data function for validateSong() + */ + void validateSong_data(); + + /** + * @brief Tests validation of song candidates. + */ + void validateSong(); + + /** + * @brief Tests the addition of custom music. + */ + void addCustomSong(); + + /** + * @brief Tests the addition of a custom category. + */ + void addCustomCategory(); + + /** + * @brief Test the sanitisation of the custom list when root prepend is reenabled. + */ + void sanitiseCustomList(); + + /** + * @brief Tests the removing of custom songs and categories. + */ + void removeCategorySong(); + + /** + * @brief Tests the retrieval of song information. + */ + void songInformation(); + + /** + * @brief Tests the retrieval of the full musiclist for an area. + */ + void musiclist(); + + +}; + +void MusicListManager::init() +{ + QMap> l_test_list; + l_test_list.insert("==Music==",{"==Music==",0}); + l_test_list.insert("Announce The Truth (AJ).opus",{"Announce The Truth (AJ).opus",59}); + l_test_list.insert("Announce The Truth (JFA).opus",{"Announce The Truth (JFA).opus",98}); + + QStringList l_list = {}; + l_list << "==Music==" << "Announce The Truth (AJ).opus" << "Announce The Truth (JFA).opus"; + + m_music_manager = new MusicManager(nullptr, l_list ,{"my.cdn.com","your.cdn.com"}, l_test_list); +} + +void MusicListManager::registerArea() +{ + { + //We register a single area with the music manager of ID 0. + //Creation should work as there are no other areas yet. + bool l_creation_success = m_music_manager->registerArea(0); + QCOMPARE(l_creation_success,true); + } + { + //Someone tries to register the same area again! + //This should fail as this area already exists. + bool l_creation_success = m_music_manager->registerArea(0); + QCOMPARE(l_creation_success,false); + } +} + +void MusicListManager::toggleRootEnabled() +{ + { + //We register an area of ID0 and toggle the inclusion of global list. + m_music_manager->registerArea(0); + QCOMPARE(m_music_manager->toggleRootEnabled(0), false); + } + { + //We toggle it again. It should return true now. + //Since this is now true, we should have the root list with customs cleared. + QCOMPARE(m_music_manager->toggleRootEnabled(0), true); + } +} + +void MusicListManager::validateSong_data() +{ + //Songname can also be the realname. + QTest::addColumn("songname"); + QTest::addColumn("expectedResult"); + + QTest::addRow("Songname - No extension") << "Announce The Truth (AA)" << false; + QTest::addRow("Songname - Valid Extension") << "Announce The Truth (AA).opus" << true; + QTest::addRow("Songname - Invalid Extension") << "Announce The Truth (AA).aac" << false; + QTest::addRow("URL - Valid primary") << "https://my.cdn.com/mysong.opus" << true; + QTest::addRow("URL - Valid secondary") << "https://your.cdn.com/mysong.opus" << true; + QTest::addRow("URL - Invalid extension") << "https://my.cdn.com/mysong.aac." << false; + QTest::addRow("URL - Invalid prefix") << "ftp://my.cdn.com/mysong.opus" << false; + QTest::addRow("URL - Invalid missing prefix") << "my.cdn.com/mysong.opus" << false; + QTest::addRow("URL - Invalid CDN") << "https://myipgrabber.com/mysong.opus" << false; + QTest::addRow("URL - Subdomain Attack") << "https://my.cdn.com.fakedomain.com/mysong.opus" << false; +} + +void MusicListManager::validateSong() +{ + QFETCH(QString,songname); + QFETCH(bool,expectedResult); + + bool l_result = m_music_manager->validateSong(songname, {"my.cdn.com","your.cdn.com"}); + QCOMPARE(expectedResult,l_result); +} + +void MusicListManager::addCustomSong() +{ + { + //Dummy register. + m_music_manager->registerArea(0); + + //No custom songs, so musiclist = root_list.size() + QCOMPARE(m_music_manager->musiclist(0).size(), 3); + } + { + //Add a song that's valid. The musiclist is now root_list.size() + custom_list.size() + m_music_manager->addCustomSong("mysong","mysong.opus",0,0); + QCOMPARE(m_music_manager->musiclist(0).size(), 4); + } + { + //Add a song that's part of the root list. This should fail and not increase the size. + bool l_result = m_music_manager->addCustomSong("Announce The Truth (AJ)","Announce The Truth (AJ).opus",0,0); + QCOMPARE(l_result,false); + QCOMPARE(m_music_manager->musiclist(0).size(), 4); + } + { + //Disable the root list. Musiclist is now custom_list.size() + m_music_manager->toggleRootEnabled(0); + QCOMPARE(m_music_manager->musiclist(0).size(), 1); + } + { + //Add an item that is in the root list into the custom list. Size is still custom_list.size() + bool l_result = m_music_manager->addCustomSong("Announce The Truth (AJ)","Announce The Truth (AJ).opus",0,0); + QCOMPARE(l_result,true); + QCOMPARE(m_music_manager->musiclist(0).size(), 2); + } + { + bool l_result = m_music_manager->addCustomSong("Announce The Truth (AJ)2","https://my.cdn.com/mysong.opus",0,0); + QCOMPARE(l_result,true); + QCOMPARE(m_music_manager->musiclist(0).size(), 3); + } +} + +void MusicListManager::addCustomCategory() +{ + { + //Dummy register. + m_music_manager->registerArea(0); + + //Add category to the custom list. Category marker are added manually. + bool l_result = m_music_manager->addCustomCategory("Music2",0); + QCOMPARE(l_result,true); + QCOMPARE(m_music_manager->musiclist(0).size(), 4); + QCOMPARE(m_music_manager->musiclist(0).at(3), "==Music2=="); + } + { + //Add a category that already exists on root. This should fail and not increase the size of our list. + bool l_result = m_music_manager->addCustomCategory("Music",0); + QCOMPARE(l_result, false); + QCOMPARE(m_music_manager->musiclist(0).size(), 4); + } + { + //We disable the root list. We now insert the category again. + m_music_manager->toggleRootEnabled(0); + bool l_result = m_music_manager->addCustomCategory("Music",0); + QCOMPARE(l_result, true); + QCOMPARE(m_music_manager->musiclist(0).size(), 2); + QCOMPARE(m_music_manager->musiclist(0).at(1), "==Music=="); + } + { + //Global now enabled. We add a song with three ===. + m_music_manager->toggleRootEnabled(0); + bool l_result = m_music_manager->addCustomCategory("===Music===",0); + QCOMPARE(l_result, true); + QCOMPARE(m_music_manager->musiclist(0).size(), 5); + QCOMPARE(m_music_manager->musiclist(0).at(4), "===Music==="); + + } +} + +void MusicListManager::sanitiseCustomList() +{ + //Prepare a dummy area with root list disabled.Insert both non-root and root elements. + m_music_manager->registerArea(0); + m_music_manager->toggleRootEnabled(0); + m_music_manager->addCustomCategory("Music",0); + m_music_manager->addCustomCategory("Music2",0); + m_music_manager->addCustomSong("Announce The Truth (AJ)","Announce The Truth (AJ).opus",0,0); + m_music_manager->addCustomSong("mysong","mysong.opus",0,0); + + //We now only have custom elements. + QCOMPARE(m_music_manager->musiclist(0).size(), 4); + + //We reenable the root list. Sanisation should only leave the non-root elements in the custom list. + m_music_manager->toggleRootEnabled(0); + QCOMPARE(m_music_manager->musiclist(0).size(), 5); + QCOMPARE(m_music_manager->musiclist(0).at(3), "==Music2=="); + QCOMPARE(m_music_manager->musiclist(0).at(4), "mysong.opus"); + +} + +void MusicListManager::removeCategorySong() +{ + { + //Prepare dummy area. Add both custom songs and categories. + m_music_manager->registerArea(0); + m_music_manager->addCustomCategory("Music2",0); + m_music_manager->addCustomSong("mysong","mysong.opus",0,0); + m_music_manager->addCustomCategory("Music3",0); + m_music_manager->addCustomSong("mysong2","mysong.opus",0,0); + QCOMPARE(m_music_manager->musiclist(0).size(), 7); + } + { + //Delete a category that is not custom. This should fail. + bool l_success = m_music_manager->removeCategorySong("==Music==",0); + QCOMPARE(l_success, false); + QCOMPARE(m_music_manager->musiclist(0).size(), 7); + } + { + //Correct category name, wrong format. + bool l_success = m_music_manager->removeCategorySong("Music2",0); + QCOMPARE(l_success, false); + QCOMPARE(m_music_manager->musiclist(0).size(), 7); + } + { + //Correct category name. This should be removed. + bool l_success = m_music_manager->removeCategorySong("==Music2==",0); + QCOMPARE(l_success, true); + QCOMPARE(m_music_manager->musiclist(0).size(), 6); + } + { + //Correct song name. This should be removed. This needs to be with the extension. + bool l_success = m_music_manager->removeCategorySong("mysong2.opus",0); + QCOMPARE(l_success, true); + QCOMPARE(m_music_manager->musiclist(0).size(), 5); + } +} + +void MusicListManager::songInformation() +{ + { + //Prepare dummy area. Add both custom songs and categories. + m_music_manager->registerArea(0); + m_music_manager->addCustomCategory("Music2",0); + m_music_manager->addCustomSong("mysong","realmysong.opus",47,0); + m_music_manager->addCustomCategory("Music3",0); + m_music_manager->addCustomSong("mysong2","mysong.opus",42,0); + } + { + QPair l_song_information = m_music_manager->songInformation("mysong.opus",0); + QCOMPARE(l_song_information.first, "realmysong.opus"); + QCOMPARE(l_song_information.second, 47); + } + { + QPair l_song_information = m_music_manager->songInformation("Announce The Truth (AJ).opus",0); + QCOMPARE(l_song_information.first, "Announce The Truth (AJ).opus"); + QCOMPARE(l_song_information.second, 59); + } +} + +void MusicListManager::musiclist() +{ + { + //Prepare dummy area. Add both custom songs and categories. + m_music_manager->registerArea(0); + m_music_manager->addCustomCategory("Music2",0); + m_music_manager->addCustomSong("mysong","realmysong.opus",47,0); + m_music_manager->addCustomCategory("Music3",0); + m_music_manager->addCustomSong("mysong2","mysong.opus",42,0); + } + { + QCOMPARE(m_music_manager->musiclist(0).size(), 7); + } +} + +} +} + +QTEST_APPLESS_MAIN(tests::unittests::MusicListManager) + +#include "tst_unittest_music_manager.moc" diff --git a/tests/unittest_music_manager/unittest_music_manager.pro b/tests/unittest_music_manager/unittest_music_manager.pro new file mode 100644 index 0000000..1b94722 --- /dev/null +++ b/tests/unittest_music_manager/unittest_music_manager.pro @@ -0,0 +1,5 @@ +QT -= gui + +include(../tests_common.pri) + +SOURCES += tst_unittest_music_manager.cpp