├── src ├── InterruptHandler.h ├── modules │ ├── TimezoneModule.h │ ├── ModerationModule.h │ ├── LevelModule.h │ ├── EventModule.h │ ├── UserModule.h │ ├── FunModule.h │ ├── TimezoneModule.cpp │ ├── PollSettings.cpp │ ├── CurrencyModule.h │ ├── UserModule.cpp │ ├── ModerationModule.cpp │ ├── FunModule.cpp │ ├── EventModule.cpp │ └── LevelModule.cpp ├── main.cpp ├── core │ ├── Permissions.h │ ├── Currency.h │ ├── Module.h │ ├── GuildSettings.h │ ├── Module.cpp │ ├── Permissions.cpp │ ├── Currency.cpp │ ├── Utility.h │ └── GuildSettings.cpp ├── InterruptHandler.cpp ├── Logger.h ├── Logger.cpp └── UmikoBot.h ├── .gitignore ├── .gitmodules ├── generate_project_files.sh ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── premake5.lua ├── init.sh ├── README.md └── res └── commands.json /src/InterruptHandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace InterruptHandler 4 | { 5 | void Init(); 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ 3 | /pmk/premake5.exe 4 | /pmk/premake5 5 | /sln/ 6 | /tmp/ 7 | /res/ 8 | !/res/commands.json 9 | /.vscode 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dep/QDiscord"] 2 | path = dep/QDiscord 3 | url = https://github.com/Gaztin/QDiscord.git 4 | [submodule "pmk/extensions/premake-qt"] 5 | path = pmk/extensions/premake-qt 6 | url = https://github.com/dcourtois/premake-qt.git 7 | [submodule "pmk/extensions/premake-qmake"] 8 | path = pmk/extensions/premake-qmake 9 | url = https://github.com/Gaztin/premake-qmake.git 10 | -------------------------------------------------------------------------------- /generate_project_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ ! -f "pmk/premake5" ]; then 3 | echo "Premake executable not found. Run 'init' to fetch one." 4 | read -p "Press enter to continue.." 5 | exit 6 | fi 7 | 8 | # If passing custom arguments, use those instead 9 | if [ $# -gt 0 ]; then 10 | args=$@ 11 | else 12 | # Determine default action from system 13 | os=$(uname -s) 14 | if [ "$os" == "Linux" ]; then 15 | args="qmake" 16 | elif [ "$os" == "Darwin" ]; then 17 | args="xcode4" 18 | else 19 | args="vs2019" 20 | fi 21 | fi 22 | 23 | pmk/premake5 $args 24 | -------------------------------------------------------------------------------- /src/modules/TimezoneModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "core/Module.h" 4 | 5 | class TimezoneModule : public Module 6 | { 7 | public: 8 | TimezoneModule(); 9 | 10 | void StatusCommand(QString& result, snowflake_t guild, snowflake_t user) override; 11 | 12 | private: 13 | void OnSave(QJsonDocument& doc) const override; 14 | void OnLoad(const QJsonDocument& doc) override; 15 | 16 | static QPair UtcOffsetFromString(const QString& string); 17 | static QString StringFromUtcOffset(int offset); 18 | 19 | struct Setting 20 | { 21 | int secondsFromUtc; 22 | }; 23 | 24 | QMap m_settings; 25 | }; 26 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "InterruptHandler.h" 2 | #include "UmikoBot.h" 3 | 4 | #include 5 | #include 6 | 7 | int main(int argc, char* argv[]) 8 | { 9 | InterruptHandler::Init(); 10 | 11 | qputenv("QT_QPA_PLATFORM_PLUGIN_PATH", "."); 12 | 13 | QApplication app(argc, argv); 14 | 15 | // Retrieve token from program arguments 16 | QStringList arguments = app.arguments(); 17 | if (arguments.count() < 2) 18 | { 19 | QMessageBox::critical(nullptr, "No token", "No token was provided!"); 20 | return -1; 21 | } 22 | 23 | // Log in 24 | Discord::Token token; 25 | token.generate(arguments.last(), Discord::Token::Type::BOT); 26 | UmikoBot::Instance().login(token); 27 | 28 | return app.exec(); 29 | } 30 | -------------------------------------------------------------------------------- /src/core/Permissions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | using PermissionCallback = std::function; 8 | 9 | namespace CommandPermission 10 | { 11 | enum 12 | { 13 | ADMIN = Discord::Permissions::ADMINISTRATOR | Discord::Permissions::MANAGE_GUILD, 14 | MODERATOR = ADMIN | Discord::Permissions::MANAGE_MESSAGES | Discord::Permissions::KICK_MEMBERS | Discord::Permissions::BAN_MEMBERS, 15 | }; 16 | }; 17 | 18 | class Permissions 19 | { 20 | public: 21 | static void ContainsPermission(Discord::Client& client, snowflake_t guildId, snowflake_t memberId, unsigned int permissionList, PermissionCallback callback); 22 | static void MatchesPermission(Discord::Client& client, snowflake_t guildId, snowflake_t memberId, unsigned int requiredPermission, PermissionCallback callback); 23 | }; -------------------------------------------------------------------------------- /src/InterruptHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "InterruptHandler.h" 2 | 3 | #include 4 | 5 | #if defined(Q_OS_WIN32) 6 | #include 7 | #elif defined(Q_OS_UNIX) 8 | #include 9 | #include 10 | #include 11 | #include 12 | #endif 13 | 14 | #if defined(Q_OS_WIN32) 15 | static BOOL WINAPI HandlerRoutine(DWORD sig) 16 | { 17 | QApplication::quit(); 18 | return TRUE; 19 | } 20 | #elif defined(Q_OS_UNIX) 21 | static void HandlerRoutine(int sig) 22 | { 23 | QApplication::quit(); 24 | } 25 | #endif 26 | 27 | void InterruptHandler::Init() 28 | { 29 | #if defined(Q_OS_WIN32) 30 | SetConsoleCtrlHandler(&HandlerRoutine, TRUE); 31 | #elif defined(Q_OS_UNIX) 32 | struct sigaction sigIntHandler{}; 33 | sigIntHandler.sa_handler = &HandlerRoutine; 34 | sigemptyset(&sigIntHandler.sa_mask); 35 | sigaction(SIGINT, &sigIntHandler, NULL); 36 | #endif 37 | } 38 | -------------------------------------------------------------------------------- /src/core/Currency.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "UmikoBot.h" 3 | 4 | class Currency { 5 | private: 6 | int m_Units = 0; 7 | int m_Cents = 0; 8 | 9 | public: 10 | Currency(); 11 | Currency(double); 12 | Currency(const Currency&); 13 | 14 | int cents() const; 15 | int units() const; 16 | 17 | Currency& operator=(const Currency&); 18 | 19 | explicit operator double() const; 20 | 21 | Currency operator+(const Currency&) const; 22 | Currency operator-(const Currency&) const; 23 | Currency operator*(const Currency&) const; 24 | Currency operator/(const Currency&) const; 25 | 26 | void operator+=(const Currency&); 27 | void operator-=(const Currency&); 28 | void operator*=(const Currency&); 29 | void operator/=(const Currency&); 30 | 31 | bool operator>=(const Currency&) const; 32 | bool operator<=(const Currency&) const; 33 | bool operator>(const Currency&) const; 34 | bool operator<(const Currency&) const; 35 | bool operator==(const Currency&) const; 36 | }; -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build_windows: 11 | runs-on: [self-hosted, windows] 12 | defaults: 13 | run: 14 | shell: powershell 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | submodules: true 19 | - name: Generate Project Files 20 | env: 21 | QTDIR: C:/Qt/5.15.1/msvc2019_64 22 | run: premake5.exe vs2019 23 | - name: Build Solution 24 | run: C:/"Program Files (x86)"/"Microsoft Visual Studio"/2019/BuildTools/MSBuild/Current/Bin/MSBuild.exe -property:Platform=x64 -property:Configuration=Release -maxCpuCount -verbosity:minimal -noLogo sln/UmikoBot.sln 25 | build_linux: 26 | runs-on: [self-hosted, linux] 27 | defaults: 28 | run: 29 | shell: bash 30 | steps: 31 | - uses: actions/checkout@v2 32 | with: 33 | submodules: true 34 | - name: Generate Project Files 35 | run: premake5 qmake 36 | - name: Make project 37 | run: | 38 | cd sln 39 | qmake-qt5 40 | make 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sebastian Kylander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modules/ModerationModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core/Module.h" 4 | 5 | class ModerationModule: public Module 6 | { 7 | public: 8 | ModerationModule(); 9 | 10 | void OnMessage(Discord::Client& client, const Discord::Message& message) override; 11 | 12 | void OnSave(QJsonDocument& doc) const override; 13 | void OnLoad(const QJsonDocument& doc) override; 14 | 15 | private: 16 | bool m_invitationModeration = true; 17 | QTimer m_warningCheckTimer; 18 | 19 | struct UserWarning 20 | { 21 | snowflake_t warnedBy; 22 | QDateTime when; 23 | QString message; 24 | bool expired; 25 | 26 | UserWarning(snowflake_t warnedBy, QString message) 27 | : warnedBy(warnedBy), when(QDateTime::currentDateTime()), message(message), expired(false) 28 | { 29 | } 30 | 31 | UserWarning(snowflake_t warnedBy, QDateTime when, QString message, bool expired) 32 | : warnedBy(warnedBy), when(when), message(message), expired(expired) 33 | { 34 | } 35 | }; 36 | 37 | // Maps user ID to a list of warnings 38 | QMap> warnings; 39 | 40 | unsigned int countWarnings(snowflake_t user, bool countExpired = false); 41 | void checkWarningsExpiry(); 42 | 43 | QSet dodgyDomainNames {}; 44 | }; 45 | -------------------------------------------------------------------------------- /src/modules/LevelModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "core/Module.h" 9 | 10 | #define LEVELMODULE_MAXIMUM_LEVEL 100 11 | #define LEVELMODULE_EXP_REQUIREMENT 100 // per level 12 | #define LEVELMODULE_EXP_GROWTH 1.12f 13 | 14 | class UmikoBot; 15 | 16 | struct ExpLevelData { 17 | unsigned int exp; 18 | unsigned int level; 19 | unsigned int xpRequirement; 20 | }; 21 | 22 | class LevelModule : public Module 23 | { 24 | public: 25 | LevelModule(UmikoBot* client); 26 | 27 | void OnSave(QJsonDocument& doc) const override; 28 | void OnLoad(const QJsonDocument& doc) override; 29 | 30 | ExpLevelData ExpToLevel(snowflake_t guild, unsigned int exp); 31 | 32 | void OnMessage(Discord::Client& client, const Discord::Message& message) override; 33 | 34 | void StatusCommand(QString& result, snowflake_t guild, snowflake_t user) override; 35 | 36 | private: 37 | struct GuildLevelData { 38 | snowflake_t user; 39 | int exp; 40 | int messageCount; 41 | }; 42 | 43 | GuildLevelData GetData(snowflake_t guild, snowflake_t user); 44 | 45 | mutable QMap> m_exp; 46 | mutable QMap> m_backupexp; 47 | QTimer m_timer; 48 | 49 | UmikoBot* m_client; 50 | }; 51 | -------------------------------------------------------------------------------- /src/core/Module.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | #include 6 | #include "GuildSettings.h" 7 | 8 | struct Command 9 | { 10 | using Callback = std::function; 11 | 12 | unsigned int id; 13 | QString name; 14 | Callback callback; 15 | }; 16 | 17 | class Module 18 | { 19 | public: 20 | virtual ~Module(); 21 | 22 | virtual void OnMessage(Discord::Client& client, const Discord::Message& message); 23 | inline bool IsEnabledByDefault() const { return m_enabledByDefault; } 24 | 25 | void Save() const; 26 | void Load(); 27 | 28 | virtual void StatusCommand(QString& result, snowflake_t guild, snowflake_t user) {} 29 | 30 | QList GetCommands() const { return m_commands; } 31 | QList& GetCommands() { return m_commands; } 32 | QString GetName() const { return m_name; } 33 | 34 | protected: 35 | Module(const QString& name, bool enabledByDefault); 36 | 37 | void RegisterCommand(unsigned int id, const QString& name, Command::Callback callback); 38 | 39 | virtual void OnSave(QJsonDocument& doc) const { Q_UNUSED(doc) }; 40 | virtual void OnLoad(const QJsonDocument& doc) { Q_UNUSED(doc) }; 41 | 42 | private: 43 | QString m_name; 44 | const bool m_enabledByDefault; 45 | QList m_commands; 46 | }; 47 | -------------------------------------------------------------------------------- /src/Logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Logging Macros 10 | #if defined(_UMIKO_DEBUG) 11 | #define UStartLogger(file) Logger::StartLogger_(file) 12 | #define ULog(severity, msg) Logger::Log_(severity, msg) 13 | #define USetThreadName(name) Logger::SetThreadName_(name) 14 | #define UStopLogger() Logger::StopLogger_() 15 | #else 16 | #define UStartLogger(file) 17 | #define USetThreadName(name) 18 | #define ULog(severity, msg) 19 | #define UStopLogger() 20 | #endif 21 | 22 | template 23 | constexpr QString UFString(const QString& fm, T value, Args&& ... d) 24 | { 25 | return QString::asprintf(fm.toUtf8().constData(), value, std::forward(d)...); 26 | } 27 | 28 | namespace ulog 29 | { 30 | enum Severity 31 | { 32 | Error, Warning, Debug 33 | }; 34 | } 35 | 36 | class Logger : QThread 37 | { 38 | private: 39 | #if defined(_UMIKO_DEBUG) 40 | static Logger* logger; 41 | 42 | QFile m_logFile; 43 | unsigned int m_lineNumber; 44 | QVector m_logs; 45 | 46 | QMutex m_mutex; 47 | QAtomicInt m_flag; 48 | 49 | Logger(const QString& fileLocation); 50 | protected: 51 | void run(); 52 | #endif 53 | public: 54 | 55 | #if defined(_UMIKO_DEBUG) 56 | static void StartLogger_(const QString& file_loc); 57 | static void SetThreadName_(const QString& str); 58 | static void StopLogger_(); 59 | 60 | static void Log_(ulog::Severity s, const QString&fm); 61 | #endif 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /src/core/GuildSettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct LevelRank 13 | { 14 | QString name; 15 | unsigned int minimumLevel; 16 | }; 17 | 18 | struct GuildSetting 19 | { 20 | snowflake_t id; 21 | 22 | QString prefix; 23 | snowflake_t primaryChannel; 24 | QList> modules; 25 | 26 | // Level system related 27 | QList ranks; 28 | unsigned int maximumLevel; 29 | unsigned int expRequirement; 30 | float growthRate; 31 | QList levelWhitelistedChannels; 32 | QList levelBlacklistedChannels; 33 | 34 | QList outputWhitelistedChannels; 35 | QList outputBlacklistedChannels; 36 | }; 37 | 38 | class GuildSettings 39 | { 40 | static QList s_settings; 41 | static QString s_location; 42 | 43 | public: 44 | static void Load(const QString& location); 45 | static void Save(); 46 | 47 | static GuildSetting& GetGuildSetting(snowflake_t id); 48 | 49 | static void AddGuild(snowflake_t id); 50 | 51 | static bool IsModuleEnabled(snowflake_t guild, const QString& moduleName, bool isDefault = true); 52 | static void ToggleModule(snowflake_t guild, const QString& moduleName, bool enabled, bool isDefault = true); 53 | 54 | static bool OutputAllowed(snowflake_t guild, snowflake_t channel); 55 | static bool ExpAllowed(snowflake_t guild, snowflake_t channel); 56 | private: 57 | static GuildSetting CreateGuildSetting(snowflake_t id); 58 | }; 59 | -------------------------------------------------------------------------------- /src/core/Module.cpp: -------------------------------------------------------------------------------- 1 | #include "Module.h" 2 | #include "core/Permissions.h" 3 | 4 | #include 5 | #include 6 | 7 | Module::Module(const QString& name, bool enabledByDefault) 8 | : m_name(name) 9 | , m_enabledByDefault(enabledByDefault) 10 | { 11 | } 12 | 13 | Module::~Module() 14 | { 15 | 16 | } 17 | 18 | void Module::OnMessage(Discord::Client& client, const Discord::Message& message) 19 | { 20 | client.getChannel(message.channelId()).then( 21 | [this, message, &client](const Discord::Channel& channel) 22 | { 23 | GuildSetting setting = GuildSettings::GetGuildSetting(channel.guildId()); 24 | if (channel.guildId() != 0 && !message.author().bot()) // DM 25 | if (GuildSettings::IsModuleEnabled(channel.guildId(), m_name, m_enabledByDefault) && GuildSettings::OutputAllowed(channel.guildId(), channel.id())) 26 | { 27 | for (const Command& command : m_commands) 28 | { 29 | if (message.content().startsWith(setting.prefix + command.name) && message.content().split(' ').at(0) == (setting.prefix + command.name)) 30 | { 31 | command.callback(client, message, channel); 32 | } 33 | } 34 | } 35 | }); 36 | } 37 | 38 | void Module::RegisterCommand(unsigned int id, const QString& name, Command::Callback callback) 39 | { 40 | m_commands.push_back({id, name, callback}); 41 | } 42 | 43 | void Module::Save() const 44 | { 45 | QFile file("configs/" + m_name + ".json"); 46 | if (file.open(QFile::ReadWrite | QFile::Truncate)) 47 | { 48 | QJsonDocument doc; 49 | OnSave(doc); 50 | 51 | file.write(doc.toJson(QJsonDocument::Indented)); 52 | file.close(); 53 | } 54 | } 55 | 56 | void Module::Load() 57 | { 58 | QFile file("configs/" + m_name + ".json"); 59 | if (file.open(QFile::ReadOnly)) 60 | { 61 | OnLoad(QJsonDocument::fromJson(file.readAll())); 62 | file.close(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/EventModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "core/Module.h" 4 | #include "UmikoBot.h" 5 | 6 | //Used for the events (Needed in the EventModule.cpp and CurrencyModule.cpp) 7 | #define highRiskRewardBonus 50 // In percentage 8 | #define lowRiskRewardPenalty 40 //In percentage 9 | 10 | class EventModule : public Module 11 | { 12 | public: 13 | struct RaffleDraw 14 | { 15 | public: 16 | snowflake_t m_UserId; 17 | QList m_TicketIds; 18 | RaffleDraw(snowflake_t userID) 19 | :m_UserId(userID){} 20 | }; 21 | 22 | struct EventConfig 23 | { 24 | QTimer* eventTimer{ nullptr }; 25 | QTimer* raffleDrawRewardClaimTimer{ nullptr }; 26 | bool isEventRunning{ false }; 27 | bool isHighRiskHighRewardRunning{ false }; 28 | bool isLowRiskLowRewardRunning{ false }; 29 | bool claimedReward = true; 30 | bool eventRaffleDrawRunning{ false }; 31 | unsigned int numTicketsBought{ 0 }; 32 | int raffleDrawTicketPrice{ 50 }; 33 | int maxUserTickets{ 20 }; 34 | snowflake_t luckyUser; 35 | QList roleWhiteList; 36 | }; 37 | EventModule(UmikoBot* client); 38 | void OnMessage(Discord::Client& client, const Discord::Message& message) override; 39 | 40 | void OnSave(QJsonDocument& doc) const override; 41 | void OnLoad(const QJsonDocument& doc) override; 42 | private: 43 | QList eventNamesAndCodes = {"HRHR", "LRLR", "RaffleDraw"}; 44 | QMap> raffleDrawGuildList; 45 | QMapserverEventConfig; 46 | 47 | int raffleDrawGetUserIndex(snowflake_t guild, snowflake_t id); 48 | 49 | public: 50 | void EndEvent(const snowflake_t& channelID, const snowflake_t& guildID, const snowflake_t& authorID, bool isInQObjectConnect, QString eventNameOrCode); 51 | RaffleDraw getUserRaffleDrawData(snowflake_t guild, snowflake_t id); 52 | 53 | EventConfig& getServerEventData(snowflake_t guild) 54 | { 55 | return serverEventConfig[guild]; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/core/Permissions.cpp: -------------------------------------------------------------------------------- 1 | #include "Permissions.h" 2 | #include "UmikoBot.h" 3 | 4 | void Permissions::ContainsPermission(Discord::Client& client, snowflake_t guildId, snowflake_t memberId, unsigned int permissionList, PermissionCallback callback) 5 | { 6 | client.getGuildMember(guildId, memberId).then( 7 | [&client, guildId, memberId, permissionList, callback](const Discord::GuildMember& member) 8 | { 9 | UmikoBot* bot = reinterpret_cast(&client); 10 | if (bot->IsOwner(guildId, memberId)) 11 | return callback(true); 12 | 13 | unsigned int totalPermissions = 0; 14 | for (const Discord::Role& role : bot->GetRoles(guildId)) 15 | for (snowflake_t roleId : member.roles()) 16 | if (roleId == role.id()) 17 | { 18 | totalPermissions |= role.permissions(); 19 | break; 20 | } 21 | 22 | unsigned int x = 1; 23 | while (x <= permissionList) 24 | { 25 | unsigned int currentPermission = permissionList & x; 26 | if ((totalPermissions & currentPermission) != 0) 27 | return callback(true); 28 | x = x << 1; 29 | } 30 | return callback(false); 31 | }); 32 | } 33 | 34 | void Permissions::MatchesPermission(Discord::Client& client, snowflake_t guildId, snowflake_t memberId, unsigned int requiredPermission, PermissionCallback callback) 35 | { 36 | client.getGuildMember(guildId, memberId).then( 37 | [&client, guildId, memberId, requiredPermission, callback](const Discord::GuildMember& member) 38 | { 39 | UmikoBot* bot = reinterpret_cast(&client); 40 | if (bot->IsOwner(guildId, memberId)) 41 | return callback(true); 42 | 43 | unsigned int totalPermissions = 0; 44 | for (const Discord::Role& role : bot->GetRoles(guildId)) 45 | for (snowflake_t roleId : member.roles()) 46 | if (roleId == role.id()) 47 | { 48 | totalPermissions |= role.permissions(); 49 | break; 50 | } 51 | 52 | if ((totalPermissions & requiredPermission) == requiredPermission) 53 | return callback(true); 54 | 55 | return callback(false); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/UserModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "core/Module.h" 8 | 9 | class UmikoBot; 10 | 11 | class UserModule : public Module 12 | { 13 | public: 14 | UserModule(); 15 | 16 | void OnSave(QJsonDocument& doc) const override; 17 | void OnLoad(const QJsonDocument& doc) override; 18 | void OnMessage(Discord::Client& client, const Discord::Message& message) override; 19 | 20 | private: 21 | struct UserDescription 22 | { 23 | snowflake_t userId; 24 | 25 | QString name; 26 | QString location; 27 | QString industry; 28 | QString programmingInterests; 29 | QString currentlyWorkingOn; 30 | QString githubLink; 31 | }; 32 | 33 | QMap> userDescriptions; 34 | 35 | struct DescriptionData 36 | { 37 | bool isBeingUsed = false; 38 | snowflake_t messageId; // The iam message that started it 39 | snowflake_t userId = 0; 40 | QTimer* timer; 41 | unsigned int questionUpTo = 0; 42 | 43 | UserDescription* currentUserDescription; 44 | UserDescription oldUserDescription; 45 | }; 46 | 47 | QMap guildDescriptionData; 48 | using questionFunc = void (*)(UserDescription& desc, const QString& value); 49 | 50 | #define QUESTION(question, field) qMakePair(question, [](UserDescription& desc, const QString& value) { desc.field = value; }) 51 | 52 | const QList> descriptionQuestions = { 53 | QUESTION("What is your name?", name), 54 | QUESTION("Where are you from?", location), 55 | QUESTION("What industry do you work in?", industry), 56 | QUESTION("What areas of programming are you interested in?", programmingInterests), 57 | QUESTION("What are you currently working on?", currentlyWorkingOn), 58 | QUESTION("Link to a GitHub profile:", githubLink), 59 | }; 60 | 61 | #undef QUESTION 62 | 63 | snowflake_t getUserIndex(snowflake_t guild, snowflake_t id); 64 | QString formDescriptionMessage(const UserDescription& desc) const; 65 | }; 66 | -------------------------------------------------------------------------------- /src/Logger.cpp: -------------------------------------------------------------------------------- 1 | #include "Logger.h" 2 | 3 | #include 4 | #include 5 | 6 | #if defined(_UMIKO_DEBUG) 7 | 8 | Logger* Logger::logger = nullptr; 9 | 10 | Logger::Logger(const QString& fileLocation) 11 | { 12 | m_logFile.setFileName(fileLocation); 13 | assert(m_logFile.open(QIODevice::Append)); 14 | m_lineNumber = 1; 15 | m_logs.clear(); 16 | m_flag = true; 17 | } 18 | 19 | void Logger::run() 20 | { 21 | do 22 | { 23 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 24 | if (m_logs.size()) 25 | { 26 | if (!m_mutex.try_lock_for(std::chrono::microseconds(50))) 27 | continue; 28 | Q_FOREACH(QString log, m_logs) 29 | { 30 | m_logFile.write(log.toUtf8()); 31 | qDebug("%s", log.toUtf8().constData()); 32 | } 33 | m_logs.clear(); 34 | m_mutex.unlock(); 35 | } 36 | } while (m_flag.testAndSetRelease(true, true) || m_logs.size()); 37 | m_logFile.close(); 38 | } 39 | 40 | void Logger::StartLogger_(const QString& file_loc) 41 | { 42 | if (!logger) 43 | logger = new Logger(file_loc); 44 | logger->start(); 45 | } 46 | 47 | void Logger::SetThreadName_(const QString & str) 48 | { 49 | QThread::currentThread()->setObjectName(str); 50 | } 51 | 52 | void Logger::StopLogger_() 53 | { 54 | logger->m_flag = false; 55 | logger->quit(); 56 | logger->wait(); 57 | delete logger; 58 | logger = nullptr; 59 | } 60 | 61 | void Logger::Log_(ulog::Severity s, const QString & fm) 62 | { 63 | QString log = QString::number(logger->m_lineNumber++) + "> "; 64 | log.append(QTime::currentTime().toString("hh:mm:ss")); 65 | log.append(" Thread: " + QThread::currentThread()->objectName()); 66 | 67 | switch (s) 68 | { 69 | case ulog::Severity::Error: 70 | log.append(" [ERROR] "); break; 71 | case ulog::Severity::Warning: 72 | log.append(" [WARNING] "); break; 73 | case ulog::Severity::Debug: 74 | log.append(" [DEBUG] "); break; 75 | } 76 | 77 | log.append(fm); 78 | //std::lock_guard lock(logger->m_mutex); 79 | QMutexLocker lock(&logger->m_mutex); 80 | logger->m_logs.push_back(log); 81 | //logger->m_mutex.unlock(); 82 | } 83 | #endif -------------------------------------------------------------------------------- /src/modules/FunModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | // Fun and Utilities! 3 | #include "core/Module.h" 4 | #include "UmikoBot.h" 5 | #include "QtCore/QMap" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class FunModule : public Module, public QObject 14 | { 15 | private: 16 | struct PollOption { 17 | QString emote = ""; 18 | QString desc = ""; 19 | bool isAnimated = false; 20 | std::size_t count = 0; 21 | }; 22 | using PollOptions = QList; 23 | 24 | struct PollSettings; 25 | using Poll = std::shared_ptr; 26 | using ServerPolls = std::shared_ptr>; 27 | using Polls = QMap; 28 | 29 | struct PollSettings { 30 | PollOptions options; 31 | //Default: Finish only when the timer ends 32 | long long maxVotes; 33 | snowflake_t notifChannel; 34 | snowflake_t pollMsg; 35 | QString pollName; 36 | int pollNum; 37 | std::shared_ptr timer; 38 | ServerPolls polls; 39 | 40 | PollSettings(const PollOptions& op, long long maxvotes, snowflake_t chan, const QString& name, int num, double time, const ServerPolls& pollList, snowflake_t msg); 41 | }; 42 | Polls m_polls; 43 | 44 | //! A list of roles (for each server) that have been given poll 45 | //! creation access 46 | QMap> m_pollWhitelist; 47 | QNetworkAccessManager m_MemeManager, m_GithubManager; 48 | snowflake_t m_memeChannel, m_GithubChannel; 49 | UmikoBot* m_client; 50 | 51 | void pollReactAndAdd(const PollOptions& options, int pos, const Poll& poll, snowflake_t msg, snowflake_t chan, snowflake_t guild); 52 | 53 | public: 54 | FunModule(UmikoBot* client); 55 | 56 | void onReact(snowflake_t user, snowflake_t channel, snowflake_t message, const Discord::Emoji& emoji) const; 57 | void onUnReact(snowflake_t user, snowflake_t channel, snowflake_t message, const Discord::Emoji& emoji) const; 58 | 59 | void OnMessage(Discord::Client& client, const Discord::Message& message) override; 60 | 61 | void OnSave(QJsonDocument& doc) const override; 62 | void OnLoad(const QJsonDocument& doc) override; 63 | }; -------------------------------------------------------------------------------- /premake5.lua: -------------------------------------------------------------------------------- 1 | if _ACTION:match("vs*") then 2 | require "pmk/extensions/premake-qt/qt" 3 | elseif _ACTION == "qmake" then 4 | require "pmk/extensions/premake-qmake/qmake" 5 | end 6 | 7 | local qtdir_x86 = io.readfile("tmp/.qtdir_x86") 8 | local qtdir_x64 = io.readfile("tmp/.qtdir_x64") 9 | 10 | workspace "UmikoBot" 11 | location "sln/" 12 | configurations { 13 | "Debug", 14 | "Release", 15 | } 16 | platforms { 17 | "x86", 18 | "x64", 19 | } 20 | 21 | project "UmikoBot" 22 | location "sln/prj/" 23 | kind "ConsoleApp" 24 | cppdialect "C++11" 25 | files { 26 | "src/**.cpp", 27 | "src/**.h", 28 | "src/**.qrc", 29 | "src/**.ui", 30 | } 31 | flags { 32 | "MultiProcessorCompile", 33 | } 34 | includedirs { 35 | "src/", 36 | "dep/QDiscord/src/core/", 37 | } 38 | links { 39 | "QDiscordCore", 40 | } 41 | qtmodules { 42 | "core", 43 | "gui", 44 | "network", 45 | "websockets", 46 | "widgets", 47 | } 48 | 49 | filter {"configurations:Release"} 50 | optimize "Full" 51 | defines { 52 | "QT_NO_DEBUG", 53 | } 54 | 55 | filter {"platforms:x86"} 56 | debugdir "res/" 57 | objdir "obj/x86/" 58 | targetdir "bin/x86/" 59 | 60 | filter {"platforms:x64"} 61 | debugdir "res/" 62 | objdir "obj/x64/" 63 | targetdir "bin/x64/" 64 | 65 | filter {"toolset:msc"} 66 | disablewarnings { "C4996" } 67 | 68 | filter {} 69 | 70 | -- Enable premake-qt when targeting Visual Studio 71 | if _ACTION:match("vs*") then 72 | premake.extensions.qt.enable() 73 | qtprefix "Qt5" 74 | qtgenerateddir "src/GeneratedFiles/" 75 | 76 | filter {"configurations:Debug"} 77 | qtsuffix "d" 78 | 79 | filter {"platforms:x86"} 80 | qtpath (qtdir_x86) 81 | 82 | filter {"platforms:x64"} 83 | qtpath (qtdir_x64) 84 | end 85 | 86 | group "QDiscord" 87 | 88 | project "QDiscordCore" 89 | location "sln/prj/" 90 | kind "StaticLib" 91 | cppdialect "C++11" 92 | files { 93 | "dep/QDiscord/src/core/**.h", 94 | "dep/QDiscord/src/core/**.cpp", 95 | } 96 | flags { 97 | "MultiProcessorCompile", 98 | } 99 | includedirs { 100 | "dep/QDiscord/src/core/", 101 | } 102 | 103 | filter {"configurations:Release"} 104 | optimize "Full" 105 | defines { 106 | "QT_NO_DEBUG", 107 | } 108 | 109 | filter {"platforms:x86"} 110 | objdir "obj/x86/" 111 | targetdir "bin/x86/" 112 | 113 | filter {"platforms:x64"} 114 | objdir "obj/x64/" 115 | targetdir "bin/x64/" 116 | 117 | filter {} 118 | 119 | -- Enable premake-qt when targeting Visual Studio 120 | if _ACTION:match("vs*") then 121 | premake.extensions.qt.enable() 122 | qtprefix "Qt5" 123 | qtgenerateddir "dep/QDiscord/src/core/GeneratedFiles/" 124 | 125 | filter {"configurations:Debug"} 126 | qtsuffix "d" 127 | 128 | filter {"platforms:x86"} 129 | qtpath (qtdir_x86) 130 | 131 | filter {"platforms:x64"} 132 | qtpath (qtdir_x64) 133 | end 134 | -------------------------------------------------------------------------------- /src/core/Currency.cpp: -------------------------------------------------------------------------------- 1 | #include "Currency.h" 2 | #include 3 | 4 | Currency::Currency() : m_Cents(0), m_Units(0) 5 | { 6 | } 7 | 8 | Currency::Currency(double cents) 9 | : m_Cents(qFloor(cents * 100.0) % 100), m_Units(qFloor(cents)) 10 | { 11 | } 12 | 13 | Currency::Currency(const Currency& other) 14 | : m_Cents(other.m_Cents), m_Units(other.m_Units) 15 | { 16 | } 17 | 18 | int Currency::cents() const 19 | { 20 | return m_Cents; 21 | } 22 | 23 | int Currency::units() const 24 | { 25 | return m_Units; 26 | } 27 | 28 | Currency& Currency::operator=(const Currency& other) 29 | { 30 | m_Units = other.m_Units; 31 | m_Cents = other.m_Cents; 32 | return *this; 33 | } 34 | 35 | Currency::operator double() const 36 | { 37 | return (double)m_Cents / 100.0 + (double)m_Units; 38 | } 39 | 40 | Currency Currency::operator+(const Currency& other) const 41 | { 42 | return (m_Cents + other.m_Cents) / 100 + m_Units + other.m_Units; 43 | } 44 | Currency Currency::operator-(const Currency& other) const 45 | { 46 | return (m_Cents - other.m_Cents) / 100 + m_Units - other.m_Units; 47 | } 48 | 49 | Currency Currency::operator*(const Currency& other) const 50 | { 51 | double tDouble = (double)*this; 52 | double oDouble = (double)other; 53 | return tDouble * oDouble; 54 | } 55 | Currency Currency::operator/(const Currency& other) const 56 | { 57 | double tDouble = (double)*this; 58 | double oDouble = (double)other; 59 | return tDouble / oDouble; 60 | } 61 | 62 | void Currency::operator+=(const Currency& other) 63 | { 64 | m_Cents += other.m_Cents; 65 | m_Units += other.m_Units; 66 | } 67 | 68 | void Currency::operator-=(const Currency& other) 69 | { 70 | m_Cents -= other.m_Cents; 71 | m_Units -= other.m_Units; 72 | } 73 | 74 | void Currency::operator*=(const Currency& other) 75 | { 76 | double tDouble = (double)*this; 77 | double oDouble = (double)other; 78 | *(this) = tDouble * oDouble; 79 | } 80 | 81 | void Currency::operator/=(const Currency& other) 82 | { 83 | double tDouble = (double)*this; 84 | double oDouble = (double)other; 85 | *(this) = tDouble / oDouble; 86 | } 87 | 88 | bool Currency::operator<(const Currency& other) const 89 | { 90 | return (m_Cents + m_Units) < (other.m_Cents + other.m_Units); 91 | } 92 | 93 | bool Currency::operator>(const Currency& other) const 94 | { 95 | return (m_Cents + m_Units) > (other.m_Cents + other.m_Units); 96 | } 97 | 98 | bool Currency::operator<=(const Currency& other) const 99 | { 100 | return (m_Cents + m_Units) <= (other.m_Cents + other.m_Units); 101 | } 102 | 103 | bool Currency::operator>=(const Currency& other) const 104 | { 105 | return (m_Cents + m_Units) >= (other.m_Cents + other.m_Units); 106 | } 107 | 108 | bool Currency::operator==(const Currency& other) const 109 | { 110 | return (m_Cents + m_Units) == (other.m_Cents + other.m_Units); 111 | } -------------------------------------------------------------------------------- /src/modules/TimezoneModule.cpp: -------------------------------------------------------------------------------- 1 | #include "TimezoneModule.h" 2 | #include "UmikoBot.h" 3 | 4 | using namespace Discord; 5 | 6 | TimezoneModule::TimezoneModule() 7 | : Module("timezone", true) 8 | { 9 | RegisterCommand(Commands::TIMEZONE_MODULE_TIMEOFFSET, "timeoffset", 10 | [this](Client& client, const Message& message, const Channel& channel) 11 | { 12 | QStringList arguments = message.content().split(' '); 13 | if (arguments.count() == 2) 14 | { 15 | Setting& setting = m_settings[message.author().id()]; 16 | auto offsetResult = UtcOffsetFromString(arguments[1]); 17 | if (offsetResult.second) 18 | { 19 | setting.secondsFromUtc = offsetResult.first; 20 | client.createMessage(message.channelId(), "Timezone set to " + StringFromUtcOffset(setting.secondsFromUtc)); 21 | } 22 | else 23 | { 24 | client.createMessage(message.channelId(), "Invalid time format"); 25 | } 26 | 27 | qDebug() << m_settings[message.author().id()].secondsFromUtc; 28 | } 29 | }); 30 | } 31 | 32 | void TimezoneModule::StatusCommand(QString& result, snowflake_t guild, snowflake_t user) 33 | { 34 | const int secondsFromUtc = m_settings[user].secondsFromUtc; 35 | result += "Time offset: " + StringFromUtcOffset(secondsFromUtc) + "\n"; 36 | result += "Current time: " + QDateTime::currentDateTimeUtc().addSecs(secondsFromUtc).toString("hh':'mm") + "\n"; 37 | result += "\n"; 38 | } 39 | 40 | void TimezoneModule::OnSave(QJsonDocument& doc) const 41 | { 42 | QJsonObject docObj; 43 | 44 | for (auto it = m_settings.begin(); it != m_settings.end(); ++it) 45 | { 46 | QJsonObject obj; 47 | obj["timezone"] = it->secondsFromUtc; 48 | 49 | docObj[QString::number(it.key())] = obj; 50 | } 51 | 52 | doc.setObject(docObj); 53 | } 54 | 55 | void TimezoneModule::OnLoad(const QJsonDocument& doc) 56 | { 57 | QJsonObject docObj = doc.object(); 58 | 59 | for (auto it = docObj.begin(); it != docObj.end(); ++it) 60 | { 61 | const QJsonObject obj = it.value().toObject(); 62 | Setting& setting = m_settings[it.key().toULongLong()]; 63 | 64 | setting.secondsFromUtc = obj["timezone"].toInt(); 65 | } 66 | } 67 | 68 | QPair TimezoneModule::UtcOffsetFromString(const QString& string) 69 | { 70 | auto clamp = [](int v, int mini, int maxi) { return (v < mini) ? mini : (v > maxi) ? maxi : v; }; 71 | 72 | QStringList units = string.split(':'); 73 | switch (units.size()) 74 | { 75 | case 1: 76 | { 77 | bool ok; 78 | int h = units[0].toInt(&ok); 79 | if (!ok) 80 | return qMakePair(0, false); 81 | h = clamp(h, -23, 23); 82 | 83 | return qMakePair(h * 3600, true); 84 | } 85 | 86 | case 2: 87 | { 88 | bool ok; 89 | int h = units[0].toInt(&ok); 90 | if (!ok) 91 | return qMakePair(0, false); 92 | h = clamp(h, -23, 23); 93 | 94 | int m = units[1].toInt(&ok); 95 | if (!ok || m < 0) 96 | return qMakePair(h * 3600, true); 97 | m = clamp(m, 0, 59); 98 | if (h < 0) 99 | m *= -1; 100 | 101 | return qMakePair(h * 3600 + m * 60, true); 102 | } 103 | 104 | default: 105 | return qMakePair(0, false); 106 | } 107 | } 108 | 109 | QString TimezoneModule::StringFromUtcOffset(int offset) 110 | { 111 | if (offset < 0) 112 | { 113 | offset *= -1; 114 | return QString::asprintf("UTC-%02d:%02d", offset / 3600, (offset % 3600) / 60); 115 | } 116 | else 117 | { 118 | return QString::asprintf("UTC+%02d:%02d", offset / 3600, (offset % 3600) / 60); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/UmikoBot.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "core/GuildSettings.h" 4 | #include "core/Module.h" 5 | 6 | namespace Commands { 7 | enum { 8 | GLOBAL_STATUS, 9 | GLOBAL_HELP, 10 | GLOBAL_SET_PREFIX, 11 | GLOBAL_MODULE, 12 | GLOBAL_OUTPUT, 13 | GLOBAL_SET_PRIMARY_CHANNEL, 14 | 15 | LEVEL_MODULE_TOP, 16 | LEVEL_MODULE_RANK, 17 | LEVEL_MODULE_MAX_LEVEL, 18 | LEVEL_MODULE_EXP_REQUIREMENT, 19 | LEVEL_MODULE_EXP_GROWTH_RATE, 20 | LEVEL_MODULE_EXP_GIVE, 21 | LEVEL_MODULE_EXP_TAKE, 22 | LEVEL_MODULE_BLOCK_EXP, 23 | 24 | USER_MODULE_WHO_IS, 25 | USER_MODULE_I_AM, 26 | USER_MODULE_ACHIEVEMENTS, 27 | 28 | TIMEZONE_MODULE_TIMEOFFSET, 29 | 30 | MODERATION_INVITATION_TOGGLE, 31 | MODERATION_ADD_DODGY_DOMAIN, 32 | MODERATION_REMOVE_DODGY_DOMAIN, 33 | 34 | MODERATION_WARN, 35 | MODERATION_WARNINGS, 36 | MODERATION_WARNINGS_ALL, 37 | 38 | CURRENCY_WALLET, 39 | CURRENCY_DAILY, 40 | CURRENCY_GAMBLE, 41 | CURRENCY_CLAIM, 42 | CURRENCY_GIFT, 43 | CURRENCY_SET_PRIZE_CHANNEL, 44 | CURRENCY_SET_NAME, 45 | CURRENCY_SET_SYMBOL, 46 | CURRENCY_SET_DAILY, 47 | CURRENCY_SET_PRIZE, 48 | CURRENCY_SET_GAMBLE_LOSS, 49 | CURRENCY_SET_GAMBLE_REWARD, 50 | CURRENCY_SET_GAMBLE_MIN_GUESS, 51 | CURRENCY_SET_GAMBLE_MAX_GUESS, 52 | CURRENCY_SET_PRIZE_PROB, 53 | CURRENCY_SET_PRIZE_EXPIRY, 54 | CURRENCY_RICH_LIST, 55 | CURRENCY_DONATE, 56 | CURRENCY_STEAL, 57 | CURRENCY_SET_STEAL_SUCCESS_CHANCE, 58 | CURRENCY_SET_STEAL_FINE_PERCENT, 59 | CURRENCY_SET_STEAL_VICTIM_BONUS, 60 | CURRENCY_SET_STEAL_JAIL_HOURS, 61 | CURRENCY_SET_DAILY_BONUS_AMOUNT, 62 | CURRENCY_SET_DAILY_BONUS_PERIOD, 63 | CURRENCY_COMPENSATE, 64 | CURRENCY_BRIBE, 65 | CURRENCY_SET_BRIBE_SUCCESS_CHANCE, 66 | CURRENCY_SET_MAX_BRIBE_AMOUNT, 67 | CURRENCY_SET_LEAST_BRIBE_AMOUNT, 68 | 69 | EVENT_LAUNCH, 70 | EVENT_END, 71 | EVENT, 72 | EVENT_GIVE_NEW_ACCESS, 73 | EVENT_TAKE_NEW_ACCESS, 74 | EVENT_SET_HRHR_STEAL_SUCCESS_CHANCE, 75 | EVENT_SET_LRLR_STEAL_SUCCESS_CHANCE, 76 | EVENT_GET_REWARD, 77 | EVENT_BUY_TICKETS, 78 | EVENT_TICKET, 79 | EVENT_SET_TICKET_PRICE, 80 | EVENT_SET_USER_MAX_TICKET, 81 | 82 | FUN_MEME, 83 | FUN_ROLL, 84 | FUN_POLL, 85 | FUN_GITHUB, 86 | FUN_GIVE_NEW_POLL_ACCESS, 87 | FUN_TAKE_NEW_POLL_ACCESS 88 | }; 89 | } 90 | 91 | struct UserData { 92 | QString nickname; 93 | QString username; 94 | }; 95 | 96 | struct GuildData { 97 | QMap userdata; 98 | QList roles; 99 | snowflake_t ownerId; 100 | }; 101 | 102 | struct CommandInfo { 103 | QString briefDescription; 104 | QString usage; 105 | QString additionalInfo; 106 | bool adminPermission; 107 | }; 108 | 109 | class UmikoBot : public Discord::Client 110 | { 111 | public: 112 | static UmikoBot& Instance(); 113 | UmikoBot(const UmikoBot&) = delete; 114 | void operator=(const UmikoBot&) = delete; 115 | ~UmikoBot(); 116 | 117 | QString GetNick(snowflake_t guild, snowflake_t user); 118 | QString GetUsername(snowflake_t guild, snowflake_t user); 119 | QString GetName(snowflake_t guild, snowflake_t user); 120 | Discord::Promise& GetAvatar(snowflake_t guild, snowflake_t user); 121 | 122 | snowflake_t GetUserFromArg(snowflake_t guild, QStringList args, int startIndex); 123 | Module* GetModuleByName(const QString& name); 124 | 125 | const QList& GetRoles(snowflake_t guild); 126 | bool IsOwner(snowflake_t guild, snowflake_t user); 127 | 128 | QString GetCommandHelp(QString commandName, QString prefix); 129 | QList GetAllCommands(); 130 | 131 | static void VerifyAndRunAdminCmd(Discord::Client& client, const Discord::Message& message, const Discord::Channel& channel, unsigned int requiredNumberOfArgs, const QStringList& args, bool argumentShouldBeANumber, std::function callback); 132 | 133 | private slots: 134 | void OnDisconnected(); 135 | 136 | private: 137 | UmikoBot(QObject* parent = nullptr); 138 | 139 | void Save(); 140 | void Load(); 141 | void GetGuilds(snowflake_t after = 0); 142 | void GetGuildMemberInformation(snowflake_t guild, snowflake_t after = 0); 143 | 144 | QList m_modules; 145 | QTimer m_timer; 146 | 147 | QMap m_guildDatas; 148 | QList m_commands; 149 | QMap m_commandsInfo; 150 | }; 151 | -------------------------------------------------------------------------------- /src/modules/PollSettings.cpp: -------------------------------------------------------------------------------- 1 | #include "FunModule.h" 2 | 3 | #define EMBED_BAR_MAX_WIDTH 15.0 4 | 5 | using namespace Discord; 6 | 7 | FunModule::PollSettings::PollSettings(const PollOptions& op, long long maxvotes, snowflake_t chan, const QString& name, int num, double time, const ServerPolls& pollList, snowflake_t msg) 8 | : options(op), maxVotes(maxvotes), notifChannel(chan), 9 | pollName(name), pollNum(num), polls(pollList), pollMsg(msg) 10 | { 11 | timer = std::make_shared(); 12 | timer->setInterval(time * 1000); 13 | timer->setSingleShot(true); 14 | timer->start(); 15 | 16 | QObject::connect(timer.get(), &QTimer::timeout, 17 | [this]() 18 | { 19 | auto poll_num = this->pollNum; 20 | auto serverPolls = this->polls; 21 | //! Create a message with the results 22 | auto settings = serverPolls->at(this->pollNum); 23 | auto poll_name = settings->pollName; 24 | auto notif_chan = settings->notifChannel; 25 | auto poll_msg = settings->pollMsg; 26 | 27 | std::size_t total = 0; 28 | for (auto& option : settings->options) 29 | { 30 | total += option.count; 31 | } 32 | 33 | if (total == 0) total = 1; 34 | 35 | QList fields; 36 | 37 | for (auto& option : settings->options) 38 | { 39 | double val = static_cast(option.count) / static_cast(total) * EMBED_BAR_MAX_WIDTH; 40 | double percentage = static_cast(option.count) / static_cast(total) * 100.0; 41 | int num = qFloor(val); 42 | 43 | QString str{ utility::consts::ZERO_WIDTH_SPACE }; 44 | 45 | for (int i = 0; i < num; i++) 46 | { 47 | str += utility::consts::emojis::GREEN_BLOCK; 48 | } 49 | 50 | for (int i = 0; i < EMBED_BAR_MAX_WIDTH - num; i++) 51 | { 52 | str += utility::consts::emojis::BLACK_BLOCK; 53 | } 54 | 55 | EmbedField field; 56 | QRegExp reg{ ".+:\\d+" }; 57 | 58 | QString desc = ""; 59 | QString emoji = option.emote; 60 | 61 | if (option.desc != "") 62 | { 63 | desc = " (" + option.desc + ") "; 64 | } 65 | 66 | if (reg.exactMatch(option.emote)) 67 | { 68 | if (option.isAnimated) 69 | { 70 | emoji.prepend(""); 74 | } 75 | field.setName(emoji + desc + ": " + QString::number(percentage) + "%"); 76 | field.setValue(str); 77 | 78 | fields.push_back(field); 79 | } 80 | 81 | Embed embed; 82 | embed.setColor(qrand() % 16777216); 83 | embed.setTitle("Results for Poll#" + QString::number(poll_num) + " " + poll_name); 84 | embed.setFields(fields); 85 | 86 | 87 | UmikoBot::Instance().createMessage(notif_chan, embed).then([poll_num, serverPolls](const Message& message) 88 | { 89 | 90 | auto settings = serverPolls->at(poll_num); 91 | auto poll_name = settings->pollName; 92 | auto notif_chan = settings->notifChannel; 93 | auto poll_msg = settings->pollMsg; 94 | 95 | //! Get the id of the resulting message 96 | 97 | auto resultMsg = message.id(); 98 | 99 | //! This part is used to get the guild id 100 | 101 | UmikoBot::Instance().getChannel(notif_chan).then([poll_num, resultMsg, serverPolls](const Channel& chan) 102 | { 103 | 104 | auto settings = serverPolls->at(poll_num); 105 | auto poll_name = settings->pollName; 106 | auto notif_chan = settings->notifChannel; 107 | auto poll_msg = settings->pollMsg; 108 | 109 | MessagePatch patch; 110 | 111 | Embed embed; 112 | embed.setColor(qrand() % 16777216); 113 | embed.setTitle("Poll#" + QString::number(poll_num) + " " + poll_name); 114 | embed.setDescription("This poll has ended. See the results [here.](https://discordapp.com/channels/" 115 | + QString::number(chan.guildId()) + "/" 116 | + QString::number(notif_chan) + "/" 117 | + QString::number(resultMsg) + ")"); 118 | 119 | patch.setEmbed(embed); 120 | UmikoBot::Instance().editMessage(notif_chan, poll_msg, patch); 121 | UmikoBot::Instance().deleteAllReactions(chan.id(), poll_msg); 122 | 123 | //! Remove the poll from our pending list 124 | bool removedOne = false; 125 | for (int i = 0; i < serverPolls->size(); i++) 126 | { 127 | if (serverPolls->at(i)->pollNum == poll_num) 128 | { 129 | serverPolls->removeAt(i); 130 | removedOne = true; 131 | if (serverPolls->empty()) break; 132 | } 133 | if (removedOne) 134 | { 135 | serverPolls->at(i)->pollNum--; 136 | } 137 | } 138 | 139 | }); 140 | }); 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PREMAKE_VERSION="5.0.0-alpha12" 4 | os=$(uname -s) 5 | machine=$(uname -m) 6 | 7 | # Linux system 8 | if [ "$os" == "Linux" ]; then 9 | os="linux" 10 | 11 | # Max OS X system 12 | elif [ "$os" == "Darwin" ]; then 13 | os="osx" 14 | 15 | # Assume Windows 16 | else 17 | os="windows" 18 | fi 19 | 20 | 21 | # Generate directories 22 | $(mkdir -p "tmp" "bin" "bin/x86" "bin/x64") 23 | 24 | 25 | # Windows setup 26 | if [ "$os" == "windows" ]; then 27 | 28 | # Qt (x86) directory 29 | qtdir_x86="" 30 | if [ true ]; then 31 | echo -n "Enter your local Qt (x86) directory and press ENTER: " 32 | read -e qtdir_x86 33 | echo -n $qtdir_x86 >"tmp/.qtdir_x86" 34 | fi 35 | 36 | # Qt (x64) directory 37 | qtdir_x64="" 38 | if [ "$machine" == "x86_64" ]; then 39 | echo -n "Enter your local Qt (x64) directory and press ENTER: " 40 | read -e qtdir_x64 41 | echo -n $qtdir_x64 >"tmp/.qtdir_x64" 42 | fi 43 | 44 | # OpenSSL (x86) directory 45 | ssldir_x86="" 46 | if [ true ]; then 47 | echo -n "Enter your local OpenSSL (x86) directory and press ENTER: " 48 | read -e ssldir_x86 49 | echo -n $ssldir_x86 >"tmp/.ssldir_x86" 50 | fi 51 | 52 | # OpenSSL (x64) directory 53 | ssldir_x64="" 54 | if [ "$machine" == "x86_64" ]; then 55 | echo -n "Enter your local OpenSSL (x64) directory and press ENTER: " 56 | read -e ssldir_x64 57 | echo -n $ssldir_x64 >"tmp/.ssldir_x64" 58 | fi 59 | 60 | 61 | # Copy Qt (x86) DLLs 62 | if [ -n "$qtdir_x86" ]; then 63 | $(cp "$qtdir_x86/plugins/platforms/qwindows.dll" "bin/x86/") 64 | $(cp "$qtdir_x86/plugins/platforms/qwindowsd.dll" "bin/x86/") 65 | $(cp "$qtdir_x86/bin/Qt5Core.dll" "bin/x86/") 66 | $(cp "$qtdir_x86/bin/Qt5Cored.dll" "bin/x86/") 67 | $(cp "$qtdir_x86/bin/Qt5Gui.dll" "bin/x86/") 68 | $(cp "$qtdir_x86/bin/Qt5Guid.dll" "bin/x86/") 69 | $(cp "$qtdir_x86/bin/Qt5Network.dll" "bin/x86/") 70 | $(cp "$qtdir_x86/bin/Qt5Networkd.dll" "bin/x86/") 71 | $(cp "$qtdir_x86/bin/Qt5WebSockets.dll" "bin/x86/") 72 | $(cp "$qtdir_x86/bin/Qt5WebSocketsd.dll" "bin/x86/") 73 | $(cp "$qtdir_x86/bin/Qt5Widgets.dll" "bin/x86/") 74 | $(cp "$qtdir_x86/bin/Qt5Widgetsd.dll" "bin/x86/") 75 | fi 76 | 77 | # Copy Qt (x64) DLLs 78 | if [ -n "$qtdir_x64" ]; then 79 | $(cp "$qtdir_x64/plugins/platforms/qwindows.dll" "bin/x64/") 80 | $(cp "$qtdir_x64/plugins/platforms/qwindowsd.dll" "bin/x64/") 81 | $(cp "$qtdir_x64/bin/Qt5Core.dll" "bin/x64/") 82 | $(cp "$qtdir_x64/bin/Qt5Cored.dll" "bin/x64/") 83 | $(cp "$qtdir_x64/bin/Qt5Gui.dll" "bin/x64/") 84 | $(cp "$qtdir_x64/bin/Qt5Guid.dll" "bin/x64/") 85 | $(cp "$qtdir_x64/bin/Qt5Network.dll" "bin/x64/") 86 | $(cp "$qtdir_x64/bin/Qt5Networkd.dll" "bin/x64/") 87 | $(cp "$qtdir_x64/bin/Qt5WebSockets.dll" "bin/x64/") 88 | $(cp "$qtdir_x64/bin/Qt5WebSocketsd.dll" "bin/x64/") 89 | $(cp "$qtdir_x64/bin/Qt5Widgets.dll" "bin/x64/") 90 | $(cp "$qtdir_x64/bin/Qt5Widgetsd.dll" "bin/x64/") 91 | fi 92 | 93 | # Copy OpenSSL (x86) DLLs 94 | if [ -n "$ssldir_x86" ]; then 95 | $(cp "$ssldir_x86/bin/libeay32.dll" "bin/x86/") 96 | $(cp "$ssldir_x86/bin/ssleay32.dll" "bin/x86/") 97 | fi 98 | 99 | # Copy OpenSSL (x64) DLLs 100 | if [ -n "$ssldir_x64" ]; then 101 | $(cp "$ssldir_x64/bin/libeay32.dll" "bin/x64/") 102 | $(cp "$ssldir_x64/bin/ssleay32.dll" "bin/x64/") 103 | fi 104 | 105 | # Download premake executable 106 | $(curl -L -o "tmp/premake5.zip" "https://github.com/premake/premake-core/releases/download/v$PREMAKE_VERSION/premake-$PREMAKE_VERSION-windows.zip") 107 | $(unzip -u -q "tmp/premake5.zip" -d "pmk") 108 | 109 | # Linux setup 110 | elif [ "$os" == "linux" ]; then 111 | # Determine whether we need to build from source or not 112 | if [ "$machine" == "x86_64" ]; then 113 | # Download premake executable 114 | $(curl -L -o "tmp/premake5.tar.gz" "https://github.com/premake/premake-core/releases/download/v$PREMAKE_VERSION/premake-$PREMAKE_VERSION-linux.tar.gz") 115 | $(tar -xvzf "tmp/premake5.tar.gz" -C "pmk") 116 | else 117 | # Download premake source package 118 | $(curl -L -o "tmp/premake5-src.zip" "https://github.com/premake/premake-core/releases/download/v$PREMAKE_VERSION/premake-$PREMAKE_VERSION-src.zip") 119 | $(unzip -o "tmp/premake5-src.zip" -d "tmp") 120 | 121 | # Build premake 122 | echo "Building premake from source.." 123 | $(make -C "tmp/premake-$PREMAKE_VERSION/build/gmake.unix/") 124 | $(cp "tmp/premake-$PREMAKE_VERSION/bin/release/premake5" "pmk/") 125 | fi 126 | 127 | # Mac OS X setup 128 | elif [ "$os" == "osx" ]; then 129 | # Download premake executable 130 | $(curl -L -o "tmp/premake5.tar.gz" "https://github.com/premake/premake-core/releases/download/v$PREMAKE_VERSION/premake-$PREMAKE_VERSION-macosx.tar.gz") 131 | $(tar -xvzf "tmp/premake5.tar.gz" -C "pmk") 132 | fi 133 | 134 | 135 | # Init or update submodules 136 | git submodule update --init --recursive 137 | -------------------------------------------------------------------------------- /src/modules/CurrencyModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "core/Module.h" 4 | #include "UmikoBot.h" 5 | #include 6 | #include "core/Currency.h" 7 | 8 | class UmikoBot; 9 | 10 | class CurrencyModule : public Module 11 | { 12 | friend class EventModule; 13 | public: 14 | struct UserCurrency 15 | { 16 | private: 17 | Currency m_Currency; 18 | 19 | public: 20 | snowflake_t userId; 21 | bool isDailyClaimed; 22 | bool isBribeUsed; 23 | Currency maxCurrency; 24 | unsigned int dailyStreak; 25 | QTimer* jailTimer; 26 | unsigned int numberOfDailysClaimed; 27 | unsigned int numberOfGiveawaysClaimed; 28 | bool hasClaimedCurrentGift; 29 | 30 | UserCurrency(snowflake_t userId, Currency currency, Currency maxCurrency, bool isDailyClaimed, unsigned int dailyStreak, unsigned int numberOfDailysClaimed, unsigned int numberOfGiveawaysClaimed, bool hasClaimedCurrentGift) 31 | : userId(userId), maxCurrency(maxCurrency), isDailyClaimed(isDailyClaimed), isBribeUsed(false), dailyStreak(dailyStreak), jailTimer(new QTimer()), numberOfDailysClaimed(numberOfDailysClaimed), numberOfGiveawaysClaimed(numberOfGiveawaysClaimed), hasClaimedCurrentGift(hasClaimedCurrentGift) 32 | { 33 | setCurrency(currency); 34 | jailTimer->setSingleShot(true); 35 | } 36 | 37 | ~UserCurrency() 38 | { 39 | delete jailTimer; 40 | } 41 | 42 | UserCurrency(const UserCurrency& other) 43 | : userId(other.userId), maxCurrency(other.maxCurrency), isDailyClaimed(other.isDailyClaimed), isBribeUsed(other.isBribeUsed), dailyStreak(other.dailyStreak), jailTimer(new QTimer()), numberOfDailysClaimed(other.numberOfDailysClaimed), numberOfGiveawaysClaimed(other.numberOfGiveawaysClaimed), hasClaimedCurrentGift(other.hasClaimedCurrentGift) 44 | { 45 | setCurrency(other.currency()); 46 | jailTimer->setSingleShot(true); 47 | 48 | if (other.jailTimer->remainingTime() > 0) 49 | { 50 | jailTimer->start(other.jailTimer->remainingTime()); 51 | } 52 | } 53 | 54 | UserCurrency& operator=(const UserCurrency& other) 55 | { 56 | userId = other.userId; 57 | maxCurrency = other.maxCurrency; 58 | setCurrency(other.currency()); 59 | isDailyClaimed = other.isDailyClaimed; 60 | isBribeUsed = other.isBribeUsed; 61 | dailyStreak = other.dailyStreak; 62 | numberOfDailysClaimed = other.numberOfDailysClaimed; 63 | numberOfGiveawaysClaimed = other.numberOfGiveawaysClaimed; 64 | hasClaimedCurrentGift = other.hasClaimedCurrentGift; 65 | 66 | jailTimer = new QTimer(); 67 | jailTimer->setSingleShot(true); 68 | 69 | if (other.jailTimer->remainingTime() > 0) 70 | { 71 | jailTimer->start(other.jailTimer->remainingTime()); 72 | } 73 | 74 | return *this; 75 | } 76 | 77 | const Currency& currency() const 78 | { 79 | return m_Currency; 80 | } 81 | 82 | void setCurrency(const Currency& value) 83 | { 84 | m_Currency = value; 85 | 86 | if (maxCurrency < currency()) 87 | maxCurrency = currency(); 88 | } 89 | }; 90 | 91 | struct CurrencyConfig 92 | { 93 | double randGiveawayProb { 0.001 }; 94 | unsigned int freebieExpireTime { 60 }; //in seconds 95 | Currency dailyReward { 100 }; 96 | Currency freebieReward { 300 }; 97 | Currency gambleReward { 50 }; 98 | int minGuess { 0 }; 99 | int maxGuess { 5 }; 100 | Currency gambleLoss { 10 }; 101 | snowflake_t giveawayChannelId { 0 }; 102 | QString currencyName; 103 | QString currencySymbol; 104 | bool isRandomGiveawayDone{ false }; 105 | bool allowGiveaway{ false }; 106 | snowflake_t giveawayClaimer { 0 }; 107 | QTimer* freebieTimer{ nullptr }; 108 | Currency dailyBonusAmount { 50 }; 109 | int dailyBonusPeriod { 3 }; 110 | int stealSuccessChance { 30 }; 111 | int stealFinePercent { 50 }; 112 | int stealVictimBonusPercent { 25 }; 113 | int stealFailedJailTime { 3 }; 114 | Currency bribeMaxAmount { 150 }; 115 | Currency bribeLeastAmount { 20 }; 116 | int bribeSuccessChance { 68 }; 117 | 118 | int lowRiskRewardStealSuccessChance{ 50 }; 119 | int highRiskRewardStealSuccessChance{ 30 }; 120 | }; 121 | 122 | private: 123 | //! Map server id with user currency list 124 | QMap> guildList; 125 | 126 | std::random_device random_device; 127 | std::mt19937 random_engine{ random_device() }; 128 | std::bernoulli_distribution randp; 129 | 130 | QTimer m_timer; //! For dailies, and the giveaway 131 | 132 | QMapserverCurrencyConfig; 133 | 134 | struct GambleData { 135 | bool gamble{ false }; 136 | bool doubleOrNothing{ false }; 137 | snowflake_t userId{ 0 }; 138 | int randNum = 0; 139 | snowflake_t channelId{ 0 }; 140 | Currency betAmount{ 0 }; //!Use if doubleOrNothing 141 | QTimer* timer; 142 | }; 143 | 144 | //! Map each !gamble (on a server) with its own gamble 145 | QMap gambleData; 146 | 147 | UmikoBot* m_client; 148 | 149 | // Holiday special stuff 150 | QTimer holidaySpecialCheckTimer; // Checks if the current day is special 151 | QTimer holidaySpecialTimer; // Either stores time left for people to claim or time until next gift event 152 | bool isHolidaySpecialActive = false; // Whether today is a special day 153 | bool isHolidaySpecialClaimable = false; 154 | 155 | private: 156 | void OnSave(QJsonDocument& doc) const override; 157 | void OnLoad(const QJsonDocument& doc) override; 158 | 159 | snowflake_t getUserIndex(snowflake_t guild, snowflake_t id) { 160 | 161 | for (auto it = guildList[guild].begin(); it != guildList[guild].end(); ++it) { 162 | if (it->userId == id) { 163 | return std::distance(guildList[guild].begin(), it); 164 | } 165 | } 166 | //! If user is not added to the system, make a new one 167 | guildList[guild].append(UserCurrency{ id, 0, 0, false, 0, 0, 0, false }); 168 | return std::distance(guildList[guild].begin(), std::prev(guildList[guild].end())); 169 | } 170 | 171 | public: 172 | CurrencyModule(UmikoBot* client); 173 | void StatusCommand(QString& result, snowflake_t guild, snowflake_t user) override; 174 | void OnMessage(Discord::Client& client, const Discord::Message& message) override; 175 | 176 | UserCurrency& getUserData(snowflake_t guild, snowflake_t id) 177 | { 178 | for (auto& user : guildList[guild]) 179 | { 180 | if (user.userId == id) 181 | { 182 | return user; 183 | } 184 | } 185 | 186 | //! If user is not added to the system, make a new one 187 | UserCurrency user{ id, 0, 0, false, 0, 0, 0, false }; 188 | 189 | guildList[guild].append(user); 190 | return guildList[guild].back(); 191 | } 192 | 193 | CurrencyConfig& getServerData(snowflake_t guild) 194 | { 195 | return serverCurrencyConfig[guild]; 196 | } 197 | }; -------------------------------------------------------------------------------- /src/core/Utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace utility 11 | { 12 | 13 | namespace consts 14 | { 15 | constexpr QChar ZERO_WIDTH_SPACE = QChar(0x200B); 16 | namespace emojis 17 | { 18 | namespace reacts 19 | { 20 | constexpr auto ANGRY_PING = "anrgyping:777298312021672038"; 21 | constexpr auto PARTY_CAT = "partyCat:777298761318793217"; 22 | 23 | constexpr auto ARROW_FORWARD = u8"▶️"; 24 | constexpr auto ARROW_BACKWARD = u8"◀️"; 25 | constexpr auto X_CANCEL = u8"❌"; 26 | 27 | constexpr auto REGIONAL_INDICATOR_A = u8"🇦"; 28 | constexpr auto REGIONAL_INDICATOR_B = u8"🇧"; 29 | constexpr auto REGIONAL_INDICATOR_C = u8"🇨"; 30 | constexpr auto REGIONAL_INDICATOR_D = u8"🇩"; 31 | constexpr auto REGIONAL_INDICATOR_E = u8"🇪"; 32 | constexpr auto REGIONAL_INDICATOR_F = u8"🇫"; 33 | constexpr auto REGIONAL_INDICATOR_G = u8"🇬"; 34 | constexpr auto REGIONAL_INDICATOR_H = u8"🇭"; 35 | constexpr auto REGIONAL_INDICATOR_I = u8"🇮"; 36 | constexpr auto REGIONAL_INDICATOR_J = u8"🇯"; 37 | constexpr auto REGIONAL_INDICATOR_K = u8"🇰"; 38 | constexpr auto REGIONAL_INDICATOR_L = u8"🇱"; 39 | constexpr auto REGIONAL_INDICATOR_M = u8"🇲"; 40 | constexpr auto REGIONAL_INDICATOR_N = u8"🇳"; 41 | constexpr auto REGIONAL_INDICATOR_O = u8"🇴"; 42 | constexpr auto REGIONAL_INDICATOR_P = u8"🇵"; 43 | constexpr auto REGIONAL_INDICATOR_Q = u8"🇶"; 44 | constexpr auto REGIONAL_INDICATOR_R = u8"🇷"; 45 | constexpr auto REGIONAL_INDICATOR_S = u8"🇸"; 46 | constexpr auto REGIONAL_INDICATOR_T = u8"🇹"; 47 | constexpr auto REGIONAL_INDICATOR_U = u8"🇺"; 48 | constexpr auto REGIONAL_INDICATOR_V = u8"🇻"; 49 | constexpr auto REGIONAL_INDICATOR_W = u8"🇼"; 50 | constexpr auto REGIONAL_INDICATOR_X = u8"🇽"; 51 | constexpr auto REGIONAL_INDICATOR_Y = u8"🇾"; 52 | constexpr auto REGIONAL_INDICATOR_Z = u8"🇿"; 53 | constexpr std::array REGIONAL_INDICATORS = 54 | { 55 | REGIONAL_INDICATOR_A, 56 | REGIONAL_INDICATOR_B, 57 | REGIONAL_INDICATOR_C, 58 | REGIONAL_INDICATOR_D, 59 | REGIONAL_INDICATOR_E, 60 | REGIONAL_INDICATOR_F, 61 | REGIONAL_INDICATOR_G, 62 | REGIONAL_INDICATOR_H, 63 | REGIONAL_INDICATOR_I, 64 | REGIONAL_INDICATOR_J, 65 | REGIONAL_INDICATOR_K, 66 | REGIONAL_INDICATOR_L, 67 | REGIONAL_INDICATOR_M, 68 | REGIONAL_INDICATOR_N, 69 | REGIONAL_INDICATOR_O, 70 | REGIONAL_INDICATOR_P, 71 | REGIONAL_INDICATOR_Q, 72 | REGIONAL_INDICATOR_R, 73 | REGIONAL_INDICATOR_S, 74 | REGIONAL_INDICATOR_T, 75 | REGIONAL_INDICATOR_U, 76 | REGIONAL_INDICATOR_V, 77 | REGIONAL_INDICATOR_W, 78 | REGIONAL_INDICATOR_X, 79 | REGIONAL_INDICATOR_Y, 80 | REGIONAL_INDICATOR_Z, 81 | }; 82 | } 83 | 84 | constexpr auto TICKETS = ":tickets:"; 85 | 86 | constexpr auto REGIONAL_INDICATOR_A = ":regional_indicator_a:"; 87 | constexpr auto REGIONAL_INDICATOR_B = ":regional_indicator_b:"; 88 | constexpr auto REGIONAL_INDICATOR_C = ":regional_indicator_c:"; 89 | constexpr auto REGIONAL_INDICATOR_D = ":regional_indicator_d:"; 90 | constexpr auto REGIONAL_INDICATOR_E = ":regional_indicator_e:"; 91 | constexpr auto REGIONAL_INDICATOR_F = ":regional_indicator_f:"; 92 | constexpr auto REGIONAL_INDICATOR_G = ":regional_indicator_g:"; 93 | constexpr auto REGIONAL_INDICATOR_H = ":regional_indicator_h:"; 94 | constexpr auto REGIONAL_INDICATOR_I = ":regional_indicator_i:"; 95 | constexpr auto REGIONAL_INDICATOR_J = ":regional_indicator_j:"; 96 | constexpr auto REGIONAL_INDICATOR_K = ":regional_indicator_k:"; 97 | constexpr auto REGIONAL_INDICATOR_L = ":regional_indicator_l:"; 98 | constexpr auto REGIONAL_INDICATOR_M = ":regional_indicator_m:"; 99 | constexpr auto REGIONAL_INDICATOR_N = ":regional_indicator_n:"; 100 | constexpr auto REGIONAL_INDICATOR_O = ":regional_indicator_o:"; 101 | constexpr auto REGIONAL_INDICATOR_P = ":regional_indicator_p:"; 102 | constexpr auto REGIONAL_INDICATOR_Q = ":regional_indicator_q:"; 103 | constexpr auto REGIONAL_INDICATOR_R = ":regional_indicator_r:"; 104 | constexpr auto REGIONAL_INDICATOR_S = ":regional_indicator_s:"; 105 | constexpr auto REGIONAL_INDICATOR_T = ":regional_indicator_t:"; 106 | constexpr auto REGIONAL_INDICATOR_U = ":regional_indicator_u:"; 107 | constexpr auto REGIONAL_INDICATOR_V = ":regional_indicator_v:"; 108 | constexpr auto REGIONAL_INDICATOR_W = ":regional_indicator_w:"; 109 | constexpr auto REGIONAL_INDICATOR_X = ":regional_indicator_x:"; 110 | constexpr auto REGIONAL_INDICATOR_Y = ":regional_indicator_y:"; 111 | constexpr auto REGIONAL_INDICATOR_Z = ":regional_indicator_z:"; 112 | 113 | constexpr auto GREEN_BLOCK = "<:green_block:777298339858612255>"; 114 | constexpr auto BLACK_BLOCK = "<:black_block:777298360267833345>"; 115 | constexpr auto WE_SMART = "<:wesmart:777299810658680892>"; 116 | constexpr auto AANGER = "<:aanger:777298666485841921>"; 117 | constexpr auto SHOPPING_BAGS = ":shopping_bags:"; 118 | 119 | constexpr auto GIFT = ":gift:"; 120 | constexpr auto GIFT_HEART = ":gift_heart:"; 121 | } 122 | } 123 | 124 | enum class StringMSFormat 125 | { 126 | DESCRIPTIVE, 127 | DESCRIPTIVE_COMMA, 128 | MINIMAL, 129 | MINIMAL_COMMA 130 | }; 131 | 132 | inline QString StringifyMilliseconds(qint64 milliseconds, StringMSFormat fmt = StringMSFormat::DESCRIPTIVE_COMMA) 133 | { 134 | QDateTime conv; 135 | QDateTime start; 136 | start.setOffsetFromUtc(0); 137 | start.setMSecsSinceEpoch(0); 138 | 139 | conv.setOffsetFromUtc(0); 140 | conv.setMSecsSinceEpoch(milliseconds); 141 | 142 | QStringList values; 143 | 144 | #define get(x) QString::number(conv.toString(#x).toInt() - start.toString(#x).toInt()) 145 | values.push_back(get(yyyy)); //! years 146 | values.push_back(get(M)); //! months 147 | values.push_back(get(d)); //! days 148 | values.push_back(get(h)); //! hours 149 | values.push_back(get(m)); //! mins 150 | values.push_back(get(s)); //! seconds 151 | #undef get 152 | 153 | const QStringList descriptiveUnits = {"years", "months", "days", "hrs", "mins", "secs"}; 154 | const QStringList minimalUnits = { "y", "M", "d", "h", "m", "s" }; 155 | QString sep = " "; 156 | 157 | switch (fmt) 158 | { 159 | case StringMSFormat::DESCRIPTIVE_COMMA: 160 | case StringMSFormat::MINIMAL_COMMA: 161 | sep = ", "; 162 | } 163 | 164 | int off = 0; 165 | 166 | for (const auto& value : values) 167 | { 168 | if (value != "0") break; 169 | off++; 170 | } 171 | 172 | QString time; 173 | 174 | switch (fmt) 175 | { 176 | case StringMSFormat::MINIMAL: 177 | case StringMSFormat::MINIMAL_COMMA: 178 | for (int i = off; i < values.size(); i++) 179 | { 180 | time += values.at(i) + minimalUnits.at(i) + ((i == values.size()-1) ? "" : sep); 181 | } 182 | break; 183 | case StringMSFormat::DESCRIPTIVE: 184 | case StringMSFormat::DESCRIPTIVE_COMMA: 185 | for (int i = off; i < values.size(); i++) 186 | { 187 | time += values.at(i) + descriptiveUnits.at(i) + ((i == values.size() - 1) ? "" : sep); 188 | } 189 | break; 190 | } 191 | 192 | return time; 193 | } 194 | 195 | inline QString stringifyEmoji(const Discord::Emoji& emoji) 196 | { 197 | QString str; 198 | str = emoji.name(); 199 | str += (emoji.id() == 0) ? "" : ":" + QString::number(emoji.id()); 200 | return str; 201 | } 202 | 203 | inline QString spaces(int length) 204 | { 205 | QString txt; 206 | for (; length; length--) txt += " "; 207 | return txt; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/core/GuildSettings.cpp: -------------------------------------------------------------------------------- 1 | #include "GuildSettings.h" 2 | #include "modules/LevelModule.h" 3 | 4 | #include "Logger.h" 5 | 6 | QList GuildSettings::s_settings; 7 | QString GuildSettings::s_location; 8 | 9 | void GuildSettings::Load(const QString& location) 10 | { 11 | s_location = "configs/" + location; 12 | 13 | ULog(ulog::Severity::Debug, UFString("Configuration file found, loading from: %s", qPrintable(s_location))); 14 | 15 | QFile file(s_location); 16 | if (file.open(QIODevice::ReadOnly)) 17 | { 18 | QByteArray data = file.readAll(); 19 | 20 | QJsonDocument doc(QJsonDocument::fromJson(data)); 21 | QJsonObject json = doc.object(); 22 | QStringList guildIds = json.keys(); 23 | 24 | for (const QString& id : guildIds) 25 | { 26 | QJsonObject current = json.value(id).toObject(); 27 | GuildSetting setting = CreateGuildSetting(id.toULongLong()); 28 | 29 | if (current.contains("prefix")) 30 | { 31 | setting.prefix = current["prefix"].toString(); 32 | } 33 | 34 | if (current.contains("primaryChannel")) 35 | { 36 | setting.primaryChannel = current["primaryChannel"].toString().toULongLong(); 37 | } 38 | 39 | if (current.contains("modules")) 40 | { 41 | QJsonObject moduleJson = current["modules"].toObject(); 42 | QStringList modules = moduleJson.keys(); 43 | 44 | for (const QString& moduleName : modules) 45 | { 46 | setting.modules.push_back({ moduleName, moduleJson[moduleName].toBool() }); 47 | } 48 | } 49 | 50 | if (current.contains("levelModule")) 51 | { 52 | QJsonObject levelModuleJson = current["levelModule"].toObject(); 53 | if (levelModuleJson.contains("maximumLevel")) 54 | setting.maximumLevel = levelModuleJson["maximumLevel"].toString().toUInt(); 55 | 56 | if (levelModuleJson.contains("growthRate")) 57 | setting.growthRate = levelModuleJson["growthRate"].toString().toFloat(); 58 | 59 | if (levelModuleJson.contains("expRequirement")) 60 | setting.expRequirement = levelModuleJson["expRequirement"].toString().toUInt(); 61 | 62 | QJsonObject ranksJson = levelModuleJson["ranks"].toObject(); 63 | QStringList ranks = ranksJson.keys(); 64 | for (const QString& rankName : ranks) 65 | setting.ranks.push_back({ rankName, ranksJson[rankName].toString().toUInt() }); 66 | 67 | qSort(setting.ranks.begin(), setting.ranks.end(), 68 | [](const LevelRank& v1, const LevelRank& v2) -> bool 69 | { 70 | return v1.minimumLevel < v2.minimumLevel; 71 | }); 72 | } 73 | 74 | if (current.contains("behaviourModule")) 75 | { 76 | QJsonObject behaviourModuleJson = current["behaviourModule"].toObject(); 77 | 78 | if (behaviourModuleJson.contains("levelBehaviourChannels")) 79 | { 80 | QJsonObject levelBehaviourChannelsJson = behaviourModuleJson["levelBehaviourChannels"].toObject(); 81 | QStringList levelBehaviourChannels = levelBehaviourChannelsJson.keys(); 82 | for (const QString& channel : levelBehaviourChannels) 83 | if (levelBehaviourChannelsJson[channel].toBool() == true) 84 | setting.levelWhitelistedChannels.push_back(channel.toULongLong()); 85 | 86 | else 87 | setting.levelBlacklistedChannels.push_back(channel.toULongLong()); 88 | } 89 | 90 | if (behaviourModuleJson.contains("outputBehaviourChannels")) 91 | { 92 | QJsonObject outputBehaviourChannelsJson = behaviourModuleJson["outputBehaviourChannels"].toObject(); 93 | QStringList outputBehaviourChannels = outputBehaviourChannelsJson.keys(); 94 | for (const QString& channel : outputBehaviourChannels) 95 | if (outputBehaviourChannelsJson[channel].toBool() == true) 96 | setting.outputWhitelistedChannels.push_back(channel.toULongLong()); 97 | else 98 | setting.outputBlacklistedChannels.push_back(channel.toULongLong()); 99 | } 100 | } 101 | 102 | s_settings.push_back(setting); 103 | } 104 | 105 | file.close(); 106 | } 107 | } 108 | 109 | void GuildSettings::Save() 110 | { 111 | QFile file(s_location); 112 | file.open(QIODevice::WriteOnly); 113 | 114 | QJsonObject json; 115 | 116 | for (const GuildSetting& setting : s_settings) 117 | { 118 | QJsonObject current; 119 | if(setting.prefix != "!") 120 | current["prefix"] = setting.prefix; 121 | 122 | if (setting.primaryChannel != 0) 123 | current["primaryChannel"] = QString::number(setting.primaryChannel); 124 | 125 | if (setting.modules.size() > 0) 126 | { 127 | QJsonObject moduleSettings; 128 | bool hasModuleSettings = false; 129 | for (const QPair& module : setting.modules) 130 | moduleSettings[module.first] = module.second; 131 | 132 | current["modules"] = moduleSettings; 133 | } 134 | 135 | bool levelModuleDefault = true; 136 | QJsonObject levelModule; 137 | QJsonObject ranks; 138 | if (setting.ranks.size() > 0) 139 | { 140 | levelModuleDefault = false; 141 | 142 | for (const LevelRank& rank : setting.ranks) 143 | ranks[rank.name] = QString::number(rank.minimumLevel); 144 | 145 | levelModule["ranks"] = ranks; 146 | } 147 | 148 | if (setting.maximumLevel != LEVELMODULE_MAXIMUM_LEVEL) 149 | { 150 | levelModuleDefault = false; 151 | levelModule["maximumLevel"] = QString::number(setting.maximumLevel); 152 | } 153 | 154 | if (setting.growthRate != LEVELMODULE_EXP_GROWTH) 155 | { 156 | levelModuleDefault = false; 157 | levelModule["growthRate"] = QString::number(setting.growthRate); 158 | } 159 | 160 | if (setting.expRequirement != LEVELMODULE_EXP_REQUIREMENT) 161 | { 162 | levelModuleDefault = false; 163 | levelModule["expRequirement"] = QString::number(setting.expRequirement); 164 | } 165 | 166 | if(!levelModuleDefault) 167 | current["levelModule"] = levelModule; 168 | 169 | bool behaviourModuleDefault = true; 170 | QJsonObject behaviourModule; 171 | if (setting.levelWhitelistedChannels.size() > 0 || setting.levelBlacklistedChannels.size() > 0 || setting.outputBlacklistedChannels.size() > 0 || setting.outputWhitelistedChannels.size() > 0) 172 | { 173 | QJsonObject levelBehaviourChannels; 174 | QJsonObject outputBehaviourChannels; 175 | 176 | for (snowflake_t channel : setting.levelWhitelistedChannels) 177 | levelBehaviourChannels[QString::number(channel)] = true; 178 | 179 | for (snowflake_t channel : setting.levelBlacklistedChannels) 180 | levelBehaviourChannels[QString::number(channel)] = false; 181 | 182 | for (snowflake_t channel : setting.outputWhitelistedChannels) 183 | outputBehaviourChannels[QString::number(channel)] = true; 184 | 185 | for (snowflake_t channel : setting.outputBlacklistedChannels) 186 | outputBehaviourChannels[QString::number(channel)] = false; 187 | 188 | behaviourModule["levelBehaviourChannels"] = levelBehaviourChannels; 189 | behaviourModule["outputBehaviourChannels"] = outputBehaviourChannels; 190 | behaviourModuleDefault = false; 191 | } 192 | 193 | if (!behaviourModuleDefault) 194 | current["behaviourModule"] = behaviourModule; 195 | 196 | json[QString::number(setting.id)] = current; 197 | } 198 | 199 | QJsonDocument doc(json); 200 | 201 | QString result = doc.toJson(QJsonDocument::Indented); 202 | file.write(qPrintable(result)); 203 | file.close(); 204 | } 205 | 206 | GuildSetting& GuildSettings::GetGuildSetting(snowflake_t id) 207 | { 208 | for (GuildSetting& setting : s_settings) 209 | { 210 | if (setting.id == id) 211 | return setting; 212 | } 213 | 214 | s_settings.append(CreateGuildSetting(id)); 215 | return s_settings[s_settings.size() - 1]; 216 | } 217 | 218 | void GuildSettings::AddGuild(snowflake_t id) 219 | { 220 | s_settings.push_back(CreateGuildSetting(id)); 221 | } 222 | 223 | bool GuildSettings::IsModuleEnabled(snowflake_t guild, const QString& moduleName, bool isDefault) 224 | { 225 | GuildSetting& setting = GetGuildSetting(guild); 226 | for (const QPair& module : setting.modules) 227 | { 228 | if (module.first == moduleName) 229 | return module.second; 230 | } 231 | 232 | // Not adding the actual module to the list of existing modules means that it's already 233 | // using the default enabled setting, thus the only modules who are going to be stored 234 | // in the module list of the guild settings are only going to be modules which don't have 235 | // their usual default enabled setting 236 | 237 | return isDefault; 238 | } 239 | 240 | void GuildSettings::ToggleModule(snowflake_t guild, const QString& moduleName, bool enabled, bool isDefault) 241 | { 242 | GuildSetting& setting = GetGuildSetting(guild); 243 | 244 | QList>& modules = setting.modules; 245 | 246 | for (int i = 0; i < modules.size(); i++) 247 | { 248 | if (modules[i].first == moduleName) 249 | if (enabled == isDefault) 250 | modules.removeAt(i); 251 | } 252 | if (enabled != isDefault) 253 | modules.append({ moduleName, enabled }); 254 | 255 | } 256 | 257 | bool GuildSettings::OutputAllowed(snowflake_t guild, snowflake_t channel) 258 | { 259 | GuildSetting s = GuildSettings::GetGuildSetting(guild); 260 | if (s.outputWhitelistedChannels.size() > 0) { 261 | for (int i = 0; i < s.outputWhitelistedChannels.size(); i++) 262 | if (s.outputWhitelistedChannels[i] == channel) 263 | return true; 264 | return false; 265 | } 266 | if (s.outputBlacklistedChannels.size() > 0) 267 | for (int i = 0; i < s.outputBlacklistedChannels.size(); i++) 268 | if (s.outputBlacklistedChannels[i] == channel) 269 | return false; 270 | return true; 271 | } 272 | 273 | bool GuildSettings::ExpAllowed(snowflake_t guild, snowflake_t channel) 274 | { 275 | GuildSetting s = GuildSettings::GetGuildSetting(guild); 276 | if (s.levelWhitelistedChannels.size() > 0) { 277 | for (int i = 0; i < s.levelWhitelistedChannels.size(); i++) 278 | if (s.levelWhitelistedChannels[i] == channel) 279 | return true; 280 | return false; 281 | } 282 | if (s.levelBlacklistedChannels.size() > 0) 283 | for (int i = 0; i < s.levelBlacklistedChannels.size(); i++) 284 | if (s.levelBlacklistedChannels[i] == channel) 285 | return false; 286 | return true; 287 | } 288 | 289 | 290 | GuildSetting GuildSettings::CreateGuildSetting(snowflake_t id) 291 | { 292 | GuildSetting s; 293 | s.id = id; 294 | s.maximumLevel = LEVELMODULE_MAXIMUM_LEVEL; 295 | s.expRequirement = LEVELMODULE_EXP_REQUIREMENT; 296 | s.growthRate = LEVELMODULE_EXP_GROWTH; 297 | s.prefix = "!"; 298 | s.primaryChannel = 0; 299 | return s; 300 | } 301 | -------------------------------------------------------------------------------- /src/modules/UserModule.cpp: -------------------------------------------------------------------------------- 1 | #include "UserModule.h" 2 | #include "CurrencyModule.h" 3 | #include "UmikoBot.h" 4 | 5 | #define descriptionTimeout 60 6 | 7 | using namespace Discord; 8 | 9 | UserModule::UserModule() 10 | : Module("users", true) 11 | { 12 | RegisterCommand(Commands::USER_MODULE_WHO_IS, "whois", 13 | [this](Client& client, const Message& message, const Channel& channel) 14 | { 15 | QStringList args = message.content().split(' '); 16 | snowflake_t authorId = message.author().id(); 17 | 18 | if (args.size() != 2) 19 | { 20 | client.createMessage(message.channelId(), "**Wrong Usage of Command!**"); 21 | return; 22 | } 23 | 24 | QList mentions = message.mentions(); 25 | snowflake_t userId; 26 | 27 | if (mentions.size() > 0) 28 | { 29 | userId = mentions[0].id(); 30 | } 31 | else 32 | { 33 | userId = UmikoBot::Instance().GetUserFromArg(channel.guildId(), args, 1); 34 | if (userId == 0) 35 | { 36 | client.createMessage(message.channelId(), "Could not find user!"); 37 | return; 38 | } 39 | } 40 | 41 | DescriptionData& data = guildDescriptionData[channel.guildId()]; 42 | 43 | if (data.isBeingUsed && data.userId == userId) 44 | { 45 | QString msg = "**Sorry,"; 46 | msg += UmikoBot::Instance().GetUsername(channel.guildId(), data.userId); 47 | msg += " is currently setting their description.**\nTry again later..."; 48 | client.createMessage(message.channelId(), msg); 49 | return; 50 | } 51 | 52 | UserDescription& desc = userDescriptions[channel.guildId()][getUserIndex(channel.guildId(), userId)]; 53 | QString msg = formDescriptionMessage(desc); 54 | 55 | if (msg.isEmpty()) 56 | { 57 | msg = UmikoBot::Instance().GetName(channel.guildId(), userId) + " prefers an air of mystery around them..."; 58 | client.createMessage(message.channelId(), msg); 59 | } 60 | else 61 | { 62 | UmikoBot::Instance().GetAvatar(channel.guildId(), userId).then( 63 | [this, msg, userId, channel, &client, message](const QString& icon) 64 | { 65 | Embed embed; 66 | QString name = UmikoBot::Instance().GetName(channel.guildId(), userId); 67 | embed.setAuthor(EmbedAuthor(name, "", icon)); 68 | embed.setColor(qrand() % 16777216); 69 | embed.setTitle("Description"); 70 | embed.setDescription(msg); 71 | 72 | client.createMessage(message.channelId(), embed); 73 | } 74 | ); 75 | } 76 | }); 77 | 78 | RegisterCommand(Commands::USER_MODULE_I_AM, "iam", 79 | [this](Client& client, const Message& message, const Channel& channel) 80 | { 81 | QStringList args = message.content().split(' '); 82 | snowflake_t authorId = message.author().id(); 83 | snowflake_t guildId = channel.guildId(); 84 | 85 | DescriptionData& data = guildDescriptionData[guildId]; 86 | 87 | if (data.isBeingUsed) 88 | { 89 | QString msg = "**Sorry, "; 90 | msg += UmikoBot::Instance().GetUsername(channel.guildId(), data.userId); 91 | msg += " is currently setting their description.**\nTry again later..."; 92 | client.createMessage(message.channelId(), msg); 93 | return; 94 | } 95 | 96 | if (args.size() > 1) 97 | { 98 | client.createMessage(message.channelId(), "**Wrong Usage of Command!**"); 99 | return; 100 | } 101 | 102 | data.isBeingUsed = true; 103 | data.userId = authorId; 104 | data.messageId = message.id(); 105 | data.questionUpTo = 0; 106 | data.currentUserDescription = &userDescriptions[guildId][getUserIndex(guildId, authorId)]; 107 | data.oldUserDescription = *data.currentUserDescription; 108 | 109 | data.timer = new QTimer(); 110 | data.timer->setInterval(descriptionTimeout * 1000); 111 | QObject::connect(data.timer, &QTimer::timeout, [this, &client, &data, message]() { 112 | if (data.isBeingUsed) 113 | { 114 | data.isBeingUsed = false; 115 | delete data.timer; 116 | data.timer = nullptr; 117 | client.createMessage(message.channelId(), "**Description timeout due to no valid response.**"); 118 | } 119 | }); 120 | data.timer->start(); 121 | 122 | QString msg = 123 | "**Tell me about yourself!** At any time, you can type:\n" 124 | "\t\tskip - to skip the current question (and clear the answer)\n" 125 | "\t\tcontinue - to skip the current question (and leave the answer)\n" 126 | "\t\tcancel - to revert all changes\n\n"; 127 | msg += descriptionQuestions[0].first; 128 | client.createMessage(message.channelId(), msg); 129 | }); 130 | 131 | RegisterCommand(Commands::USER_MODULE_ACHIEVEMENTS, "achievements", 132 | [this](Client& client, const Message& message, const Channel& channel) 133 | { 134 | QStringList args = message.content().split(' '); 135 | 136 | if (args.size() > 2) 137 | { 138 | client.createMessage(message.channelId(), "**Wrong Usage of Command!**"); 139 | return; 140 | } 141 | 142 | snowflake_t userId; 143 | 144 | if (args.size() == 1) 145 | { 146 | userId = message.author().id(); 147 | } 148 | else if (message.mentions().size() > 0) 149 | { 150 | userId = message.mentions()[0].id(); 151 | } 152 | else 153 | { 154 | userId = UmikoBot::Instance().GetUserFromArg(channel.guildId(), args, 1); 155 | if (userId == 0) 156 | { 157 | client.createMessage(message.channelId(), "Could not find user!"); 158 | return; 159 | } 160 | } 161 | 162 | UmikoBot::Instance().getGuildMember(channel.guildId(), userId).then( 163 | [this, &client, userId, channel, message](const GuildMember& member) 164 | { 165 | UmikoBot::Instance().GetAvatar(channel.guildId(), userId).then( 166 | [this, userId, channel, &client, message, member](const QString& icon) 167 | { 168 | Embed embed; 169 | QString name = UmikoBot::Instance().GetName(channel.guildId(), userId); 170 | embed.setAuthor(EmbedAuthor(name + "'s Achievements", "", icon)); 171 | embed.setColor(qrand() % 16777216); 172 | 173 | QString desc = "**General:**\n"; 174 | desc += "Date Joined: **" + member.joinedAt().date().toString() + "**\n"; 175 | 176 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 177 | if (currencyModule) 178 | { 179 | const CurrencyModule::CurrencyConfig& serverConfig = currencyModule->getServerData(channel.guildId()); 180 | const CurrencyModule::UserCurrency& userCurrency = currencyModule->getUserData(channel.guildId(), userId); 181 | 182 | desc += "\n**Currency:**\n"; 183 | desc += "Max " + serverConfig.currencyName + "s: **" + QString::number((double)userCurrency.maxCurrency) + " " + serverConfig.currencySymbol + "**\n"; 184 | desc += "`daily`s claimed: **" + QString::number(userCurrency.numberOfDailysClaimed) + "**\n"; 185 | desc += "`claim`s claimed: **" + QString::number(userCurrency.numberOfGiveawaysClaimed) + "**\n"; 186 | } 187 | 188 | embed.setDescription(desc); 189 | 190 | client.createMessage(message.channelId(), embed); 191 | }); 192 | }); 193 | 194 | }); 195 | } 196 | 197 | void UserModule::OnSave(QJsonDocument& doc) const 198 | { 199 | // User data 200 | QJsonObject docObj; 201 | 202 | for (auto server : userDescriptions.keys()) 203 | { 204 | QJsonObject serverJson; 205 | 206 | for (const UserDescription& user : userDescriptions[server]) 207 | { 208 | if (formDescriptionMessage(user) == "") 209 | continue; 210 | 211 | QJsonObject obj; 212 | obj["name"] = user.name; 213 | obj["location"] = user.location; 214 | obj["industry"] = user.industry; 215 | obj["programmingInterests"] = user.programmingInterests; 216 | obj["currentlyWorkingOn"] = user.currentlyWorkingOn; 217 | obj["githubLink"] = user.githubLink; 218 | 219 | serverJson[QString::number(user.userId)] = obj; 220 | } 221 | 222 | docObj[QString::number(server)] = serverJson; 223 | } 224 | 225 | doc.setObject(docObj); 226 | } 227 | 228 | void UserModule::OnLoad(const QJsonDocument& doc) 229 | { 230 | QJsonObject docObj = doc.object(); 231 | QStringList servers = docObj.keys(); 232 | 233 | userDescriptions.clear(); 234 | 235 | // User data 236 | for (auto server : servers) 237 | { 238 | snowflake_t guildId = server.toULongLong(); 239 | QJsonObject obj = docObj[server].toObject(); 240 | QStringList users = obj.keys(); 241 | QList descriptions; 242 | 243 | for (const QString& user : users) 244 | { 245 | auto userObj = obj[user].toObject(); 246 | 247 | UserDescription description { 248 | user.toULongLong(), 249 | userObj["name"].toString(), 250 | userObj["location"].toString(), 251 | userObj["industry"].toString(), 252 | userObj["programmingInterests"].toString(), 253 | userObj["currentlyWorkingOn"].toString(), 254 | userObj["githubLink"].toString(), 255 | }; 256 | 257 | descriptions.append(description); 258 | } 259 | 260 | userDescriptions.insert(guildId, descriptions); 261 | } 262 | } 263 | 264 | void UserModule::OnMessage(Client& client, const Message& message) 265 | { 266 | client.getChannel(message.channelId()).then( 267 | [this, message, &client](const Channel& channel) 268 | { 269 | snowflake_t guildId = channel.guildId(); 270 | 271 | if (guildId == 0 || message.author().bot()) 272 | { 273 | // It's a bot / we're in a DM 274 | return; 275 | } 276 | 277 | DescriptionData& descriptionData = guildDescriptionData[guildId]; 278 | snowflake_t authorId = message.author().id(); 279 | QString messageContent = message.content(); 280 | 281 | if (descriptionData.messageId == message.id()) 282 | { 283 | // This is the !iam command, ignore it 284 | return; 285 | } 286 | 287 | if (descriptionData.isBeingUsed && descriptionData.userId == authorId) 288 | { 289 | QString contentToWrite = messageContent; 290 | bool shouldWrite = true; 291 | 292 | if (messageContent == "skip") 293 | { 294 | contentToWrite = ""; 295 | } 296 | else if (messageContent == "continue") 297 | { 298 | shouldWrite = false; 299 | } 300 | else if (messageContent == "cancel") 301 | { 302 | *descriptionData.currentUserDescription = descriptionData.oldUserDescription; 303 | descriptionData.isBeingUsed = false; 304 | delete descriptionData.timer; 305 | descriptionData.timer = nullptr; 306 | 307 | client.createMessage(message.channelId(), "**Cancelled setting description\n**Reverting to old values."); 308 | return; 309 | } 310 | 311 | if (shouldWrite) 312 | { 313 | descriptionQuestions[descriptionData.questionUpTo].second(*descriptionData.currentUserDescription, contentToWrite); 314 | } 315 | 316 | descriptionData.questionUpTo += 1; 317 | descriptionData.timer->start(); 318 | 319 | if (descriptionData.questionUpTo == descriptionQuestions.size()) 320 | { 321 | client.createMessage(message.channelId(), "**All done!** Thanks for your time."); 322 | descriptionData.isBeingUsed = false; 323 | delete descriptionData.timer; 324 | descriptionData.timer = nullptr; 325 | 326 | return; 327 | } 328 | 329 | QString msg = descriptionQuestions[descriptionData.questionUpTo].first; 330 | client.createMessage(message.channelId(), msg); 331 | } 332 | } 333 | ); 334 | 335 | Module::OnMessage(client, message); 336 | } 337 | 338 | snowflake_t UserModule::getUserIndex(snowflake_t guild, snowflake_t id) 339 | { 340 | QList& guildDescriptions = userDescriptions[guild]; 341 | 342 | for (auto it = guildDescriptions.begin(); it != guildDescriptions.end(); ++it) 343 | { 344 | if (it->userId == id) 345 | { 346 | return std::distance(guildDescriptions.begin(), it); 347 | } 348 | } 349 | 350 | // If user is not added to the system, make a new one 351 | guildDescriptions.append(UserDescription { id, "" }); 352 | return std::distance(guildDescriptions.begin(), std::prev(guildDescriptions.end())); 353 | } 354 | 355 | QString UserModule::formDescriptionMessage(const UserDescription& desc) const 356 | { 357 | QString msg; 358 | 359 | if (desc.name != "") 360 | msg += "**Name: **" + desc.name + "\n"; 361 | 362 | if (desc.location != "") 363 | msg += "**Location: **" + desc.location + "\n"; 364 | 365 | if (desc.industry != "") 366 | msg += "**Industry: **" + desc.industry + "\n"; 367 | 368 | if (desc.programmingInterests != "") 369 | msg += "**Programming Interests: **" + desc.programmingInterests + "\n"; 370 | 371 | if (desc.currentlyWorkingOn != "") 372 | msg += "**Currently working on: **" + desc.currentlyWorkingOn + "\n"; 373 | 374 | if (desc.githubLink != "") 375 | msg += "**GitHub: **" + desc.githubLink + "\n"; 376 | 377 | return msg; 378 | } 379 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # UmikoBot 3 | 4 | Umiko is the bot used on [TheCherno's Discord Server](https://discord.gg/K9rMPsu9pQ). This repository is what makes Umiko; it's the source code and it is completely open source! 5 | 6 | ## ❓ What can it do? 7 | 8 | Umiko can do everything that a Discord bot can technically do. Of course it doesn't do all of it, but if you feel like there's something missing that would make Umiko better, contributions are always welcomed! 9 | 10 | ## ℹ️️ What does it do? 11 | 12 | Umiko is mainly built for fun. Currently, Umiko can 13 | 14 | - Keep track of your Currency and XP on the server: Both the Currency and XP system are extensively developed and are backed by a multitude of commands. 15 | - Show you memes from the internet. 16 | - Show you random GitHub repositories because why not? 17 | - Keep track of your timezone. 18 | - Keep track of your information so that others can use a single command to know (*everything* :eyes:) about you. 19 | - Use your XP to assign you levels so that there's a bit of a competitive spark. 20 | - Reward you for typing `daily`, well, daily. 21 | - Crash and work unexpectedly due to zillions of bugs here and there... 22 | - Reward you with some (very useful) currency because you typed and you were lucky enough. 23 | - Host events which tweak with Umiko's normal behavior to keep the fun going. 24 | 25 | ...and many more features still in works! 26 | 27 | ## 🙋‍♂️ Contributing 28 | 29 | So you want to contribute? Awesome! Let's get you going. For starters, know that Umiko uses **[QDiscord](https://github.com/Gaztin/QDiscord)** to interface with the Discord API. This is already in the repo as a submodule, so you don't need to worry much. 30 | 31 | There are some prerequisites to the whole setup and build process after you're done forking and cloning (checkout Git 101 under **[Help and FAQ](#-help-and-faq)** if you're unfamiliar with the whole process), so let's start with those: 32 | 33 | ### Prerequisites 34 | 35 | #### Windows 36 | 37 | - **[Qt](https://www.qt.io/) (`> 5`)** 38 | - **[OpenSSL](https://indy.fulgan.com/SSL/Archive/Experimental/openssl-1.0.2o-x64-VC2017.zip) (`OpenSSL v1.0.2`)** 39 | - We recommend using Visual Studio 2017 or 2019. 40 | - Some sort of bash (to run the setup scripts). 41 | 42 | #### Linux 43 | 44 | To build on Linux you're going to need to have Qt5 and the **developer version** of Qt5WebSockets. You will also need `qt5-qmake`. 45 | 46 | ### Setting up the project 47 | 48 | You'd have to do quite a bit of work to get the bot setup if we didn't have these two scripts: 49 | 50 | - `init.sh` 51 | - `generate_project_files.sh` 52 | 53 | Thanks to those, all you need to do is run them in succession and you should have all the necessary files to work with. 54 | 55 | #### `init.sh` 56 | 57 | This file makes temporary folders and files to facilitate the setup. It also makes sure you have `premake5` to generate the project files and that the submodules are initialized and cloned (helpful for those who forgot to clone with `--recurse-submodules`). 58 | 59 | On Windows it will ask you for the paths of the **x86** and **x64** versions of Qt and OpenSSL. You don't need both of the versions; you can just specify one and only build for that specific platform. 60 | 61 | > Make sure you use forward slashes instead of the backslashes. 62 | 63 | #### `generate_project_files.sh` 64 | 65 | As the name suggests, this script generates the necessary files by using [Premake](https://premake.github.io/). 66 | 67 | You can provide an action like you do with premake, but by default the script generates files for 68 | 69 | - Visual Studio 2019 if you're on Windows. 70 | - `qmake` if you're on Linux. 71 | 72 | ### Building 73 | 74 | This section assumes that you used the default targets and didn't provide a custom action. 75 | 76 | Once the files are generated, you should see a `sln/` directory. 77 | 78 | #### On Linux 79 | 80 | `cd` into `sln` and execute `qmake` followed by `make`. 81 | 82 | #### On Windows 83 | 84 | You should find a Visual Studio solution file (`.sln`) inside `sln`. Open that and then build the solution using Visual Studio. 85 | 86 | ### Running 87 | 88 | The token to run the bot is taken as a command line argument. 89 | 90 | For Visual Studio, you can add the token here: 91 | 92 | |![Visual Studio Command Args](https://cdn.discordapp.com/attachments/353076704945766403/680397059068919808/unknown.png)| 93 | |:--:| 94 | |`Project Properties` (`Alt + Enter`) > `Configuration Properties` > `Debugging` > `Command Arguments`| 95 | 96 | ## ❓ Help and FAQ 97 | 98 | If you weren't able to properly carry out the setup process, or if you just want to know more Umiko in general, considering checking these questions before opening an issue: 99 | 100 |
101 | Is Umiko going to be open for invites anytime soon? 102 | This hasn't been thought about much, but all of Umiko is developed keeping multiple servers in mind, so we're ready for that already! 103 |
104 | 105 |
106 | Git 101 107 | 108 | 109 | This isn't an extensive guide by any means, but it'll bring you up to speed to start contributing. 110 | 111 | The first thing you will need to do is to *fork* this repo (repository). You need to do this because you don't have *direct push permissions* to this repository (meaning that you can't just publish your code here directly). 112 | 113 | *Forking* this repo essentially means creating your own *copy* of this repo on your GitHub account. It's not complicated either; just click on the Fork button on the upper-right corner of this repo's page, and voila! 114 | 115 | Your *forked repo* (or simply *fork*) stays on GitHub and is thus called a remote (this remote is usually referred to as `origin`). What you need to do next would be to get it locally on your own system. This process is called *cloning*. To *clone* a repository into some local directory, go to that directory, fire up a terminal and type this: 116 | 117 | ``` 118 | git clone 119 | ``` 120 | 121 | In our case, the link would be `https://github.com//UmikoBot.git` where `` is, well, your GitHub account. We also have submodules to take care of. So we use 122 | 123 | ``` 124 | git clone --recurse-submodules -j8 https://github.com//UmikoBot.git 125 | ``` 126 | 127 | (`-j8` is an optional flag which enables fetching up to 8 submodules in parallel. We don't really need to use it, but it's always better to know about it.) 128 | 129 | > If that doesn't work, you might be using an old version of Git. Just do 130 | > 131 | > ``` 132 | > git clone https://github.com/> /GameProject-1.git 133 | > ``` 134 | > 135 | > and make sure you run [`init.sh`](#initsh) right after; it will handle the submodule stuff (by using `git submodule update --init --recursive`). 136 | 137 | You have successfully forked and cloned the project. If you came from [Contributing](#️-contributing), you can continue with the [prerequisites](#prerequisites). 138 | 139 | The following material explains the basic commands and workflow to use for making contributions after you're done setting up and can build the project. 140 | 141 | Remember, you can always check the *status* of your repo using 142 | 143 | ``` 144 | git status 145 | ``` 146 | 147 | You can also learn more about a command by using 148 | 149 | ``` 150 | git help 151 | ``` 152 | 153 | And of course, the **[documentation](https://git-scm.com/docs)** always helps. 154 | 155 | We talked about remotes just a bit earlier. We came across the `origin` which is another name for your fork. The remote from where you forked your fork is usually called `upstream`. To list the remotes you have, use 156 | 157 | ``` 158 | git remote -v 159 | ``` 160 | 161 | You'll notice that you don't actually have an upstream setup (unless you set it up yourself, and in that case why are you here?). To add the upstream repository (which would be this repository), do 162 | 163 | ``` 164 | git remote add upstream https://github.com/TheChernoCommunity/UmikoBot.git 165 | ``` 166 | 167 | You have covered all the basic prerequisites. From now on, when you intend to add a certain feature, follow these steps: 168 | 169 | 1. Firstly, you need to create a new branch dedicated to that feature. You would usually branch out from the `master` branch of the main repo and then work on those changes. 170 | 2. With each small change you bring, make sure you commit it. Each commit is essentially a package of changes to different files. It's up to you to decide when something doesn't belong to a particular commit. 171 | 3. After the whole thing is properly done, you can then push it to your origin (you can push commits one by one while you work through them as well, but well it's up to you again). 172 | 4. The only thing is that this code isn't part of the upstream. To make it part of that, you need to open a Pull Request (or a PR) which is basically a request to pull changes from your code. Pull Requests are a GitHub construct, and to open one, simply go to `Pull Requests` (on this repo's page) > `New Pull Request`. 173 | 5. The code would then be reviewed and if it's all fine, it should be merged with the repo. 174 | 6. The branch that you worked on would then become a stale branch. You can now delete it. 175 | 7. While working on a feature if you find yourself in a scenario where upstream has had new commits that you would want to have in your version as well, just *rebase* your branch onto upstream's branch (this would usually be `master`). 176 | 177 | The commands that you'll find useful to go with the above process are listed here (stuff beginning with `#` are comments to guide you): 178 | 179 | ``` 180 | # Make a branch basing off of the current point you're at 181 | git branch 182 | 183 | # Checkout that branch (you aren't moved to that branch by default) 184 | git checkout 185 | 186 | # To make a new branch and also check it out 187 | git checkout -b 188 | 189 | # Checkout the origin's master 190 | git checkout origin/master 191 | 192 | # Deleting a branch on origin 193 | git push -d origin 194 | 195 | # Deleting a local branch 196 | git branch -d 197 | 198 | # Use add followed by a list of files separated by space to add/stage files for a commit 199 | git add # and so on 200 | 201 | # Stage all changes 202 | git add -A 203 | 204 | # To commit after staging files 205 | git commit -m "A meaningful commit message" 206 | 207 | # Pushing your changes to your remote 208 | git push 209 | 210 | # Pulling changes 211 | git pull 212 | 213 | # Ensuring a local branch is up to date with a branch on upstream 214 | git fetch upstream # Fetch the meta-data 215 | git checkout # Ensure you're at the correct branch locally 216 | git merge upstream/ 217 | 218 | # Rebasing a branch (branch1) onto another branch (branch2) 219 | git checkout branch1 # First checkout the branch in concern 220 | git rebase branch2 # Then rebase 221 | ## This can also be done with a single command: 222 | git rebase branch2 branch1 223 | 224 | ## Example: Rebasing your master to origin's master 225 | git checkout master # Of course make sure you're in the correct branch 226 | git fetch origin # Update origin 227 | git rebase origin/master 228 | ## This also has the same effect as the last two commands: 229 | git pull --rebase origin master 230 | 231 | # Looking at the commits you made 232 | git log --oneline 233 | git log # More verbose 234 | ## NOTE: You will find the commit hash with the commits 235 | 236 | # Checking out a commit temporarily 237 | git checkout 238 | ## NOTE: This will detach your HEAD pointer. A detached state is when the HEAD points at a commit instead of a branch. 239 | ## If you already know you want to make changes, make a new branch instead with the following set of commands. 240 | 241 | # Making a new branch basing off of a commit and checking it out 242 | git checkout -b 243 | 244 | # Reverting to an old commit 245 | git reset --hard 246 | 247 | ## NOTE: ALL uncommitted work will be lost. If you have uncommitted changes you want to keep, stash them: 248 | git stash # Firstly, stash the changes 249 | git reset --hard # Revert to this commit 250 | git stash pop # Pop all the changes from the stash 251 | 252 | ``` 253 | 254 |
255 | 256 |
257 | Is there some kind of a Roadmap? 258 | 259 | 260 | The closest thing to that we currently have is [this](https://github.com/TheChernoCommunity/UmikoBot/projects/1). 261 |
262 | 263 |
264 | Is there some kind of a community where I can interact with fellow contributors? 265 | 266 | 267 | Glad you asked! We'll be happy to see you on our [Discord Server!](https://discord.gg/zrUpn7RG5k) 268 |
269 | -------------------------------------------------------------------------------- /src/modules/ModerationModule.cpp: -------------------------------------------------------------------------------- 1 | #include "ModerationModule.h" 2 | #include "UmikoBot.h" 3 | #include "core/Permissions.h" 4 | 5 | using namespace Discord; 6 | 7 | ModerationModule::ModerationModule() 8 | : Module("moderation", true) 9 | { 10 | m_warningCheckTimer.setInterval(24 * 60 * 60 * 1000); // 24hr timer 11 | QObject::connect(&m_warningCheckTimer, &QTimer::timeout, [this] 12 | { 13 | checkWarningsExpiry(); 14 | }); 15 | m_warningCheckTimer.start(); 16 | 17 | RegisterCommand(Commands::MODERATION_INVITATION_TOGGLE, "invitations", 18 | [this](Client& client, const Message& message, const Channel& channel) 19 | { 20 | QStringList args = message.content().split(' '); 21 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 1, args, true, [this, &client, channel, message, args]() 22 | { 23 | m_invitationModeration ^= true; 24 | client.createMessage(message.channelId(), m_invitationModeration ? "Invitations will be deleted!" : "Invitations won't be deleted!"); 25 | }); 26 | }); 27 | 28 | RegisterCommand(Commands::MODERATION_WARN, "warn", 29 | [this](Client& client, const Message& message, const Channel& channel) 30 | { 31 | QStringList args = message.content().split(' '); 32 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, &client, channel, message, args]() 33 | { 34 | snowflake_t warnedBy = message.author().id(); 35 | snowflake_t user; 36 | QList mentions = message.mentions(); 37 | 38 | if (mentions.size() > 0) 39 | { 40 | user = mentions[0].id(); 41 | } 42 | else 43 | { 44 | user = UmikoBot::Instance().GetUserFromArg(channel.guildId(), args, 1); 45 | 46 | if (!user) 47 | { 48 | client.createMessage(message.channelId(), "**Couldn't find " + args.at(1) + "**"); 49 | return; 50 | } 51 | } 52 | 53 | QString msg = "[no message]"; 54 | 55 | if (args.size() > 2) 56 | { 57 | // The 6 starts the search after "!warn " 58 | msg = message.content().mid(message.content().indexOf(QRegExp("[ \t\n\v\f\r]"), 6)).trimmed(); 59 | } 60 | 61 | warnings[user].append(UserWarning { warnedBy, msg }); 62 | 63 | unsigned int numberOfWarnings = countWarnings(user); 64 | QString warningNumberString = ""; 65 | 66 | switch (numberOfWarnings) 67 | { 68 | case 1: warningNumberString = "First"; break; 69 | case 2: warningNumberString = "Second"; break; 70 | case 3: warningNumberString = "Third"; break; 71 | 72 | // Good enough, shouldn't be seen very often (if at all) 73 | default: warningNumberString = QString::number(numberOfWarnings) + "th"; break; 74 | } 75 | 76 | QString output = QString("%1 warning for <@%2>.").arg(warningNumberString, QString::number(user)); 77 | client.createMessage(message.channelId(), output); 78 | }); 79 | }); 80 | 81 | // This is used by both !warnings and !warnings-all 82 | auto warningsCommand = [this](Client& client, const Message& message, const Channel& channel, bool showExpired) 83 | { 84 | QStringList args = message.content().split(' '); 85 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, &client, channel, message, args, showExpired]() 86 | { 87 | snowflake_t user; 88 | QList mentions = message.mentions(); 89 | 90 | if (mentions.size() > 0) 91 | { 92 | user = mentions[0].id(); 93 | } 94 | else 95 | { 96 | user = UmikoBot::Instance().GetUserFromArg(channel.guildId(), args, 1); 97 | 98 | if (!user) 99 | { 100 | client.createMessage(message.channelId(), "**Couldn't find " + args.at(1) + "**"); 101 | return; 102 | } 103 | } 104 | 105 | UmikoBot::Instance().GetAvatar(channel.guildId(), user).then( 106 | [this, user, channel, &client, message, showExpired](const QString& icon) 107 | { 108 | Embed embed; 109 | embed.setColor(qrand() % 11777216); 110 | embed.setAuthor(EmbedAuthor("Warnings for " + UmikoBot::Instance().GetName(channel.guildId(), user), "", icon)); 111 | 112 | QString desc = countWarnings(user, showExpired) == 0 ? "Nothing to see here..." : ""; 113 | QList& userWarnings = warnings[user]; 114 | bool hasOutputExpiredMessage = false; 115 | 116 | // Sorts the warnings in order of newest to oldest 117 | qSort(userWarnings.begin(), userWarnings.end(), [](const UserWarning& first, const UserWarning& second) 118 | { 119 | return second.when < first.when; 120 | }); 121 | 122 | for (auto& warning : userWarnings) 123 | { 124 | if (!showExpired && warning.expired) 125 | { 126 | continue; 127 | } 128 | 129 | if (warning.expired) 130 | { 131 | if (!showExpired) 132 | continue; 133 | 134 | if (hasOutputExpiredMessage) 135 | continue; 136 | 137 | desc += "\n===== Expired =====\n\n"; 138 | hasOutputExpiredMessage = true; 139 | } 140 | 141 | desc += QString("%1 - warned by %2\n**%3**\n\n").arg(warning.when.toString("yyyy-MM-dd hh:mm:ss"), 142 | UmikoBot::Instance().GetName(channel.guildId(), warning.warnedBy), 143 | warning.message); 144 | 145 | } 146 | 147 | embed.setDescription(desc); 148 | client.createMessage(message.channelId(), embed); 149 | }); 150 | }); 151 | }; 152 | 153 | RegisterCommand(Commands::MODERATION_WARNINGS, "warnings", 154 | [this, warningsCommand](Client& client, const Message& message, const Channel& channel) 155 | { 156 | return warningsCommand(client, message, channel, false); 157 | }); 158 | 159 | RegisterCommand(Commands::MODERATION_WARNINGS_ALL, "warnings-all", 160 | [this, warningsCommand](Client& client, const Message& message, const Channel& channel) 161 | { 162 | return warningsCommand(client, message, channel, true); 163 | }); 164 | 165 | RegisterCommand(Commands::MODERATION_ADD_DODGY_DOMAIN, "add-dodgy-domain", 166 | [this](Client& client, const Message& message, const Channel& channel) 167 | { 168 | QStringList args = message.content().split(' '); 169 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, &client, channel, message, args]() 170 | { 171 | dodgyDomainNames.insert(args.at(1)); 172 | client.createMessage(message.channelId(), QString("Added '%1' as a dodgy domain name.").arg(args.at(1))); 173 | }); 174 | }); 175 | 176 | RegisterCommand(Commands::MODERATION_REMOVE_DODGY_DOMAIN, "remove-dodgy-domain", 177 | [this](Client& client, const Message& message, const Channel& channel) 178 | { 179 | QStringList args = message.content().split(' '); 180 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, &client, channel, message, args]() 181 | { 182 | if (dodgyDomainNames.remove(args.at(1))) 183 | { 184 | client.createMessage(message.channelId(), QString("Removed '%1' as a dodgy domain name.").arg(args.at(1))); 185 | } 186 | else 187 | { 188 | client.createMessage(message.channelId(), QString("Could not find '%1' in my list of dodgy domain names.").arg(args.at(1))); 189 | } 190 | }); 191 | }); 192 | } 193 | 194 | void ModerationModule::OnMessage(Client& client, const Message& message) 195 | { 196 | client.getChannel(message.channelId()).then( 197 | [this, message, &client](const Channel& channel) 198 | { 199 | const QString& content = message.content(); 200 | 201 | if (m_invitationModeration) 202 | { 203 | if (content.contains("https://discord.gg/", Qt::CaseInsensitive)) 204 | { 205 | auto authorID = message.author().id(); 206 | ::Permissions::ContainsPermission(client, channel.guildId(), message.author().id(), CommandPermission::ADMIN, 207 | [this, message, &client, authorID, channel, content](bool result) 208 | { 209 | if (!result) 210 | { 211 | client.deleteMessage(message.channelId(), message.id()); 212 | UmikoBot::Instance().createDm(authorID) 213 | .then([authorID, &client, message, content](const Channel& channel) 214 | { 215 | client.createMessage(channel.id(), "**Invitation link of servers aren't allowed in any channels on this server. Please take it to DMs!** Here is your message which you posted in the server:\n"); 216 | client.createMessage(channel.id(), content); 217 | }); 218 | } 219 | }); 220 | } 221 | } 222 | 223 | for (auto& domain : dodgyDomainNames) 224 | { 225 | if (content.contains(domain)) 226 | { 227 | if (content.contains(".com") || content.contains(".ru") || content.contains(".net")|| content.contains(".site") || 228 | content.contains(".info")|| content.contains(".gift") || content.contains(".ga") || content.contains(".birth")) 229 | { 230 | snowflake_t outputChannel = GuildSettings::GetGuildSetting(channel.guildId()).primaryChannel; 231 | if (!outputChannel) outputChannel = channel.id(); 232 | 233 | client.createMessage(outputChannel, QString("<@%1> posted a message containing a dodgy URL in <#%2>. Removed!") 234 | .arg(message.author().id()).arg(message.channelId())); 235 | client.deleteMessage(channel.id(), message.id()).otherwise([&client, message, outputChannel]() 236 | { 237 | client.createMessage(outputChannel, QString("Failed to remove <@%1>'s message in <#%2>! I might be lacking permissions...") 238 | .arg(message.author().id()).arg(message.channelId()));; 239 | }); 240 | } 241 | } 242 | } 243 | }); 244 | 245 | Module::OnMessage(client, message); 246 | } 247 | 248 | 249 | void ModerationModule::OnSave(QJsonDocument& doc) const 250 | { 251 | QJsonObject json; 252 | QJsonObject moderation; 253 | 254 | moderation["invitationModeration"] = m_invitationModeration; 255 | 256 | QJsonObject warningsJson; // Holds a map of user:array_of_warnings 257 | for (auto user : warnings.keys()) 258 | { 259 | const QList& userWarnings = warnings.value(user); 260 | QJsonArray warningsArrayJson; // Holds the array of warnings for a specific user 261 | 262 | for (auto& warning : userWarnings) 263 | { 264 | QJsonObject warningJson; 265 | warningJson["warnedBy"] = QString::number(warning.warnedBy); 266 | warningJson["when"] = warning.when.toString(); 267 | warningJson["message"] = warning.message; 268 | warningJson["expired"] = warning.expired; 269 | 270 | warningsArrayJson.append(warningJson); 271 | } 272 | 273 | warningsJson[QString::number(user)] = warningsArrayJson; 274 | } 275 | 276 | moderation["warnings"] = warningsJson; 277 | 278 | QJsonArray dodgyDomainsArray; 279 | for (auto& domain : dodgyDomainNames) 280 | { 281 | dodgyDomainsArray.append(domain); 282 | } 283 | moderation["dodgyDomainNames"] = dodgyDomainsArray; 284 | 285 | json["moderation"] = moderation; 286 | doc.setObject(moderation); 287 | } 288 | 289 | void ModerationModule::OnLoad(const QJsonDocument& doc) 290 | { 291 | QJsonObject json = doc.object(); 292 | QJsonObject moderation = json["moderation"].toObject(); 293 | m_invitationModeration = json["invitationModeration"].toBool(); 294 | 295 | // Adds in default dodgy links 296 | dodgyDomainNames.clear(); 297 | dodgyDomainNames.insert("discorcl"); 298 | dodgyDomainNames.insert("cliscord"); 299 | dodgyDomainNames.insert("dlscord"); 300 | dodgyDomainNames.insert("d1scord"); 301 | dodgyDomainNames.insert("disc0rd"); 302 | dodgyDomainNames.insert("d1sc0rd"); 303 | dodgyDomainNames.insert("disord"); 304 | dodgyDomainNames.insert("discordgifts"); 305 | dodgyDomainNames.insert("disordgifts"); 306 | dodgyDomainNames.insert("discord-app"); 307 | dodgyDomainNames.insert("discordgg"); 308 | dodgyDomainNames.insert("discrod"); 309 | dodgyDomainNames.insert("dicsord"); 310 | 311 | QJsonArray dodgyDomainsArray = json["dodgyDomainNames"].toArray(); 312 | for (const auto& domain : dodgyDomainsArray) 313 | { 314 | dodgyDomainNames.insert(domain.toString()); 315 | } 316 | 317 | // Loads warnings 318 | warnings.clear(); 319 | QJsonObject warningsObj = json["warnings"].toObject(); 320 | QStringList users = warningsObj.keys(); 321 | 322 | for (auto& userString : users) 323 | { 324 | snowflake_t user = userString.toULongLong(); 325 | QList userWarnings; 326 | QJsonArray warningsArrayJson = warningsObj[userString].toArray(); 327 | 328 | for (const auto& warningJson : warningsArrayJson) 329 | { 330 | QJsonObject warningObj = warningJson.toObject(); 331 | 332 | UserWarning warning { 333 | warningObj["warnedBy"].toString().toULongLong(), 334 | QDateTime::fromString(warningObj["when"].toString()), 335 | warningObj["message"].toString(), 336 | warningObj["expired"].toBool() 337 | }; 338 | 339 | userWarnings.append(warning); 340 | } 341 | 342 | warnings[user] = userWarnings; 343 | } 344 | 345 | checkWarningsExpiry(); 346 | } 347 | 348 | unsigned int ModerationModule::countWarnings(snowflake_t user, bool countExpired) 349 | { 350 | const QList& userWarnings = warnings[user]; 351 | 352 | if (countExpired) 353 | { 354 | return userWarnings.size(); 355 | } 356 | 357 | unsigned int total = 0; 358 | 359 | for (const UserWarning& warning : userWarnings) 360 | { 361 | if (!warning.expired) 362 | { 363 | total += 1; 364 | } 365 | } 366 | 367 | return total; 368 | } 369 | 370 | // Checks expiry (warnings expire after 3 months) 371 | void ModerationModule::checkWarningsExpiry() 372 | { 373 | QDateTime now = QDateTime::currentDateTime(); 374 | 375 | for (auto& userWarnings : warnings) 376 | { 377 | for (auto& warning : userWarnings) 378 | { 379 | if (!warning.expired && (warning.when.addMonths(3) <= now)) 380 | { 381 | warning.expired = true; 382 | } 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /res/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "GLOBAL_STATUS": { 3 | "brief": "Retrieves status of a user", 4 | "usage": "status [name/@mention/id]", 5 | "additional": "If no name/mention/id is specified it returns the status of the person who executes the command", 6 | "admin": false 7 | }, 8 | "GLOBAL_HELP": { 9 | "brief": "Returns the usage of a command", 10 | "usage": "help [command]", 11 | "additional": "", 12 | "admin": false 13 | }, 14 | "GLOBAL_SET_PREFIX": { 15 | "brief": "Sets the prefix of the bot", 16 | "usage": "setprefix ", 17 | "additional": "", 18 | "admin": true 19 | }, 20 | "GLOBAL_MODULE": { 21 | "brief": "Controls all the modules of the bot", 22 | "usage": "module [name]", 23 | "additional": "**module list** is used to list all the commands, if the name of a module is passed, it will list all the commands of that module\n**module enable** is used to enable a disabled module\n**module disable** is used to disable a module", 24 | "admin": true 25 | }, 26 | "GLOBAL_OUTPUT": { 27 | "brief": "Controls the output of the bot", 28 | "usage": "output ", 29 | "additional": "", 30 | "admin": true 31 | }, 32 | "LEVEL_MODULE_TOP": { 33 | "brief": "Returns the top of a specified user count", 34 | "usage": "top (List of top people until )\ntop (List of top people from to )", 35 | "additional": "**Example:\n** `!top 3`: Displays the top 3 people. \n`!top 3 5`: Displays the 3rd, 4th and 5th top users.", 36 | "admin": false 37 | }, 38 | "LEVEL_MODULE_RANK": { 39 | "brief": "Used to control the ranking system", 40 | "usage": "rank \nrank list\nrank add \nrank remove \nrank edit ", 41 | "additional": "**rank list** returns the list of all the ranks, if any\n**rank add** adds a rank with a minimum level and a name\n**rank remove** removes a rank using an id\n**rank edit** edits a rank using it's id\n\nTo get the id of a rank you must use **rank list**", 42 | "admin": true 43 | }, 44 | "LEVEL_MODULE_MAX_LEVEL": { 45 | "brief": "Used to change the ranking system's maximum level", 46 | "usage": "setmaxlevel \nsetmaxlevel current", 47 | "additional": "**setmaxlevel current** will return the current maximum level", 48 | "admin": true 49 | }, 50 | "LEVEL_MODULE_EXP_REQUIREMENT": { 51 | "brief": "Used to change the ranking system's exp requirement", 52 | "usage": "setexpreq \nsetexpreq current", 53 | "additional": "**setexpreq current** will return the current exp requirement", 54 | "admin": true 55 | }, 56 | "LEVEL_MODULE_EXP_GROWTH_RATE": { 57 | "brief": "Used to change the ranking system's exp growth rate", 58 | "usage": "setgrowthrate \nsetgrowthrate current", 59 | "additional": "The rate must be a number above 1\nHow the exp for the levels are calculated is like so:\nCurrentLevel = LastLevel * GrowthRate\n**setgrowthrate current** will return the current exp requirement", 60 | "admin": true 61 | }, 62 | "LEVEL_MODULE_EXP_GIVE": { 63 | "brief": "Used to give exp to a user.", 64 | "usage": "givexp [L] ", 65 | "additional": "**givexp 50L user** will give 50 levels of experience to that user\n**givexp 50 user**, will give 50 exp points to that user", 66 | "admin": true 67 | }, 68 | "LEVEL_MODULE_EXP_TAKE": { 69 | "brief": "Used to take exp from a user.", 70 | "usage": "takexp [L] ", 71 | "additional": "**takexp 50L user** will take 50 levels of experience from that user\n**takexp 50 user**, will take 50 exp points from that user", 72 | "admin": true 73 | }, 74 | "LEVEL_MODULE_BLOCK_EXP": { 75 | "brief": "Controls the exp gain of the bot on specific channels", 76 | "usage": "blockxp ", 77 | "additional": "", 78 | "admin": true 79 | }, 80 | "USER_MODULE_WHO_IS": { 81 | "brief": "Returns a description on the person requested.", 82 | "usage": "whois Person", 83 | "additional": "", 84 | "admin": false 85 | }, 86 | "USER_MODULE_I_AM": { 87 | "brief": "Allows the user to set information about themselves.", 88 | "usage": "iam", 89 | "additional": "", 90 | "admin:": false 91 | }, 92 | "USER_MODULE_ACHIEVEMENTS": { 93 | "brief": "Shows achievements of a user.", 94 | "usage": "achievements ", 95 | "additional": "", 96 | "admin:": false 97 | }, 98 | "TIMEZONE_MODULE_TIMEOFFSET": { 99 | "brief": "Sets the timezone offset of the user.", 100 | "usage": "timeoffset ", 101 | "additional": "Where offset is the timezone offset from **UTC**\nIf your timezone is UTC+3 then your offset is +3", 102 | "admin": false 103 | }, 104 | "MODERATION_INVITATION_TOGGLE": { 105 | "brief": "Toggles if invitations should be automatically deleted", 106 | "usage": "invitations", 107 | "additional": "**NOTE:** Admins can send server links even if the invitation is turned on", 108 | "admin": true 109 | }, 110 | "MODERATION_WARN": { 111 | "brief": "Warns a user.", 112 | "usage": "warn [message]", 113 | "additional": "", 114 | "admin": true 115 | }, 116 | "MODERATION_WARNINGS": { 117 | "brief": "Displays active warnings for a user.", 118 | "usage": "warnings ", 119 | "additional": "To see all warnings, use `!warnings-all`", 120 | "admin": true 121 | }, 122 | "MODERATION_WARNINGS_ALL": { 123 | "brief": "Displays all warnings for a user.", 124 | "usage": "warnings-all ", 125 | "additional": "This shows all warnings, expired and active. To see only active warnings, use `!warnings`", 126 | "admin": true 127 | }, 128 | "CURRENCY_WALLET": { 129 | "brief": "Shows the user's wallet which contains information related to the user's currency.", 130 | "usage": "wallet [name/@mention/id]", 131 | "additional": "If no argument is provided, it shows the wallet of the user who queried.", 132 | "admin": false 133 | }, 134 | "CURRENCY_DAILY": { 135 | "brief": "Collects the user's extra credits for the day.", 136 | "usage": "daily", 137 | "additional": "The daily reward resets every 24hrs. Make sure to collect it everytime!", 138 | "admin": false 139 | }, 140 | "CURRENCY_GAMBLE": { 141 | "brief": "Play a game of gamble with the command.", 142 | "usage": "gamble (more details below)", 143 | "additional": "This is a number guessing game which can earn you extra credits. There are two ways to play: \n\n**Normal: **In this mode you use the default bet amount to gamble.\n*Usage:* `!gamble` \n\n**Double or Nothing: ** In this mode you can bet whatever amount under 100 credits.\n *Usage:* `!gamble `", 144 | "admin": false 145 | }, 146 | "CURRENCY_CLAIM": { 147 | "brief": "Claims the giveaway prize (credits) when it comes out.", 148 | "usage": "claim", 149 | "additional": "Don't use it unless the bot notifies everyone of a giveaway!", 150 | "admin": false 151 | }, 152 | "CURRENCY_GIFT": { 153 | "brief": "Accepts a gift on a special day.", 154 | "usage": "gift", 155 | "additional": "You only have 5 minutes to accept each gift... be speedy!", 156 | "admin": false 157 | }, 158 | "CURRENCY_SET_PRIZE_CHANNEL": { 159 | "brief": "Sets the giveaway announcement channel to the current channel in use.", 160 | "usage": "setannouncechan", 161 | "additional": "", 162 | "admin": true 163 | }, 164 | "CURRENCY_SET_NAME": { 165 | "brief": "Sets the currency name.", 166 | "usage": "setcurrenname", 167 | "additional": "", 168 | "admin": true 169 | }, 170 | "CURRENCY_SET_SYMBOL": { 171 | "brief": "Sets the currency symbol.", 172 | "usage": "setcurrensymb", 173 | "additional": "", 174 | "admin": true 175 | }, 176 | "CURRENCY_SET_DAILY": { 177 | "brief": "Sets the daily reward amount.", 178 | "usage": "setdaily", 179 | "additional": "", 180 | "admin": true 181 | }, 182 | "CURRENCY_SET_PRIZE": { 183 | "brief": "Sets the freebie reward amount.", 184 | "usage": "setprize", 185 | "additional": "", 186 | "admin": true 187 | }, 188 | "CURRENCY_SET_GAMBLE_LOSS": { 189 | "brief": "Sets the amount to be taken when the player loses in the normal gamble mode.", 190 | "usage": "setgambleloss", 191 | "additional": "", 192 | "admin": true 193 | }, 194 | "CURRENCY_SET_GAMBLE_REWARD": { 195 | "brief": "Sets the amount to be rewarded when the player wins in the normal gamble mode.", 196 | "usage": "setgamblereward", 197 | "additional": "", 198 | "admin": true 199 | }, 200 | "CURRENCY_SET_GAMBLE_MIN_GUESS": { 201 | "brief": "Sets the minimum guess number for the gamble mode.", 202 | "usage": "setgambleminguess", 203 | "additional": "", 204 | "admin": true 205 | }, 206 | "CURRENCY_SET_GAMBLE_MAX_GUESS": { 207 | "brief": "Sets the maximum guess number for the gamble mode.", 208 | "usage": "setgamblemaxguess", 209 | "additional": "", 210 | "admin": true 211 | }, 212 | "CURRENCY_SET_PRIZE_PROB": { 213 | "brief": "Sets the probability that the freebie giveaway occurs for each message.", 214 | "usage": "setprizeprob", 215 | "additional": "", 216 | "admin": true 217 | }, 218 | "CURRENCY_SET_PRIZE_EXPIRY": { 219 | "brief": "Sets the expriy time (in seconds) for the freebie giveaway at the end of the day (when it has not been claimed).", 220 | "usage": "setprizeexpiry", 221 | "additional": "", 222 | "admin": true 223 | }, 224 | "CURRENCY_RICH_LIST": { 225 | "brief": "Displays a list of the richest people on the server", 226 | "usage": "richlist (List top 30 richest people)\nrichlist (List of richest people until )\nrichlist (List of richest people from to )", 227 | "additional": "Make sure you're in the list!", 228 | "admin": false 229 | }, 230 | "CURRENCY_DONATE": { 231 | "brief": "Distributes an equal amount of credits (specified by you) from your wallet among the people you listed (pinged).", 232 | "usage": "donate ", 233 | "additional": "**Example: ** `!donate 20 @Person1 @Person2` gives both Person1 and Person2 10 credits each." 234 | }, 235 | "CURRENCY_BRIBE": { 236 | "brief": "Chance to get out of the jail by paying a fixed amount, otherwise it will add more time to your sentence", 237 | "usage": "bribe ", 238 | "additional": "**Example: ** `!bribe 60` will give you a chance to get out of the jail and take 60 or add more time to your sentence without taking the money", 239 | "admin": false 240 | }, 241 | "CURRENCY_SET_BRIBE_SUCCESS_CHANCE": { 242 | "brief": "Sets the chance (as a percentage) that a !bribe will succeed.", 243 | "usage": "setbribesuccesschance ", 244 | "admin": true 245 | }, 246 | "CURRENCY_SET_MAX_BRIBE_AMOUNT": { 247 | "brief": "Sets the max amount that a user can `!bribe`.", 248 | "usage": "setmaxbribeamount ", 249 | "admin": true 250 | }, 251 | "CURRENCY_SET_LEAST_BRIBE_AMOUNT": { 252 | "brief": "Sets the least amount that a user can `!bribe`.", 253 | "usage": "setleastbribeamount ", 254 | "admin": true 255 | }, 256 | "CURRENCY_STEAL": { 257 | "brief": "Chance to steal an amount from a user, otherwise it will fine you.", 258 | "usage": "steal ", 259 | "additional": "**Example: ** `!steal 20 @Person1` will try to steal 20 credits from Person 1", 260 | "admin": false 261 | }, 262 | "EVENT_SET_HRHR_STEAL_SUCCESS_CHANCE": { 263 | "brief": "Sets the chance (as a percentage) that a !steal at the time of HighRiskHighReward will succeed.", 264 | "usage": "setHRHRstealsuccesschance ", 265 | "admin": true 266 | }, 267 | "EVENT_SET_LRLR_STEAL_SUCCESS_CHANCE": { 268 | "brief": "Sets the chance (as a percentage) that a !steal at the time of LowRiskLowReward will succeed.", 269 | "usage": "setLRLRstealsuccesschance ", 270 | "admin": true 271 | }, 272 | "CURRENCY_SET_STEAL_SUCCESS_CHANCE": { 273 | "brief": "Sets the chance (as a percentage) that a !steal will succeed.", 274 | "usage": "setstealsuccesschance ", 275 | "admin": true 276 | }, 277 | "CURRENCY_SET_STEAL_FINE_PERCENT": { 278 | "brief": "Sets the amount (as a percentage of what they tried to steal) that the theif will get fined.", 279 | "usage": "setstealfine ", 280 | "admin": true 281 | }, 282 | "CURRENCY_SET_STEAL_VICTIM_BONUS": { 283 | "brief": "Sets the amount (as a percentage of what the theif tried to steal) that the victim will get.", 284 | "usage": "setstealvictimbonus ", 285 | "admin": true 286 | }, 287 | "CURRENCY_SET_STEAL_JAIL_HOURS": { 288 | "brief": "Sets the number of hours that a theif will spend in jail.", 289 | "usage": "setstealjailhours ", 290 | "admin": true 291 | }, 292 | "CURRENCY_SET_DAILY_BONUS_AMOUNT": { 293 | "brief": "Sets the bonus amount for dailies.", 294 | "usage": "setdailybonus ", 295 | "admin": true 296 | }, 297 | "CURRENCY_SET_DAILY_BONUS_PERIOD": { 298 | "brief": "Sets how often (in days) the bonus is granted.", 299 | "usage": "setdailybonusperiod ", 300 | "admin": true 301 | }, 302 | "CURRENCY_COMPENSATE": { 303 | "brief": "Adds some amount to everyone's currency (if only the amount is provided). Intended to be used as a compensation for when the bot is down.", 304 | "usage": "compensate \ncompensate @Person ", 305 | "admin": true 306 | }, 307 | "EVENT": { 308 | "brief": "Retrieve information about the running event.", 309 | "usage": "event", 310 | "admin": false 311 | }, 312 | "EVENT_LAUNCH": { 313 | "brief": "Starts an event.", 314 | "usage": "launch ", 315 | "admin": true 316 | }, 317 | "EVENT_END": { 318 | "brief": "Stops the running event manually.", 319 | "usage": "endevent", 320 | "admin": true 321 | }, 322 | "EVENT_GIVE_NEW_ACCESS": { 323 | "brief": "Gives event launching and ending ability to the people with the mentioned role(s).", 324 | "usage": "give-new-event-access ", 325 | "admin": true 326 | }, 327 | "EVENT_TAKE_NEW_ACCESS": { 328 | "brief": "Removes event launching and ending ability from the provided role(s) that previously had event launching and ending abilities.", 329 | "usage": "take-new-event-access ", 330 | "admin": true 331 | }, 332 | "EVENT_SET_TICKET_PRICE": { 333 | "brief": "Sets the ticket price of the RaffleDraw.", 334 | "usage": "setticketprice ", 335 | "admin": true 336 | }, 337 | "EVENT_SET_USER_MAX_TICKET": { 338 | "brief": "Sets the maximum tickets a user can buy.", 339 | "usage": "setusermaxticket ", 340 | "admin": true 341 | }, 342 | "EVENT_TICKET": { 343 | "brief": "Retrive information about the ticket(s) of the user.", 344 | "usage": "ticket", 345 | "admin": false 346 | }, 347 | "EVENT_BUY_TICKETS": { 348 | "brief": "Buy ticket(s) for the RaffleDraw event", 349 | "usage": "buytickets ", 350 | "additional": "**Example:** `!buyticket 4` will cut the required money and give the user `4` tickets", 351 | "admin": false 352 | }, 353 | "EVENT_GET_REWARD": { 354 | "brief": "Gets the prize of the RaffleDraw event when the lucky ticket is announced.", 355 | "usage": "getreward", 356 | "additional": "Only use this when the lucky ticket is announced and you have the lucky ticket.", 357 | "admin": false 358 | }, 359 | "FUN_MEME": { 360 | "brief": "Shows memes from reddit.", 361 | "usage": "meme", 362 | "admin": false 363 | }, 364 | "FUN_ROLL": { 365 | "brief": "Gets a random number.", 366 | "usage": "roll \nroll ", 367 | "additional": "**Example:\n** `!roll 4 10`: Gets a random number between `4` and `10` (inclusive).\n `!roll 10`: Gets a random number from `0` to `10` (inclusive).", 368 | "admin": false 369 | }, 370 | "FUN_POLL": { 371 | "brief": "Creates a new poll.", 372 | "usage": "poll [options] ...", 373 | "additional": "**Options**: Options facilitate better interaction with the polling system. All options are case-insensitive and some can be used together. The available options are as follows:\n\n`--maxvotes `: The number of votes after which a poll should finish.\n`--hours `: The number of hours a poll should go on for.\n`--list`: To list all the active polls.\n`--name \"\" | `: To provide the name of the poll (this also goes into the poll title if provided).\n`--cancel `: To cancel an active poll.", 374 | "admin": false 375 | }, 376 | "FUN_GITHUB": { 377 | "brief": "Shows popular projects on GitHub", 378 | "usage": "github", 379 | "admin": false 380 | }, 381 | "FUN_GIVE_NEW_POLL_ACCESS": { 382 | "brief": "Gives poll creation ability to the people with the mentioned role(s).", 383 | "usage": "give-new-poll-access ", 384 | "admin": true 385 | }, 386 | "FUN_TAKE_NEW_POLL_ACCESS": { 387 | "brief": "Removes poll creation ability from the provided role(s) that previously had creation abilities.", 388 | "usage": "take-new-poll-access ", 389 | "admin": true 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/modules/FunModule.cpp: -------------------------------------------------------------------------------- 1 | #include "FunModule.h" 2 | #include 3 | #include 4 | #include 5 | #include "core/Permissions.h" 6 | #include "Discord/Patches/MessagePatch.h" 7 | #include 8 | #include 9 | #include 10 | 11 | #define POLL_DEFAULT_TIME 1.0*60.0*60.0 //seconds ~~ 1 hr 12 | #define POLL_MAX_TIME 168.0 //hours ~~ 1 week 13 | #define POLL_MIN_TIME 1.0/60.0 //hours ~~ 1 mins 14 | 15 | using namespace Discord; 16 | 17 | FunModule::FunModule(UmikoBot* client) : Module("funutil", true), m_memeChannel(0), m_client(client) 18 | { 19 | 20 | QObject::connect(&m_MemeManager, &QNetworkAccessManager::finished, 21 | this, [this](QNetworkReply* reply) { 22 | auto& client = UmikoBot::Instance(); 23 | if (reply->error()) { 24 | qDebug() << reply->errorString(); 25 | client.createMessage(m_memeChannel, reply->errorString()); 26 | 27 | return; 28 | } 29 | 30 | QString in = reply->readAll(); 31 | 32 | QJsonDocument doc = QJsonDocument::fromJson(in.toUtf8()); 33 | auto obj = doc.object(); 34 | bool isNsfw = obj["nsfw"].toBool(); 35 | if (isNsfw) { 36 | m_MemeManager.get(QNetworkRequest(QUrl("https://meme-api.com/gimme"))); 37 | return; 38 | } 39 | QString title = obj["title"].toString(); 40 | QString url = obj["url"].toString(); 41 | QString author = obj["author"].toString(); 42 | QString postLink = obj["postLink"].toString(); 43 | QString subreddit = obj["subreddit"].toString(); 44 | 45 | Embed embed; 46 | EmbedImage img; 47 | img.setUrl(url); 48 | embed.setImage(img); 49 | embed.setTitle(title); 50 | EmbedFooter footer; 51 | footer.setText("Post was made by u/" + author + " on r/" + subreddit + ".\nSee the actual post here: " + postLink); 52 | embed.setFooter(footer); 53 | 54 | client.createMessage(m_memeChannel, embed); 55 | }); 56 | 57 | QObject::connect(&m_GithubManager, &QNetworkAccessManager::finished, 58 | this, [this](QNetworkReply* reply) { 59 | auto& client = UmikoBot::Instance(); 60 | 61 | if (reply->error()) { 62 | qDebug() << reply->errorString(); 63 | client.createMessage(m_GithubChannel, reply->errorString()); 64 | 65 | return; 66 | } 67 | 68 | QString in = reply->readAll(); 69 | 70 | QJsonDocument doc = QJsonDocument::fromJson(in.toUtf8()); 71 | auto obj = doc.object(); 72 | 73 | QJsonArray items = obj["items"].toArray(); 74 | 75 | std::random_device device; 76 | std::mt19937 rng(device()); 77 | std::uniform_int_distribution dist(0, items.size()); 78 | 79 | QJsonObject repo = items[dist(rng)].toObject(); 80 | 81 | QString repo_fullname = repo["full_name"].toString(); 82 | QString repo_url = repo["html_url"].toString(); 83 | QString repo_language = repo["language"].toString(); 84 | int repo_stars = repo["stargazers_count"].toInt(); 85 | 86 | Embed embed; 87 | embed.setTitle(repo_fullname); 88 | embed.setDescription("\nStars: " + QString::number(repo_stars) + 89 | "\nLanguage: " + repo_language + "\n" + 90 | repo_url); 91 | 92 | client.createMessage(m_GithubChannel, embed); 93 | }); 94 | 95 | QObject::connect(m_client, &UmikoBot::onMessageReactionAdd, this, &FunModule::onReact); 96 | QObject::connect(m_client, &UmikoBot::onMessageReactionRemove, this, &FunModule::onUnReact); 97 | 98 | RegisterCommand(Commands::FUN_MEME, "meme", [this](Client& client, const Message& message, const Channel& channel) 99 | { 100 | 101 | QStringList args = message.content().split(' '); 102 | 103 | if (args.size() >= 2) { 104 | client.createMessage(message.channelId(), "**Wrong Usage of Command!** "); 105 | return; 106 | } 107 | 108 | m_memeChannel = channel.id(); 109 | m_MemeManager.get(QNetworkRequest(QUrl("https://meme-api.com/gimme"))); 110 | 111 | }); 112 | 113 | RegisterCommand(Commands::FUN_ROLL, "roll", [this](Client& client, const Message& message, const Channel& channel) 114 | { 115 | QStringList args = message.content().split(' '); 116 | 117 | if (args.size() < 2 || args.size() > 3) 118 | { 119 | client.createMessage(message.channelId(), "**Wrong Usage of Command!** "); 120 | return; 121 | } 122 | if (args.size() == 2) 123 | { 124 | double min = 0; 125 | double max = args.at(1).toDouble(); 126 | QRegExp re("[+-]?\\d*\\.?\\d+"); 127 | if (!re.exactMatch(args.at(1))) 128 | { 129 | client.createMessage(message.channelId(), "**You must roll with numbers!**"); 130 | return; 131 | } 132 | if (max > 2147483647 || max < -2147483647) 133 | { 134 | client.createMessage(message.channelId(), "**You can't roll that number!**"); 135 | return; 136 | } 137 | std::random_device rand_device; 138 | std::mt19937 gen(rand_device()); 139 | 140 | if (max < min) 141 | std::swap(min, max); 142 | 143 | std::uniform_int_distribution<> dist(min, max); 144 | 145 | QString text = QString("My Value was: **" + QString::number(dist(gen)) + "**"); 146 | client.createMessage(message.channelId(), text); 147 | return; 148 | } 149 | 150 | if (args.size() == 3) 151 | { 152 | double min = args.at(1).toDouble(); 153 | double max = args.at(2).toDouble(); 154 | QRegExp re("[+-]?\\d*\\.?\\d+"); 155 | if (!re.exactMatch(args.at(1)) || !re.exactMatch(args.at(2))) 156 | { 157 | client.createMessage(message.channelId(), "**You must roll with numbers!**"); 158 | return; 159 | } 160 | if (max > 2147483647 || min > 2147483647 || max < -2147483647 || min < -2147483647) 161 | { 162 | client.createMessage(message.channelId(), "**You can't roll that number!**"); 163 | return; 164 | } 165 | if (args.at(1) == args.at(2)) 166 | { 167 | client.createMessage(message.channelId(), "My Value was: **" + args.at(1) + "**"); 168 | return; 169 | } 170 | 171 | std::random_device rand_device; 172 | std::mt19937 gen(rand_device()); 173 | 174 | if (max < min) 175 | std::swap(min, max); 176 | 177 | std::uniform_int_distribution<> dist(min, max); 178 | 179 | QString text = QString("My Value was: **" + QString::number(dist(gen)) + "**"); 180 | client.createMessage(message.channelId(), text); 181 | } 182 | }); 183 | 184 | 185 | RegisterCommand(Commands::FUN_POLL, "poll", [this](Client& client, const Message& message, const Channel& channel) 186 | { 187 | 188 | QStringList lines = message.content().split('\n'); 189 | 190 | for (auto& line : lines) 191 | { 192 | line = line.trimmed(); 193 | } 194 | 195 | QStringList args = lines.at(0).simplified().split(' '); 196 | 197 | args.pop_front(); //pop the command to just get the args 198 | lines.pop_front(); //the first line is only used for args 199 | 200 | double pollTime = POLL_DEFAULT_TIME; //in seconds 201 | long long maxReacts = -1; 202 | QString pollName = ""; 203 | 204 | bool listPolls = false; 205 | 206 | bool cancelPoll = false; 207 | int cancelPollIndex = -1; 208 | 209 | int pollNum = m_polls[channel.guildId()] == nullptr ? 0 : m_polls[channel.guildId()]->size(); 210 | 211 | auto parseArgs = [&pollTime, &maxReacts, &pollName, &listPolls, &cancelPoll, &cancelPollIndex](const QStringList& args, snowflake_t chan) -> bool 212 | { //bool for success 213 | QRegExp numReg{ "[+]?\\d*\\.?\\d+" }; 214 | QRegExp countReg{ "[+]?\\d*" }; 215 | 216 | bool newPoll = false; 217 | bool listPoll = false; 218 | 219 | for (int i = 0; i < args.size(); i++) 220 | { 221 | auto text = args[i].toLower(); 222 | QString next = ""; 223 | if (i + 1 < args.size()) 224 | { 225 | next = args[i + 1]; 226 | } 227 | 228 | if (text == "--maxvotes") 229 | { 230 | if (listPoll) 231 | { 232 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or list the active polls?**"); 233 | return false; 234 | } 235 | if (cancelPoll) 236 | { 237 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or cancel an existing one?**"); 238 | return false; 239 | } 240 | newPoll = true; 241 | if (next == "" || next.startsWith("--")) 242 | { 243 | UmikoBot::Instance().createMessage(chan, "**No value for maxVotes provided. Go ahead and give me a value don't be lazy.**"); 244 | return false; 245 | } 246 | if (!countReg.exactMatch(next)) 247 | { 248 | UmikoBot::Instance().createMessage(chan, "**What's that value for maxVotes? The value should be a positive number!**\n"); 249 | return false; 250 | } 251 | long long value = next.toLongLong(); 252 | if (value == 0) 253 | { 254 | UmikoBot::Instance().createMessage(chan, "Do you even want your poll to last?"); 255 | return false; 256 | } 257 | maxReacts = value; 258 | ++i; 259 | } 260 | else if (text == "--hours") 261 | { 262 | if (listPoll) 263 | { 264 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or list the active polls?**"); 265 | return false; 266 | } 267 | if (cancelPoll) 268 | { 269 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or cancel an existing one?**"); 270 | return false; 271 | } 272 | newPoll = true; 273 | if (next == "" || next.startsWith("--")) 274 | { 275 | UmikoBot::Instance().createMessage(chan, "**If you want to use the default value for hours, just go ahead and don't type --hours in.\nOh you don't want the default value? Well you gotta type what you want!**"); 276 | return false; 277 | } 278 | if (!numReg.exactMatch(next)) 279 | { 280 | UmikoBot::Instance().createMessage(chan, "**How long should the poll last? Please provide a beatiful number...**"); 281 | return false; 282 | } 283 | double value = next.toDouble()*60.0*60.0; 284 | if (value > POLL_MAX_TIME * 60.0 * 60.0) 285 | { 286 | UmikoBot::Instance().createMessage(chan, "**I mean I want your poll to have a *lasting effect*, but don't you think that's a looong time?**"); 287 | return false; 288 | } 289 | if (value < POLL_MIN_TIME * 60.0 * 60.0) 290 | { 291 | UmikoBot::Instance().createMessage(chan, "**Oh c'mon what is this tiny hour value!?**"); 292 | return false; 293 | } 294 | pollTime = value; 295 | ++i; 296 | } 297 | else if (text == "--name") 298 | { 299 | if (listPoll) 300 | { 301 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or list the active polls?**"); 302 | return false; 303 | } 304 | if (cancelPoll) 305 | { 306 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or cancel an existing one?**"); 307 | return false; 308 | } 309 | newPoll = true; 310 | if (next == "") 311 | { 312 | UmikoBot::Instance().createMessage(chan, "**Sorry, I didn't catch the name. Oh wait! You didn't give me one!**"); 313 | return false; 314 | } 315 | if (next.startsWith('"')) 316 | { 317 | //! Should add the last next? 318 | bool requireNext = true; 319 | do 320 | { 321 | pollName += " " + next; 322 | if (next.endsWith('"')) 323 | { 324 | requireNext = false; 325 | break; 326 | } 327 | if (i+2 >= args.size()) 328 | { 329 | UmikoBot::Instance().createMessage(chan, "**The string you provided never ends.**\nPlease fix that?"); 330 | return false; 331 | } 332 | next = args[(++i) + 1]; 333 | } 334 | while (!next.endsWith('"')); 335 | if (requireNext) 336 | { 337 | pollName += " " + next; 338 | } 339 | 340 | //! remove the quotations 341 | pollName.remove(1, 1); 342 | pollName.chop(1); 343 | } 344 | else pollName = next; 345 | ++i; 346 | } 347 | else if (text == "--list") 348 | { 349 | if (newPoll) 350 | { 351 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or list the active polls?**"); 352 | return false; 353 | } 354 | 355 | if (cancelPoll) 356 | { 357 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to cancel a poll or list the active polls?**"); 358 | return false; 359 | } 360 | 361 | listPoll = true; 362 | } 363 | else if (text == "--cancel") 364 | { 365 | if (newPoll) 366 | { 367 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or cancel an existing poll?**"); 368 | return false; 369 | } 370 | 371 | if (listPoll) 372 | { 373 | UmikoBot::Instance().createMessage(chan, "**I am confused... Do you want to me to make a new poll or cancel an existing poll?**"); 374 | return false; 375 | } 376 | 377 | cancelPoll = true; 378 | 379 | if (next == "" || next.startsWith("--")) { 380 | UmikoBot::Instance().createMessage(chan, "**I expect a poll number following the `--cancel` command so that I can cancel the poll.**\n||I know I am pretty awesome, but I can't (yet) read your mind...||"); 381 | return false; 382 | } 383 | if (!numReg.exactMatch(next)) { 384 | UmikoBot::Instance().createMessage(chan, "**The poll number must be a non-negative number...**"); 385 | return false; 386 | } 387 | 388 | cancelPollIndex = next.toInt(); 389 | 390 | ++i; 391 | } 392 | else 393 | { 394 | UmikoBot::Instance().createMessage(chan, "**I didn't expect this string in the arguments:** " + args[i] + "\nPlease fix that?"); 395 | return false; 396 | } 397 | } 398 | 399 | listPolls = listPoll; 400 | return true; 401 | }; 402 | 403 | if (parseArgs(args, message.channelId())) 404 | { 405 | if (!listPolls && !cancelPoll) 406 | { 407 | 408 | client.getGuildMember(channel.guildId(), message.author().id()) 409 | .then([=](const GuildMember& member) 410 | { 411 | bool found = false; 412 | 413 | for (auto& allowedRole : m_pollWhitelist[channel.guildId()]) 414 | { 415 | if (member.roles().contains(allowedRole)) 416 | { 417 | found = true; 418 | break; 419 | } 420 | } 421 | 422 | if (!found) 423 | { 424 | UmikoBot::Instance().createMessage(channel.id(), "**You're not allowed to create polls.**"); 425 | return; 426 | } 427 | 428 | if (lines.size() == 0) 429 | { 430 | UmikoBot::Instance().createMessage(message.channelId(), "**A poll without anything to vote for huh? C'mon you can do better!**"); 431 | return; 432 | } 433 | if (lines.size() == 1) 434 | { 435 | UmikoBot::Instance().createMessage(message.channelId(), "**Why would there be a poll if there's only one thing to choose?**"); 436 | return; 437 | } 438 | 439 | PollOptions options; 440 | QString desc = "**Here are your options:**\n"; 441 | 442 | QRegExp customEmote{ "<:.+:\\d+>" }; 443 | QRegExp customAnimEmote{ "" }; 444 | 445 | for (auto& option : lines) { 446 | bool isAnimated = false; 447 | QString emoji = ""; 448 | int index = option.indexOf(' '); 449 | emoji = option.mid(0, index); 450 | QString text = ""; 451 | if (index != -1) text = option.mid(index).trimmed(); 452 | 453 | desc += emoji + " : " + text + "\n"; 454 | 455 | QString emojiData = emoji; 456 | 457 | if (customEmote.exactMatch(emojiData)) 458 | { 459 | emojiData = emojiData.remove("<:"); 460 | emojiData = emojiData.remove(">"); 461 | } 462 | else if (customAnimEmote.exactMatch(emojiData)) 463 | { 464 | isAnimated = true; 465 | emojiData = emojiData.remove(""); 467 | } 468 | 469 | //! Check if the current emoji exists already 470 | 471 | for (auto& opt : options) 472 | { 473 | if (opt.emote == emojiData) 474 | { 475 | UmikoBot::Instance().createMessage(message.channelId(), "**You used this reaction choice multiple times:** " + emoji); 476 | return; 477 | } 478 | } 479 | 480 | PollOption opt; 481 | opt.emote = emojiData; 482 | opt.desc = text; 483 | opt.isAnimated = isAnimated; 484 | options.push_back(opt); 485 | } 486 | 487 | if (options.length() > 10) 488 | { 489 | UmikoBot::Instance().createMessage(message.channelId(), "**Woah there! That's a lot of options for me to handle!**"); 490 | return; 491 | } 492 | 493 | desc += "\n**Go ahead! Vote now before the poll ends!**"; 494 | Embed embed; 495 | embed.setColor(qrand() % 16777216); 496 | embed.setTitle("Poll#" + QString::number(pollNum) + " " + pollName); 497 | embed.setDescription(desc); 498 | 499 | snowflake_t guild = channel.guildId(); 500 | 501 | UmikoBot::Instance() 502 | .createMessage(message.channelId(), embed) 503 | .then( 504 | [this, pollNum, options, maxReacts, pollName, 505 | pollTime, guild](const Message& msg) 506 | { 507 | int pos = 0; 508 | if (m_polls[guild] == nullptr) 509 | m_polls[guild] = std::make_shared>(); 510 | 511 | //! Make a new entry for this poll 512 | PollOptions tempOptions; 513 | auto poll = std::make_shared(tempOptions, maxReacts, msg.channelId(), pollName, pollNum, pollTime, m_polls[guild], msg.id()); 514 | 515 | m_polls[guild]->push_back(poll); 516 | 517 | //! This is responsible for adding reactions and adding them into the pollOptions 518 | pollReactAndAdd(options, pos, poll, msg.id(), msg.channelId(), guild); 519 | 520 | }); 521 | 522 | }); 523 | 524 | } 525 | else if (cancelPoll) 526 | { 527 | auto& serverPolls = m_polls[channel.guildId()]; 528 | if (serverPolls != nullptr && !serverPolls->isEmpty()) 529 | { 530 | bool found = false; 531 | 532 | for (int i = 0; i < serverPolls->size(); i++) { 533 | if (serverPolls->at(i)->pollNum == cancelPollIndex) { 534 | auto poll_num = serverPolls->at(i)->pollNum; 535 | auto poll_name = serverPolls->at(i)->pollName; 536 | auto notif_chan = serverPolls->at(i)->notifChannel; 537 | auto poll_msg = serverPolls->at(i)->pollMsg; 538 | serverPolls->removeAt(i); 539 | found = true; 540 | Embed embed; 541 | embed.setColor(qrand() % 16777216); 542 | embed.setTitle("Poll#" + QString::number(poll_num) + " " + poll_name); 543 | embed.setDescription("This poll has been cancelled."); 544 | MessagePatch patch; 545 | patch.setEmbed(embed); 546 | UmikoBot::Instance().deleteAllReactions(notif_chan, poll_msg); 547 | UmikoBot::Instance().editMessage(notif_chan, poll_msg, patch).then([notif_chan, poll_num](const Message&) { 548 | UmikoBot::Instance().createMessage(notif_chan, "**Poll#" + QString::number(poll_num)+ " has been cancelled.**"); 549 | }); 550 | break; 551 | } 552 | } 553 | 554 | if (!found) 555 | { 556 | client.createMessage(channel.id(), "**No active poll with the provided number exists.**"); 557 | } 558 | } 559 | else 560 | { 561 | client.createMessage(channel.id(), "**There are no active polls to cancel.**"); 562 | } 563 | } 564 | else 565 | { 566 | 567 | if (lines.size() != 0) { 568 | client.createMessage(message.channelId(), "**I'm curious; why did you pass me more lines if you just wanted to list the active polls?**"); 569 | return; 570 | } 571 | 572 | Embed embed; 573 | embed.setColor(qrand() % 16777216); 574 | embed.setTitle("List of active polls"); 575 | QList fields; 576 | 577 | auto& serverPolls = m_polls[channel.guildId()]; 578 | 579 | if (serverPolls != nullptr && !serverPolls->isEmpty()) 580 | { 581 | for (auto& poll : *serverPolls) { 582 | 583 | std::size_t total = 0; 584 | for (auto& opt : poll->options) { 585 | total += opt.count; 586 | } 587 | 588 | EmbedField field; 589 | field.setName("Poll#" 590 | + QString::number(poll->pollNum) 591 | + (poll->pollName == "" ? "" : " " + poll->pollName)); 592 | field.setValue("**Remaining Time: `" 593 | + utility::StringifyMilliseconds(poll->timer->remainingTime(), utility::StringMSFormat::MINIMAL) + "`**\n" + "**Total Votes:** `" + QString::number(total) + "`" + "\n[Link](https://discordapp.com/channels/" + QString::number(channel.guildId()) + "/" + QString::number(poll->notifChannel) + "/" + QString::number(poll->pollMsg) + ")"); 594 | 595 | fields.push_back(field); 596 | } 597 | } 598 | else 599 | { 600 | embed.setDescription("No polls active."); 601 | client.createMessage(channel.id(), embed); 602 | return; 603 | } 604 | 605 | embed.setFields(fields); 606 | 607 | client.createMessage(channel.id(), embed); 608 | } 609 | } 610 | 611 | }); 612 | 613 | RegisterCommand(Commands::FUN_GITHUB, "github", [this](Client& client, const Message& message, const Channel& channel) 614 | { 615 | QStringList args = message.content().split(' '); 616 | 617 | m_GithubChannel = channel.id(); 618 | 619 | if(args.size() != 1) 620 | { 621 | UmikoBot::Instance().createMessage(m_GithubChannel, "**Wrong Usage of Command!**"); 622 | 623 | return; 624 | } 625 | 626 | // For extra randomness 627 | std::random_device device; 628 | std::mt19937 rng(device()); 629 | std::uniform_int_distribution ch('A', 'Z'); 630 | 631 | m_GithubManager.get(QNetworkRequest(QUrl("https://api.github.com/search/repositories?q=" + QString(QChar((char)ch(rng)))))); 632 | }); 633 | 634 | RegisterCommand(Commands::FUN_GIVE_NEW_POLL_ACCESS, "give-new-poll-access", [this](Client& client, const Message& message, const Channel& channel) 635 | { 636 | QStringList args = message.content().split(' '); 637 | 638 | ::Permissions::ContainsPermission(client, channel.guildId(), message.author().id(), CommandPermission::MODERATOR, [this, args, message, channel](bool result) 639 | { 640 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 641 | if (!result) 642 | { 643 | UmikoBot::Instance().createMessage(message.channelId(), "**You don't have permissions to use this command.**"); 644 | return; 645 | } 646 | 647 | 648 | if (args.size() < 2) 649 | { 650 | UmikoBot::Instance().createMessage(channel.id(), "**Wrong Usage of Command!**"); 651 | return; 652 | } 653 | 654 | snowflake_t guild = channel.guildId(); 655 | snowflake_t chan = channel.id(); 656 | 657 | auto& roles = message.mentionRoles(); 658 | 659 | if (roles.isEmpty()) { 660 | UmikoBot::Instance().createMessage(channel.id(), "**Wrong Usage of Command!\n**Please mention some role(s) so that I can do the needed."); 661 | return; 662 | } 663 | 664 | for (auto& role : roles) { 665 | if (m_pollWhitelist[guild].contains(role)) continue; 666 | m_pollWhitelist[guild].push_back(role); 667 | } 668 | 669 | UmikoBot::Instance().createMessage(channel.id(), "**The roles have been added.**\nPeople with the role(s) can now create new polls!"); 670 | 671 | }); 672 | 673 | }); 674 | 675 | RegisterCommand(Commands::FUN_TAKE_NEW_POLL_ACCESS, "take-new-poll-access", [this](Client& client, const Message& message, const Channel& channel) 676 | { 677 | QStringList args = message.content().split(' '); 678 | 679 | ::Permissions::ContainsPermission(client, channel.guildId(), message.author().id(), CommandPermission::MODERATOR, [this, args, message, channel](bool result) 680 | { 681 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 682 | if (!result) 683 | { 684 | UmikoBot::Instance().createMessage(message.channelId(), "**You don't have permissions to use this command.**"); 685 | return; 686 | } 687 | 688 | if (args.size() < 2) { 689 | UmikoBot::Instance().createMessage(channel.id(), "**Wrong Usage of Command!**"); 690 | return; 691 | } 692 | 693 | snowflake_t guild = channel.guildId(); 694 | snowflake_t chan = channel.id(); 695 | 696 | auto& roles = message.mentionRoles(); 697 | 698 | if (roles.isEmpty()) 699 | { 700 | UmikoBot::Instance().createMessage(channel.id(), "**Wrong Usage of Command!\n**Please mention some role(s) so that I can do the needed."); 701 | return; 702 | } 703 | 704 | for (auto& role : roles) 705 | { 706 | m_pollWhitelist[guild].removeOne(role); 707 | } 708 | 709 | UmikoBot::Instance().createMessage(channel.id(), "**The roles have been removed.**\nPeople with the role(s) can no longer create new polls."); 710 | 711 | }); 712 | 713 | }); 714 | } 715 | 716 | void FunModule::onReact(snowflake_t user, snowflake_t channel, snowflake_t message, const Emoji& emoji) const 717 | { 718 | auto polls = m_polls; 719 | UmikoBot::Instance().getChannel(channel).then([user, channel, message, emoji, polls](const Channel& chan) 720 | { 721 | auto guild = chan.guildId(); 722 | if (polls[guild] != nullptr) 723 | { 724 | for (auto poll : *polls[guild]) 725 | { 726 | if (poll->pollMsg == message) 727 | { 728 | UmikoBot::Instance().getUser(user).then([channel, message, emoji, poll](const User& user) 729 | { 730 | if (!user.bot()) 731 | { 732 | QString emojiStr = utility::stringifyEmoji(emoji); 733 | bool found = false; 734 | for (auto& option : poll->options) 735 | { 736 | if (option.emote == emojiStr) 737 | { 738 | found = true; 739 | option.count++; 740 | break; 741 | } 742 | } 743 | if (!found) 744 | { 745 | //This reaction wasn't one of the choices 746 | UmikoBot::Instance().deleteUserReaction(channel, message, emojiStr, user.id()); 747 | return; 748 | } 749 | if (poll->maxVotes != -1) 750 | { 751 | std::size_t total = 0; 752 | for (auto it = poll->options.begin(); it != poll->options.end(); ++it) 753 | { 754 | total += it->count; 755 | } 756 | if (total == poll->maxVotes) 757 | { 758 | //! Manually invoke timeout for the poll timer 759 | poll->timer->QTimer::qt_metacall(QMetaObject::InvokeMetaMethod, 5, {}); 760 | } 761 | } 762 | } 763 | }); 764 | } 765 | } 766 | } 767 | 768 | }); 769 | 770 | } 771 | 772 | void FunModule::onUnReact(snowflake_t user, snowflake_t channel, snowflake_t message, const Emoji& emoji) const 773 | { 774 | auto polls = m_polls; 775 | UmikoBot::Instance().getChannel(channel) 776 | .then([user, channel, message, emoji, polls](const Channel& chan) 777 | { 778 | auto guild = chan.guildId(); 779 | if (polls[guild] != nullptr) 780 | { 781 | for (auto poll : *polls[guild]) 782 | { 783 | if (poll->pollMsg == message) 784 | { 785 | UmikoBot::Instance().getUser(user).then([channel, message, emoji, poll](const User& user) 786 | { 787 | if (!user.bot()) 788 | { 789 | QString emojiStr = utility::stringifyEmoji(emoji); 790 | for (auto& option : poll->options) 791 | { 792 | if (option.emote == emojiStr) 793 | { 794 | option.count--; 795 | break; 796 | } 797 | } 798 | } 799 | }); 800 | } 801 | } 802 | } 803 | 804 | }); 805 | } 806 | 807 | void FunModule::OnMessage(Client& client, const Message& message) 808 | { 809 | for (auto& usr : message.mentions()) 810 | { 811 | if (usr.bot()) 812 | { 813 | snowflake_t chan = message.channelId(); 814 | UmikoBot::Instance().createReaction(chan, message.id(), utility::consts::emojis::reacts::ANGRY_PING) 815 | .then([chan] 816 | { 817 | UmikoBot::Instance().triggerTypingIndicator(chan); 818 | }); 819 | return; 820 | } 821 | } 822 | 823 | Module::OnMessage(client, message); 824 | } 825 | 826 | void FunModule::pollReactAndAdd(const PollOptions& options, int pos, const Poll& poll, snowflake_t msg, snowflake_t chan, snowflake_t guild) 827 | { 828 | 829 | if (pos == options.size()) return; 830 | 831 | UmikoBot::Instance() 832 | .createReaction(chan, msg, options.at(pos).emote) 833 | .then([this, pos, options, poll, msg, chan, guild] 834 | { 835 | //! Add the reaction if success 836 | poll->options.push_back(options.at(pos)); 837 | 838 | //! Continue with the next reaction 839 | pollReactAndAdd(options, pos + 1, poll, msg, chan, guild); 840 | 841 | }) 842 | .otherwise([this, pos, options, poll, msg, chan, guild]() { 843 | //! Remove the poll from the pending list if we couldn't 844 | //! add all reactions 845 | for (int i = 0; i < m_polls[guild]->size(); i++) { 846 | if (m_polls[guild]->at(i)->pollNum == i) { 847 | m_polls[guild]->removeAt(i); 848 | break; 849 | } 850 | } 851 | UmikoBot::Instance().deleteMessage(chan, msg); 852 | UmikoBot::Instance().createMessage(chan, "**I'm sorry, but one (or more) of the vote options wasn't a reaction.**\nYou might wanna fix that. ||Unless you were trolling, and if that's the case, haha! I am well built!||"); 853 | return; 854 | }); 855 | } 856 | 857 | void FunModule::OnSave(QJsonDocument& doc) const 858 | { 859 | QJsonObject docObj; 860 | 861 | for (auto& server : m_pollWhitelist.keys()) 862 | { 863 | QJsonObject serverJSON; 864 | 865 | QJsonArray list; 866 | 867 | for (auto& roleId : m_pollWhitelist[server]) 868 | { 869 | list.push_back(QString::number(roleId)); 870 | } 871 | 872 | serverJSON["poll-whitelist"] = list; 873 | 874 | docObj[QString::number(server)] = serverJSON; 875 | } 876 | 877 | doc.setObject(docObj); 878 | } 879 | 880 | void FunModule::OnLoad(const QJsonDocument& doc) 881 | { 882 | m_pollWhitelist.clear(); 883 | 884 | auto docObj = doc.object(); 885 | 886 | auto servers = docObj.keys(); 887 | 888 | for (auto& server : servers) 889 | { 890 | snowflake_t guild = server.toULongLong(); 891 | auto serverObj = docObj[server].toObject(); 892 | auto list = serverObj["poll-whitelist"].toArray(); 893 | 894 | for (auto role : list) 895 | { 896 | snowflake_t roleId = role.toString().toULongLong(); 897 | m_pollWhitelist[guild].push_back(roleId); 898 | } 899 | } 900 | } 901 | -------------------------------------------------------------------------------- /src/modules/EventModule.cpp: -------------------------------------------------------------------------------- 1 | #include "UmikoBot.h" 2 | #include "EventModule.h" 3 | #include "core/Permissions.h" 4 | #include "core/Utility.h" 5 | #include "CurrencyModule.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define eventConfigLoc QString("eventConfig") 13 | 14 | using namespace Discord; 15 | EventModule::EventModule(UmikoBot* client) : Module("event", true) 16 | { 17 | RegisterCommand(Commands::EVENT, "event", [this](Client& client, const Message& message, const Channel& channel) 18 | { 19 | QStringList args = message.content().split(' '); 20 | auto& config = getServerEventData(channel.guildId()); 21 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 22 | auto& currencyConfig = currencyModule->getServerData(channel.guildId()); 23 | 24 | if (args.size() != 1) 25 | { 26 | client.createMessage(message.channelId(), "**Wrong Usage of Command!** "); 27 | return; 28 | } 29 | if (!config.isEventRunning) 30 | { 31 | client.createMessage(message.channelId(), "**No event is active at this moment.**"); 32 | return; 33 | } 34 | if (config.isHighRiskHighRewardRunning) 35 | { 36 | int num = config.eventTimer->remainingTime(); 37 | QString time = utility::StringifyMilliseconds(num); 38 | 39 | Embed embed; 40 | embed.setColor(15844367); 41 | embed.setTitle("High Risk High Reward event"); 42 | embed.setDescription("The steal chance is **decreased** but if you succeed you will get **BONUS " + currencyConfig.currencySymbol + "**!\n" 43 | "Event ends in `" + time + "`"); 44 | 45 | client.createMessage(message.channelId(), embed); 46 | return; 47 | 48 | } 49 | if (config.isLowRiskLowRewardRunning) 50 | { 51 | int num = config.eventTimer->remainingTime(); 52 | QString time = utility::StringifyMilliseconds(num); 53 | 54 | Embed embed; 55 | embed.setColor(15844367); 56 | embed.setTitle("Low Risk Low Reward event"); 57 | embed.setDescription("The steal chance is **increased** but you have to pay a **PENALTY** if you succeed!\n" 58 | "Event ends in `" + time + "`"); 59 | 60 | client.createMessage(message.channelId(), embed); 61 | return; 62 | } 63 | if (config.eventRaffleDrawRunning) 64 | { 65 | int num = config.eventTimer->remainingTime(); 66 | QString time = utility::StringifyMilliseconds(num); 67 | 68 | Embed embed; 69 | embed.setColor(15844367); 70 | embed.setTitle("Raffle Draw event"); 71 | embed.setDescription("You can buy tickets and earn cool rewards if you win!\n" 72 | "The result of the draw is announced when the event expires.\n" 73 | "Use `!buytickets ` to buy ticket for participating.\n" 74 | "Buying more tickets increases your success chance!\n" 75 | "Event ends in `" + time + "`"); 76 | 77 | client.createMessage(message.channelId(), embed); 78 | } 79 | 80 | }); 81 | RegisterCommand(Commands::EVENT_GIVE_NEW_ACCESS, "give-new-event-access", [this](Client& client, const Message& message, const Channel& channel) 82 | { 83 | QStringList args = message.content().split(' '); 84 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, channel, message]() 85 | { 86 | snowflake_t guild = channel.guildId(); 87 | auto& roles = message.mentionRoles(); 88 | 89 | if (roles.isEmpty()) 90 | { 91 | UmikoBot::Instance().createMessage(channel.id(), "**Wrong Usage of Command!\n**Please mention some role(s) so that I can add them."); 92 | return; 93 | } 94 | for (auto& role : roles) 95 | { 96 | if (serverEventConfig[guild].roleWhiteList.contains(role)) continue; 97 | serverEventConfig[guild].roleWhiteList.push_back(role); 98 | } 99 | 100 | UmikoBot::Instance().createMessage(channel.id(), "**The roles have been added.**\nPeople with the role(s) can now launch & end events!"); 101 | }); 102 | }); 103 | RegisterCommand(Commands::EVENT_TAKE_NEW_ACCESS, "take-new-event-access", [this](Client& client, const Message& message, const Channel& channel) 104 | { 105 | QStringList args = message.content().split(' '); 106 | 107 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, channel, message]() 108 | { 109 | snowflake_t guild = channel.guildId(); 110 | auto& roles = message.mentionRoles(); 111 | 112 | if (roles.isEmpty()) 113 | { 114 | UmikoBot::Instance().createMessage(channel.id(), "**Wrong Usage of Command!\n**Please mention some role(s) so that I can remove them."); 115 | return; 116 | } 117 | 118 | for (auto& role : roles) 119 | { 120 | serverEventConfig[guild].roleWhiteList.removeOne(role); 121 | } 122 | 123 | UmikoBot::Instance().createMessage(channel.id(), "**The roles have been removed.**\nPeople with the role(s) can no longer launch or end events!"); 124 | }); 125 | 126 | }); 127 | RegisterCommand(Commands::EVENT_LAUNCH, "launch", [this](Client& client, const Message& message, const Channel& channel) 128 | { 129 | QStringList args = message.content().split(' '); 130 | 131 | client.getGuildMember(channel.guildId(), message.author().id()) 132 | .then([=](const GuildMember& member) 133 | { 134 | bool found = false; 135 | 136 | for (auto& allowedRole : serverEventConfig[channel.guildId()].roleWhiteList) 137 | { 138 | if (member.roles().contains(allowedRole)) 139 | { 140 | found = true; 141 | break; 142 | } 143 | } 144 | 145 | if (!found) 146 | { 147 | UmikoBot::Instance().createMessage(channel.id(), "**You don't have permissions to launch events.**"); 148 | return; 149 | } 150 | 151 | auto& config = getServerEventData(channel.guildId()); 152 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 153 | auto& currencyConfig = currencyModule->getServerData(channel.guildId()); 154 | 155 | if (args.size() != 3) 156 | { 157 | UmikoBot::Instance().createMessage(message.channelId(), "**Wrong Usage of Command!** "); 158 | return; 159 | } 160 | 161 | if (config.isEventRunning) 162 | { 163 | UmikoBot::Instance().createMessage(message.channelId(), "An event is already running. Please stop that to start a new event!"); 164 | return; 165 | } 166 | 167 | double time = args.at(2).toDouble(); 168 | 169 | QRegExp re("[+]?\\d*\\.?\\d+"); 170 | if (!re.exactMatch(args.at(2))) 171 | { 172 | UmikoBot::Instance().createMessage(message.channelId(), "**Wrong Usage of Command!**."); 173 | return; 174 | } 175 | if (time > static_cast(24 * 24)) 176 | { 177 | UmikoBot::Instance().createMessage(message.channelId(), "**Events cannot last longer than 24 days!**."); 178 | return; 179 | } 180 | if (config.eventTimer != nullptr) //Delete the previous timer 181 | { 182 | delete config.eventTimer; 183 | config.eventTimer = nullptr; 184 | } 185 | config.eventTimer = new QTimer; 186 | 187 | config.eventTimer->setInterval(time * 3600000); 188 | config.eventTimer->setSingleShot(true); 189 | 190 | bool foundEvent = (std::find(eventNamesAndCodes.begin(), eventNamesAndCodes.end(), args.at(1)) != eventNamesAndCodes.end()); 191 | 192 | if (!foundEvent) 193 | { 194 | UmikoBot::Instance().createMessage(message.channelId(), "`" + args.at(1) + "` is not an event name or code!"); 195 | return; 196 | } 197 | QString eventEmoji = QString(utility::consts::emojis::REGIONAL_INDICATOR_E) + " " +QString(utility::consts::emojis::REGIONAL_INDICATOR_V) + " " + 198 | QString(utility::consts::emojis::REGIONAL_INDICATOR_E) + " " + QString(utility::consts::emojis::REGIONAL_INDICATOR_N) + " " + 199 | QString(utility::consts::emojis::REGIONAL_INDICATOR_T); 200 | 201 | if (args.at(1) == "HRHR") 202 | { 203 | QString num = QString::number(time); 204 | 205 | Embed embed; 206 | embed.setColor(15844367); 207 | embed.setTitle(eventEmoji); 208 | embed.setDescription("**HighRiskHighReward** event has been launched for `" + num + "` hour(s)!\nGive command `!event` to see what event is running and its changes!"); 209 | 210 | config.isEventRunning = true; 211 | config.isHighRiskHighRewardRunning = true; 212 | UmikoBot::Instance().createMessage(message.channelId(), embed); 213 | } 214 | if (args.at(1) == "LRLR") 215 | { 216 | QString num = QString::number(time); 217 | 218 | Embed embed; 219 | embed.setColor(15844367); 220 | embed.setTitle(eventEmoji); 221 | embed.setDescription("**LowRiskLowReward** event has been launched for `" + num + "` hour(s)!\nUse `!event` to see what event is running and its changes!"); 222 | 223 | config.isEventRunning = true; 224 | config.isLowRiskLowRewardRunning = true; 225 | UmikoBot::Instance().createMessage(message.channelId(), embed); 226 | } 227 | if (args.at(1) == "RaffleDraw") 228 | { 229 | if(config.claimedReward) 230 | { 231 | if (config.raffleDrawRewardClaimTimer != nullptr) //Delete the previous timer 232 | { 233 | delete config.raffleDrawRewardClaimTimer; 234 | config.raffleDrawRewardClaimTimer = nullptr; 235 | } 236 | auto& configRD = raffleDrawGuildList[channel.guildId()][raffleDrawGetUserIndex(channel.guildId(), message.author().id())]; 237 | QString num = QString::number(time); 238 | Embed embed; 239 | embed.setColor(15844367); 240 | embed.setTitle(eventEmoji); 241 | embed.setDescription("**RAFFLE DRAW** event has been launched for `" + num + "` hour(s)!\nUse `!event` to see what event is running and its changes!"); 242 | config.claimedReward = false; 243 | config.isEventRunning = true; 244 | config.eventRaffleDrawRunning = true; 245 | UmikoBot::Instance().createMessage(message.channelId(), embed); 246 | } 247 | else 248 | { 249 | auto& config = getServerEventData(channel.guildId()); 250 | int timeLeft = config.raffleDrawRewardClaimTimer->remainingTime(); 251 | QString time = utility::StringifyMilliseconds(timeLeft); 252 | UmikoBot::Instance().createMessage(message.channelId(), "**The winner of the previous RaffleDraw hasn't claimed their rewards yet.**\n" 253 | "Please wait `"+ time +"` to start RaffleDraw again."); 254 | return; 255 | } 256 | 257 | } 258 | auto guildID = channel.guildId(); 259 | auto chan = message.channelId(); 260 | auto authorId = message.author().id(); 261 | 262 | QObject::connect(config.eventTimer, &QTimer::timeout, [this, guildID, chan, authorId, eventEmoji]() 263 | { 264 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 265 | auto& currencyConfig = currencyModule->getServerData(guildID); 266 | 267 | auto& config = getServerEventData(guildID); 268 | if (config.isHighRiskHighRewardRunning) 269 | { 270 | EndEvent(chan, guildID, authorId, true, "HRHR"); 271 | } 272 | else if (config.isLowRiskLowRewardRunning) 273 | { 274 | EndEvent(chan, guildID, authorId, true, "LRLR"); 275 | } 276 | if (config.eventRaffleDrawRunning) 277 | { 278 | EndEvent(chan, guildID, authorId, true, "RaffleDraw"); 279 | } 280 | }); 281 | config.eventTimer->start(); 282 | }); 283 | }); 284 | RegisterCommand(Commands::EVENT_END, "endevent", [this](Client& client, const Message& message, const Channel& channel) 285 | { 286 | QStringList args = message.content().split(' '); 287 | 288 | client.getGuildMember(channel.guildId(), message.author().id()) 289 | .then([=](const GuildMember& member) 290 | { 291 | bool found = false; 292 | 293 | for (auto& allowedRole : serverEventConfig[channel.guildId()].roleWhiteList) 294 | { 295 | if (member.roles().contains(allowedRole)) 296 | { 297 | found = true; 298 | break; 299 | } 300 | } 301 | 302 | if (!found) 303 | { 304 | UmikoBot::Instance().createMessage(channel.id(), "**You don't have permissions to end events.**"); 305 | return; 306 | } 307 | auto& config = getServerEventData(channel.guildId()); 308 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 309 | auto& currencyConfig = currencyModule->getServerData(channel.guildId()); 310 | 311 | QString eventEmoji = QString(utility::consts::emojis::REGIONAL_INDICATOR_E) + " " + QString(utility::consts::emojis::REGIONAL_INDICATOR_V) + " " + 312 | QString(utility::consts::emojis::REGIONAL_INDICATOR_E) + " " + QString(utility::consts::emojis::REGIONAL_INDICATOR_N) + " " + 313 | QString(utility::consts::emojis::REGIONAL_INDICATOR_T); 314 | 315 | if (args.size() != 1) 316 | { 317 | UmikoBot::Instance().createMessage(message.channelId(), "**Wrong Usage of Command!** "); 318 | return; 319 | } 320 | if (!config.isEventRunning) 321 | { 322 | UmikoBot::Instance().createMessage(message.channelId(), "**What do I even end?**\n" 323 | "I (unlike you) can properly keep track of things, and no events are going on at the moment."); 324 | return; 325 | } 326 | if (config.eventTimer->isActive()) 327 | { 328 | if (config.isHighRiskHighRewardRunning) 329 | { 330 | EndEvent(message.channelId(), channel.guildId(), message.author().id(), false, "HRHR"); 331 | } 332 | if (config.isLowRiskLowRewardRunning) 333 | { 334 | EndEvent(message.channelId(), channel.guildId(), message.author().id(), false, "LRLR"); 335 | } 336 | if (config.eventRaffleDrawRunning) 337 | { 338 | EndEvent(message.channelId(), channel.guildId(), message.author().id(), false, "RaffleDraw"); 339 | } 340 | } 341 | 342 | }); 343 | }); 344 | RegisterCommand(Commands::EVENT_GET_REWARD, "getreward", [this](Client& client, const Message& message, const Channel& channel) 345 | { 346 | QStringList args = message.content().split(' '); 347 | if (args.size() != 1) 348 | { 349 | client.createMessage(message.channelId(), "**Wrong Usage of Command!** "); 350 | return; 351 | } 352 | auto& config = getServerEventData(channel.guildId()); 353 | if (config.isEventRunning) 354 | { 355 | client.createMessage(message.channelId(), "**You can't get rewards anytime!**"); 356 | return; 357 | } 358 | auto& configRD = raffleDrawGuildList[channel.guildId()][raffleDrawGetUserIndex(channel.guildId(), message.author().id())]; 359 | if (!config.claimedReward) 360 | { 361 | if(message.author().id() == config.luckyUser) 362 | { 363 | if (config.raffleDrawRewardClaimTimer->isActive()) 364 | { 365 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 366 | auto& currencyConfig = currencyModule->getServerData(channel.guildId()); 367 | auto& authorCurrency = currencyModule->guildList[channel.guildId()][currencyModule->getUserIndex(channel.guildId(), message.author().id())]; 368 | QString authorName = UmikoBot::Instance().GetName(channel.guildId(), message.author().id()); 369 | config.claimedReward = true; 370 | Embed embed; 371 | embed.setColor(15844367); 372 | authorCurrency.setCurrency(authorCurrency.currency() + 400.0); 373 | embed.setTitle("**Raffle Draw Reward goes to " + authorName + "!**"); 374 | embed.setDescription("Congratulations **" + authorName + "**!\n" 375 | "You just got `400 " + currencyConfig.currencySymbol + "`"); 376 | config.raffleDrawRewardClaimTimer->stop(); 377 | client.createMessage(message.channelId(), embed); 378 | return; 379 | } 380 | } 381 | else if (config.raffleDrawRewardClaimTimer->isActive()) 382 | { 383 | client.createMessage(message.channelId(), "**BRUH, you don't have the lucky ticket!**"); 384 | return; 385 | } 386 | } 387 | else 388 | { 389 | client.createMessage(message.channelId(), "**You can't get rewards anytime!**"); 390 | } 391 | }); 392 | RegisterCommand(Commands::EVENT_BUY_TICKETS, "buytickets", [this](Client& client, const Message& message, const Channel& channel) 393 | { 394 | QStringList args = message.content().split(' '); 395 | auto& config = getServerEventData(channel.guildId()); 396 | if (!config.eventRaffleDrawRunning) 397 | { 398 | client.createMessage(message.channelId(), "**RAFFLE DRAW** in not running. You can buy tickets when **RAFFLE DRAW** starts!"); 399 | return; 400 | } 401 | if (args.size() != 2) 402 | { 403 | client.createMessage(message.channelId(), "**Wrong Usage of Command!** "); 404 | return; 405 | } 406 | QRegExp re("[+]?\\d*\\.?\\d+"); 407 | if (!re.exactMatch(args.at(1))) 408 | { 409 | client.createMessage(message.channelId(), "**You can't buy ticket(s) in invalid amounts**"); 410 | return; 411 | } 412 | unsigned int ticket = args.at(1).toUInt(); 413 | snowflake_t authorID = message.author().id(); 414 | auto& configRD = raffleDrawGuildList[channel.guildId()][raffleDrawGetUserIndex(channel.guildId(), authorID)]; 415 | 416 | if ((ticket > (config.maxUserTickets - configRD.m_TicketIds.size())) || (configRD.m_TicketIds.size() >= config.maxUserTickets)) 417 | { 418 | QString num = QString::number(config.maxUserTickets); 419 | client.createMessage(message.channelId(), "**Don't be selfish!**\nLet others also have a go at some tickets. You can only have " + num + " tickets at max!"); 420 | return; 421 | } 422 | if (ticket == 0) 423 | { 424 | client.createMessage(message.channelId(), "**BrUh, don't bother me if you don't want to buy ticket..**"); 425 | return; 426 | } 427 | snowflake_t chan = message.channelId(); 428 | double totalFee = (static_cast(config.raffleDrawTicketPrice) * ticket); 429 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 430 | auto& authorCurrency = currencyModule->guildList[channel.guildId()][currencyModule->getUserIndex(channel.guildId(), authorID)]; 431 | 432 | if (authorCurrency.currency() <= 0.0) 433 | { 434 | client.createMessage(message.channelId(), "**Haha you poor bruh. Git gud enough to afford tickets.**"); 435 | return; 436 | } 437 | if (totalFee > (double)authorCurrency.currency()) 438 | { 439 | client.createMessage(message.channelId(), "**Can you afford that many tickets?**"); 440 | return; 441 | } 442 | for (int i = 0; i != ticket; i++) 443 | { 444 | config.numTicketsBought++; 445 | configRD.m_TicketIds.push_back(config.numTicketsBought); 446 | } 447 | 448 | authorCurrency.setCurrency(authorCurrency.currency() - totalFee); 449 | QString ticketEmoji = QString(utility::consts::emojis::TICKETS); 450 | QString ticketAmount = QString::number(configRD.m_TicketIds.size()); 451 | client.createMessage(message.channelId(), ticketEmoji + " **You own** `" + ticketAmount + "` **ticket(s) now!**\n *(Buy more of them to increase your chance)*"); 452 | }); 453 | RegisterCommand(Commands::EVENT_TICKET, "ticket", [this](Client& client, const Message& message, const Channel& channel) 454 | { 455 | GuildSetting setting = GuildSettings::GetGuildSetting(channel.guildId()); 456 | QStringList args = message.content().split(' '); 457 | if (args.size() != 1) 458 | { 459 | client.createMessage(message.channelId(), "**Wrong Usage of Command!** "); 460 | return; 461 | } 462 | auto& configRD = raffleDrawGuildList[channel.guildId()][raffleDrawGetUserIndex(channel.guildId(), message.author().id())]; 463 | auto& config = getServerEventData(channel.guildId()); 464 | if (configRD.m_TicketIds.size() == 0) 465 | { 466 | client.createMessage(message.channelId(), "**You have no tickets.**\nYou can buy them by using `" + setting.prefix + "buytickets `"); 467 | return; 468 | } 469 | QString name = UmikoBot::Instance().GetName(channel.guildId(), message.author().id()); 470 | QString totalTicket = QString::number(configRD.m_TicketIds.size()); 471 | QString ticketEmoji = QString(utility::consts::emojis::TICKETS); 472 | QString ticketAmount = QString::number(configRD.m_TicketIds.size()); 473 | QString txt = QString(ticketEmoji + "Total Ticket(s) belonging to **%1**: `%2`\nYour ticket number(s) are:\n").arg(name, totalTicket); 474 | 475 | for (auto ticket : configRD.m_TicketIds) 476 | { 477 | if (ticket == configRD.m_TicketIds.last()) 478 | { 479 | txt += QString("%1").arg(ticket); 480 | client.createMessage(message.channelId(), txt); 481 | return; 482 | } 483 | txt += QString("%1, ").arg(ticket); 484 | } 485 | }); 486 | RegisterCommand(Commands::EVENT_SET_TICKET_PRICE, "setticketprice", [this](Client& client, const Message& message, const Channel& channel) 487 | { 488 | QStringList args = message.content().split(' '); 489 | 490 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, true, [this, &client, channel, message, args]() 491 | { 492 | auto& config = getServerEventData(channel.guildId()); 493 | config.raffleDrawTicketPrice = args.at(1).toDouble(); 494 | client.createMessage(message.channelId(), "Raffle Draw ticket price set to **" + QString::number(config.raffleDrawTicketPrice) + "**"); 495 | }); 496 | }); 497 | RegisterCommand(Commands::EVENT_SET_USER_MAX_TICKET, "setusermaxticket", [this](Client& client, const Message& message, const Channel& channel) 498 | { 499 | QStringList args = message.content().split(' '); 500 | 501 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, true, [this, &client, channel, message, args]() 502 | { 503 | auto& config = getServerEventData(channel.guildId()); 504 | config.maxUserTickets = args.at(1).toDouble(); 505 | client.createMessage(message.channelId(), "Max ticket set to **" + QString::number(config.maxUserTickets) + "**"); 506 | }); 507 | }); 508 | } 509 | 510 | void EventModule::OnMessage(Client& client, const Message& message) 511 | { 512 | Module::OnMessage(client, message); 513 | } 514 | 515 | void EventModule::OnSave(QJsonDocument& doc) const 516 | { 517 | QJsonObject docObj; 518 | 519 | for (auto server : raffleDrawGuildList.keys()) 520 | { 521 | QJsonObject serverJSON; 522 | 523 | for (auto user = raffleDrawGuildList[server].begin(); user != raffleDrawGuildList[server].end(); user++) 524 | { 525 | QJsonObject obj; 526 | QJsonArray list; 527 | 528 | for (auto ticket : user->m_TicketIds) 529 | { 530 | list.push_back(QString::number(ticket)); 531 | } 532 | obj["tickets"] = list; 533 | serverJSON[QString::number(user->m_UserId)] = obj; 534 | } 535 | 536 | docObj[QString::number(server)] = serverJSON; 537 | } 538 | doc.setObject(docObj); 539 | 540 | QFile eventConfigfile("configs/" + eventConfigLoc + ".json"); 541 | if (eventConfigfile.open(QFile::ReadWrite | QFile::Truncate)) 542 | { 543 | QJsonDocument doc; 544 | QJsonObject serverList; 545 | for (auto server : serverEventConfig.keys()) 546 | { 547 | QJsonObject obj; 548 | auto config = serverEventConfig[server]; 549 | 550 | QJsonArray roleWhitelistArray {}; 551 | for (auto& role : serverEventConfig[server].roleWhiteList) 552 | { 553 | roleWhitelistArray.push_back(QString::number(role)); 554 | } 555 | 556 | obj["role-whitelist"] = roleWhitelistArray; 557 | obj["raffleDrawTicketPrice"] = QString::number(config.raffleDrawTicketPrice); 558 | obj["maxUserTickets"] = QString::number(config.maxUserTickets); 559 | obj["currentTicketIndex"] = QString::number(config.numTicketsBought); 560 | serverList[QString::number(server)] = obj; 561 | } 562 | doc.setObject(docObj); 563 | 564 | doc.setObject(serverList); 565 | eventConfigfile.write(doc.toJson()); 566 | eventConfigfile.close(); 567 | } 568 | } 569 | void EventModule::OnLoad(const QJsonDocument& doc) 570 | { 571 | QJsonObject docObj = doc.object(); 572 | QStringList servers = docObj.keys(); 573 | 574 | raffleDrawGuildList.clear(); 575 | 576 | for (auto server : servers) 577 | { 578 | auto guildId = server.toULongLong(); 579 | auto obj = docObj[server].toObject(); 580 | QStringList users = obj.keys(); 581 | 582 | QList list; 583 | for (auto user : users) 584 | { 585 | QJsonArray ticketList = obj[user].toObject()["tickets"].toArray(); 586 | RaffleDraw raffleDrawData 587 | { 588 | user.toULongLong() 589 | }; 590 | for (auto ticket : ticketList) 591 | { 592 | unsigned int ticketId = ticket.toString().toUInt(); 593 | raffleDrawData.m_TicketIds.push_back(ticketId); 594 | } 595 | list.append(raffleDrawData); 596 | } 597 | raffleDrawGuildList.insert(guildId, list); 598 | 599 | } 600 | QFile eventConfigfile("configs/" + eventConfigLoc + ".json"); 601 | if (eventConfigfile.open(QFile::ReadOnly)) 602 | { 603 | QJsonDocument d = QJsonDocument::fromJson(eventConfigfile.readAll()); 604 | QJsonObject rootObj = d.object(); 605 | 606 | serverEventConfig.clear(); 607 | auto servers = rootObj.keys(); 608 | 609 | for (const auto& server : servers) 610 | { 611 | EventConfig config; 612 | auto guildId = server.toULongLong(); 613 | auto serverObj = rootObj[server].toObject(); 614 | 615 | config.raffleDrawTicketPrice = serverObj["raffleDrawTicketPrice"].toString("50").toInt(); 616 | config.numTicketsBought = serverObj["currentTicketIndex"].toString("0").toInt(); 617 | config.maxUserTickets = serverObj["maxUserTickets"].toString("20").toInt(); 618 | 619 | serverEventConfig[guildId].roleWhiteList.clear(); 620 | auto list = serverObj["role-whitelist"].toArray(); 621 | 622 | for (auto role : list) 623 | { 624 | snowflake_t roleId = role.toString().toULongLong(); 625 | config.roleWhiteList.push_back(roleId); 626 | } 627 | 628 | serverEventConfig.insert(guildId, config); 629 | } 630 | eventConfigfile.close(); 631 | } 632 | } 633 | 634 | void EventModule::EndEvent(const snowflake_t& channelID, const snowflake_t& guildID, const snowflake_t& authorID, bool isInQObjectConnect, QString eventNameOrCode) 635 | { 636 | QString eventEmoji = QString(utility::consts::emojis::REGIONAL_INDICATOR_E) + " " + QString(utility::consts::emojis::REGIONAL_INDICATOR_V) + " " + 637 | QString(utility::consts::emojis::REGIONAL_INDICATOR_E) + " " + QString(utility::consts::emojis::REGIONAL_INDICATOR_N) + " " + 638 | QString(utility::consts::emojis::REGIONAL_INDICATOR_T); 639 | 640 | auto& config = getServerEventData(guildID); 641 | CurrencyModule* currencyModule = static_cast(UmikoBot::Instance().GetModuleByName("currency")); 642 | auto& currencyConfig = currencyModule->getServerData(guildID); 643 | 644 | if (eventNameOrCode == "HRHR") 645 | { 646 | Embed embed; 647 | embed.setColor(15158332); 648 | embed.setTitle(eventEmoji); 649 | embed.setDescription("**HighRiskHighReward** event has **ended**!"); 650 | config.isEventRunning = false; 651 | config.isHighRiskHighRewardRunning = false; 652 | if(!isInQObjectConnect) 653 | config.eventTimer->stop(); 654 | 655 | UmikoBot::Instance().createMessage(channelID, embed); 656 | return; 657 | } 658 | if (eventNameOrCode == "LRLR") 659 | { 660 | Embed embed; 661 | embed.setColor(15158332); 662 | embed.setTitle(eventEmoji); 663 | embed.setDescription("**LowRiskLowReward** event has **ended**!"); 664 | 665 | config.isEventRunning = false; 666 | config.isLowRiskLowRewardRunning = false; 667 | if (!isInQObjectConnect) 668 | config.eventTimer->stop(); 669 | UmikoBot::Instance().createMessage(channelID, embed); 670 | return; 671 | } 672 | if(eventNameOrCode == "RaffleDraw") 673 | { 674 | Embed embed; 675 | embed.setColor(15844367); 676 | embed.setTitle(eventEmoji); 677 | 678 | if (config.numTicketsBought == 0) 679 | { 680 | embed.setDescription("**RAFFLE DRAW** event has **ended**!\n" 681 | "**There was no draw because no one bought tickets**"); 682 | config.claimedReward = true; 683 | config.isEventRunning = false; 684 | config.eventRaffleDrawRunning = false; 685 | config.eventTimer->stop(); 686 | UmikoBot::Instance().createMessage(channelID, embed); 687 | return; 688 | } 689 | 690 | //Announce Winner 691 | int lastTicket = static_cast(config.numTicketsBought); 692 | std::random_device randomDevice; 693 | std::mt19937 gen(randomDevice()); 694 | std::uniform_int_distribution<> dist(1, lastTicket); 695 | int luckyTicketId = dist(gen); 696 | 697 | QString tiketId = QString::number(luckyTicketId); 698 | embed.setDescription("**RAFFLE DRAW** event has **ended**!\n" 699 | "**The lucky ticket which wins the draw is `" + tiketId + "`**\n" 700 | "**The owner can claim their rewards within 24 hours**"); 701 | config.claimedReward = false; 702 | config.numTicketsBought = 0; 703 | 704 | //Get the user who has the lucky ticket 705 | for (auto& user : raffleDrawGuildList[guildID]) 706 | { 707 | for (auto& userTicket : user.m_TicketIds) 708 | { 709 | if (luckyTicketId == userTicket) 710 | { 711 | config.luckyUser = user.m_UserId; 712 | } 713 | } 714 | } 715 | 716 | //Clear all the tickets 717 | for (auto& user : raffleDrawGuildList[guildID]) 718 | user.m_TicketIds.clear(); 719 | 720 | //Set the reward claiming time (24 hours) 721 | config.raffleDrawRewardClaimTimer = new QTimer; 722 | config.raffleDrawRewardClaimTimer->setInterval(24 * 3600000); 723 | config.raffleDrawRewardClaimTimer->setSingleShot(true); 724 | 725 | QObject::connect(config.raffleDrawRewardClaimTimer, &QTimer::timeout, [this, guildID, channelID, authorID]() 726 | { 727 | auto& config = getServerEventData(guildID); 728 | config.claimedReward = true; 729 | UmikoBot::Instance().createMessage(channelID, "The owner of the price in the RaffleDraw didn't get their rewards!\n" 730 | "The role(s) who have permission can start this event again!"); 731 | }); 732 | config.raffleDrawRewardClaimTimer->start(); 733 | config.isEventRunning = false; 734 | config.eventRaffleDrawRunning = false; 735 | if(!isInQObjectConnect) 736 | config.eventTimer->stop(); // Forcefully stop the timer. 737 | 738 | UmikoBot::Instance().createMessage(channelID, embed); 739 | } 740 | } 741 | 742 | int EventModule::raffleDrawGetUserIndex(snowflake_t guild, snowflake_t id) 743 | { 744 | for (auto it = raffleDrawGuildList[guild].begin(); it != raffleDrawGuildList[guild].end(); ++it) 745 | { 746 | if (it->m_UserId == id) 747 | { 748 | return std::distance(raffleDrawGuildList[guild].begin(), it); 749 | } 750 | } 751 | raffleDrawGuildList[guild].append(RaffleDraw { id }); 752 | return std::distance(raffleDrawGuildList[guild].begin(), std::prev(raffleDrawGuildList[guild].end())); 753 | } 754 | 755 | EventModule::RaffleDraw EventModule::getUserRaffleDrawData(snowflake_t guild, snowflake_t id) 756 | { 757 | for (auto user : raffleDrawGuildList[guild]) 758 | { 759 | if (user.m_UserId == id) 760 | { 761 | return user; 762 | } 763 | } 764 | RaffleDraw user { id }; 765 | raffleDrawGuildList[guild].append(user); 766 | return raffleDrawGuildList[guild].back(); 767 | } 768 | -------------------------------------------------------------------------------- /src/modules/LevelModule.cpp: -------------------------------------------------------------------------------- 1 | #include "LevelModule.h" 2 | #include "UmikoBot.h" 3 | #include "core/Permissions.h" 4 | 5 | #include 6 | 7 | using namespace Discord; 8 | 9 | LevelModule::LevelModule(UmikoBot* client) 10 | : Module("levels", true), m_client(client) 11 | { 12 | m_timer.setInterval(30 * 1000); 13 | QObject::connect(&m_timer, &QTimer::timeout, 14 | [this]() 15 | { 16 | for (auto it = m_exp.begin(); it != m_exp.end(); it++) 17 | { 18 | for (GuildLevelData& data : it.value()) 19 | { 20 | if (data.messageCount > 0) 21 | { 22 | data.messageCount = 0; 23 | data.exp += 10 + qrand() % 6; 24 | } 25 | } 26 | } 27 | }); 28 | m_timer.start(); 29 | 30 | QTime now = QTime::currentTime(); 31 | qsrand(now.msec()); 32 | 33 | RegisterCommand(Commands::LEVEL_MODULE_TOP, "top", 34 | [this](Client& client, const Message& message, const Channel& channel) 35 | { 36 | auto& exp = m_exp[channel.guildId()]; 37 | QStringList args = message.content().split(' '); 38 | GuildSetting s = GuildSettings::GetGuildSetting(channel.guildId()); 39 | QString prefix = s.prefix; 40 | 41 | if (args.size() == 2) { 42 | qSort(exp.begin(), exp.end(), 43 | [](const LevelModule::GuildLevelData& v1, const LevelModule::GuildLevelData& v2) 44 | { 45 | return v1.exp > v2.exp; 46 | }); 47 | Embed embed; 48 | embed.setColor(qrand() % 16777216); 49 | embed.setTitle("Top " + args.back()); 50 | 51 | QString desc = ""; 52 | 53 | bool ok; 54 | int count = args.back().toInt(&ok); 55 | 56 | if (!ok) 57 | { 58 | client.createMessage(message.channelId(), "**Invalid Count**"); 59 | return; 60 | } 61 | 62 | if (count < 1) 63 | { 64 | client.createMessage(message.channelId(), "**Invalid Count**"); 65 | return; 66 | } 67 | 68 | if (count > 30) 69 | { 70 | client.createMessage(message.channelId(), "**Invalid Count**: The max count is `30`"); 71 | return; 72 | } 73 | 74 | unsigned int numberOfDigits = QString::number(std::min(count, exp.size())).size(); 75 | 76 | for (int i = 0; i < count; i++) 77 | { 78 | if (i >= exp.size()) 79 | { 80 | embed.setTitle("Top " + QString::number(i)); 81 | break; 82 | } 83 | 84 | LevelModule::GuildLevelData& curr = exp[i]; 85 | desc += "`" + QString::number(i + 1).rightJustified(numberOfDigits, ' ') + "`) "; 86 | desc += "**" + reinterpret_cast(&client)->GetName(channel.guildId(), exp[i].user) + "**"; 87 | 88 | unsigned int xp = GetData(channel.guildId(), exp[i].user).exp; 89 | 90 | unsigned int xpRequirement = s.expRequirement; 91 | unsigned int level = 1; 92 | while (xp > xpRequirement && level < s.maximumLevel) { 93 | level++; 94 | xp -= xpRequirement; 95 | xpRequirement *= s.growthRate; 96 | } 97 | 98 | if (level >= s.maximumLevel) 99 | level = s.maximumLevel; 100 | 101 | desc += " - Level " + QString::number(level) + "\n"; 102 | } 103 | 104 | embed.setDescription(desc); 105 | 106 | client.createMessage(message.channelId(), embed); 107 | } 108 | else if (args.size() == 3) 109 | { 110 | qSort(exp.begin(), exp.end(), 111 | [](const LevelModule::GuildLevelData& v1, const LevelModule::GuildLevelData& v2) -> bool 112 | { 113 | return v1.exp > v2.exp; 114 | }); 115 | 116 | bool ok1, ok2; 117 | int count1 = args[1].toInt(&ok1); 118 | int count2 = args[2].toInt(&ok2); 119 | 120 | if (!ok1 || !ok2) 121 | { 122 | client.createMessage(message.channelId(), "**Invalid Count**"); 123 | return; 124 | } 125 | 126 | if (count1 < 1 || count2 < 1) 127 | { 128 | client.createMessage(message.channelId(), "**Invalid Count**"); 129 | return; 130 | } 131 | 132 | 133 | if (count2 < count1) 134 | { 135 | client.createMessage(message.channelId(), "**Invalid Range**"); 136 | return; 137 | } 138 | 139 | 140 | if (count2 - count1 > 30) { 141 | client.createMessage(message.channelId(), "**Invalid Count**: The max offset is `30`"); 142 | return; 143 | } 144 | 145 | Embed embed; 146 | embed.setColor(qrand() % 16777216); 147 | if (count2 == count1) 148 | { 149 | embed.setTitle("Top " + QString::number(count1)); 150 | } 151 | else 152 | { 153 | embed.setTitle("Top from " + QString::number(count1) + " to " + QString::number(count2)); 154 | } 155 | 156 | 157 | QString desc = ""; 158 | 159 | if (count1 > exp.size()) 160 | { 161 | client.createMessage(channel.id(), "**Not enough members to create the list.**"); 162 | return; 163 | } 164 | 165 | unsigned int numberOfDigits = QString::number(std::min(count2, exp.size())).size(); 166 | 167 | for (int i = count1 - 1; i < count2; i++) 168 | { 169 | 170 | if (i >= exp.size()) 171 | { 172 | embed.setTitle("Top from " + QString::number(count1) + " to " + QString::number(i)); 173 | break; 174 | } 175 | 176 | LevelModule::GuildLevelData& curr = exp[i]; 177 | desc += "`" + QString::number(i + 1).rightJustified(numberOfDigits, ' ') + "`) "; 178 | desc += "**" + reinterpret_cast(&client)->GetName(channel.guildId(), exp[i].user) + "**"; 179 | 180 | unsigned int xp = GetData(channel.guildId(), exp[i].user).exp; 181 | 182 | unsigned int xpRequirement = s.expRequirement; 183 | unsigned int level = 1; 184 | while (xp > xpRequirement && level < s.maximumLevel) { 185 | level++; 186 | xp -= xpRequirement; 187 | xpRequirement *= s.growthRate; 188 | } 189 | 190 | if (level >= s.maximumLevel) 191 | level = s.maximumLevel; 192 | 193 | desc += " - Level " + QString::number(level) + "\n"; 194 | } 195 | 196 | embed.setDescription(desc); 197 | 198 | client.createMessage(message.channelId(), embed); 199 | } 200 | else 201 | { 202 | UmikoBot* bot = reinterpret_cast(&client); 203 | Embed embed; 204 | embed.setColor(qrand() % 16777216); 205 | embed.setTitle("Help top"); 206 | QString description = bot->GetCommandHelp("top", prefix); 207 | embed.setDescription(description); 208 | bot->createMessage(message.channelId(), embed); 209 | } 210 | }); 211 | 212 | RegisterCommand(Commands::LEVEL_MODULE_RANK, "rank", 213 | [this](Client& client, const Message& message, const Channel& channel) 214 | { 215 | QStringList args = message.content().split(' '); 216 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 217 | QString prefix = setting->prefix; 218 | 219 | auto printHelp = [&client, prefix, message]() 220 | { 221 | UmikoBot* bot = reinterpret_cast(&client); 222 | Embed embed; 223 | embed.setColor(qrand() % 16777216); 224 | embed.setTitle("Help rank"); 225 | QString description = bot->GetCommandHelp("rank", prefix); 226 | embed.setDescription(description); 227 | bot->createMessage(message.channelId(), embed); 228 | }; 229 | 230 | if (args.size() < 2) 231 | { 232 | printHelp(); 233 | return; 234 | } 235 | 236 | if (args.last() == "list" && args.size() == 2) 237 | { 238 | QList ranks = setting->ranks; 239 | Embed embed; 240 | embed.setColor(qrand() % 16777216); 241 | embed.setTitle("Rank list"); 242 | 243 | QString description = ""; 244 | 245 | if (ranks.size() == 0) 246 | description = "No ranks found!"; 247 | else 248 | for (int i = 0; i < ranks.size(); i++) 249 | description += ranks[i].name + " id " + QString::number(i) + " minimum level: " + QString::number(ranks[i].minimumLevel) + "\n"; 250 | 251 | embed.setDescription(description); 252 | client.createMessage(message.channelId(), embed); 253 | } 254 | else if (args[1] == "add" && args.size() > 3) 255 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 4, args, false, [this, &client, channel, message, args]() 256 | { 257 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 258 | 259 | bool ok; 260 | unsigned int minimumLevel = args[2].toUInt(&ok); 261 | if (!ok) 262 | { 263 | client.createMessage(message.channelId(), "Invalid minimum level."); 264 | return; 265 | } 266 | QString name = ""; 267 | for (int i = 3; i < args.size(); i++) 268 | { 269 | name += args[i]; 270 | if (i < args.size() - 1) 271 | name += " "; 272 | } 273 | 274 | LevelRank rank = { name, minimumLevel }; 275 | 276 | for (int i = 0; i < setting->ranks.size(); i++) 277 | if (setting->ranks[i].minimumLevel == minimumLevel) 278 | { 279 | client.createMessage(message.channelId(), "Cannot add rank, minimum level already used."); 280 | return; 281 | } 282 | 283 | setting->ranks.push_back(rank); 284 | qSort(setting->ranks.begin(), setting->ranks.end(), 285 | [](const LevelRank& v1, const LevelRank& v2) -> bool 286 | { 287 | return v1.minimumLevel < v2.minimumLevel; 288 | }); 289 | 290 | for (int i = 0; i < setting->ranks.size(); i++) 291 | if (setting->ranks[i].name == name) 292 | { 293 | client.createMessage(message.channelId(), "Added rank " + name + " with id " + QString::number(i)); 294 | break; 295 | } 296 | }); 297 | else if (args[1] == "remove" && args.size() == 3) 298 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 3, args, false, [this, &client, channel, message, args]() 299 | { 300 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 301 | 302 | bool ok; 303 | unsigned int id = args[2].toUInt(&ok); 304 | if (!ok) 305 | { 306 | client.createMessage(message.channelId(), "Invalid id."); 307 | return; 308 | } 309 | 310 | if (id >= (unsigned int)setting->ranks.size()) 311 | { 312 | client.createMessage(message.channelId(), "Id not found."); 313 | return; 314 | } 315 | 316 | client.createMessage(message.channelId(), "Deleted rank " + setting->ranks[id].name + " succesfully."); 317 | setting->ranks.erase(setting->ranks.begin() + id); 318 | }); 319 | else if (args[1] == "edit" && args.size() >= 4) 320 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 5, args, false, [this, &client, channel, message, args, printHelp]() 321 | { 322 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 323 | 324 | bool ok; 325 | unsigned int id = args[3].toUInt(&ok); 326 | if (!ok) 327 | { 328 | client.createMessage(message.channelId(), "Invalid id."); 329 | return; 330 | } 331 | 332 | if (id >= (unsigned int)setting->ranks.size()) 333 | { 334 | client.createMessage(message.channelId(), "Id not found."); 335 | return; 336 | } 337 | 338 | LevelRank& rank = setting->ranks[id]; 339 | if (args[2] == "name") 340 | { 341 | QString name = ""; 342 | for (int i = 4; i < args.size(); i++) 343 | { 344 | name += args[i]; 345 | if (i < args.size() - 1) 346 | name += " "; 347 | } 348 | rank.name = name; 349 | client.createMessage(message.channelId(), "Rank id " + QString::number(id) + " has been succesfully edited."); 350 | } 351 | else if (args[2] == "level") 352 | { 353 | bool ok; 354 | unsigned int newlevel = args[4].toUInt(&ok); 355 | if (!ok) 356 | { 357 | client.createMessage(message.channelId(), "Invalid new level."); 358 | return; 359 | } 360 | 361 | rank.minimumLevel = newlevel; 362 | client.createMessage(message.channelId(), "Rank id " + QString::number(id) + " has been succesfully edited."); 363 | } 364 | else 365 | { 366 | printHelp(); 367 | } 368 | }); 369 | else 370 | printHelp(); 371 | }); 372 | 373 | RegisterCommand(Commands::LEVEL_MODULE_MAX_LEVEL, "setmaxlevel", 374 | [this](Client& client, const Message& message, const Channel& channel) 375 | { 376 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 377 | QStringList args = message.content().split(' '); 378 | QString prefix = setting->prefix; 379 | 380 | auto printHelp = [&client, prefix, message]() 381 | { 382 | UmikoBot* bot = reinterpret_cast(&client); 383 | Embed embed; 384 | embed.setColor(qrand() % 16777216); 385 | embed.setTitle("Help setmaxlevel"); 386 | QString description = bot->GetCommandHelp("setmaxlevel", prefix); 387 | embed.setDescription(description); 388 | bot->createMessage(message.channelId(), embed); 389 | }; 390 | 391 | if (args.size() == 2) 392 | { 393 | if (args.last() == "current") 394 | { 395 | client.createMessage(message.channelId(), "Maximum level is currently set to " + QString::number(setting->maximumLevel)); 396 | return; 397 | } 398 | 399 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, true, [this, &client, channel, message, args, setting]() 400 | { 401 | setting->maximumLevel = args[1].toUInt(); 402 | client.createMessage(message.channelId(), "Maximum level set to " + QString::number(setting->maximumLevel) + " succesfully!"); 403 | }); 404 | } 405 | else 406 | printHelp(); 407 | }); 408 | 409 | RegisterCommand(Commands::LEVEL_MODULE_EXP_REQUIREMENT, "setexpreq", 410 | [this](Client& client, const Message& message, const Channel& channel) 411 | { 412 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 413 | QStringList args = message.content().split(' '); 414 | QString prefix = setting->prefix; 415 | 416 | auto printHelp = [&client, prefix, message]() 417 | { 418 | UmikoBot* bot = reinterpret_cast(&client); 419 | Embed embed; 420 | embed.setColor(qrand() % 16777216); 421 | embed.setTitle("Help setexpreq"); 422 | QString description = bot->GetCommandHelp("setexpreq", prefix); 423 | embed.setDescription(description); 424 | bot->createMessage(message.channelId(), embed); 425 | }; 426 | 427 | if (args.size() == 2) 428 | { 429 | if (args.last() == "current") 430 | { 431 | client.createMessage(message.channelId(), "Exp requirement is currently set to " + QString::number(setting->expRequirement)); 432 | return; 433 | } 434 | 435 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, true, [this, &client, channel, message, args, setting]() 436 | { 437 | setting->expRequirement = args[1].toUInt(); 438 | client.createMessage(message.channelId(), "Exp requirement set to " + QString::number(setting->expRequirement) + " succesfully!"); 439 | }); 440 | } 441 | else 442 | printHelp(); 443 | }); 444 | 445 | RegisterCommand(Commands::LEVEL_MODULE_EXP_GROWTH_RATE, "setgrowthrate", 446 | [this](Client& client, const Message& message, const Channel& channel) 447 | { 448 | GuildSetting* setting = &GuildSettings::GetGuildSetting(channel.guildId()); 449 | QStringList args = message.content().split(' '); 450 | QString prefix = setting->prefix; 451 | 452 | auto printHelp = [&client, prefix, message]() 453 | { 454 | UmikoBot* bot = reinterpret_cast(&client); 455 | Embed embed; 456 | embed.setColor(qrand() % 16777216); 457 | embed.setTitle("Help setgrowthrate"); 458 | QString description = bot->GetCommandHelp("setgrowthrate", prefix); 459 | embed.setDescription(description); 460 | bot->createMessage(message.channelId(), embed); 461 | }; 462 | 463 | if (args.size() == 2) 464 | { 465 | if (args.last() == "current") 466 | { 467 | client.createMessage(message.channelId(), "Exp growth is currently set to " + QString::number(setting->growthRate)); 468 | return; 469 | } 470 | 471 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, true, [this, &client, channel, message, args, setting]() 472 | { 473 | setting->growthRate = args[1].toFloat(); 474 | client.createMessage(message.channelId(), "Growth rate set to " + QString::number(setting->growthRate) + " succesfully!"); 475 | }); 476 | } 477 | else 478 | printHelp(); 479 | }); 480 | 481 | RegisterCommand(Commands::LEVEL_MODULE_EXP_GIVE, "givexp", 482 | [this](Client& client, const Message& message, const Channel& channel) 483 | { 484 | GuildSetting s = GuildSettings::GetGuildSetting(channel.guildId()); 485 | QString prefix = s.prefix; 486 | QStringList args = message.content().split(' '); 487 | 488 | auto printHelp = [&client, prefix, message]() 489 | { 490 | UmikoBot* bot = reinterpret_cast(&client); 491 | Embed embed; 492 | embed.setColor(qrand() % 16777216); 493 | embed.setTitle("Help givexp"); 494 | QString description = bot->GetCommandHelp("givexp", prefix); 495 | embed.setDescription(description); 496 | bot->createMessage(message.channelId(), embed); 497 | }; 498 | 499 | if (args.size() >= 3) 500 | { 501 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 3, args, false, [this, &client, channel, message, args, s]() 502 | { 503 | auto& exp = m_exp[channel.guildId()]; 504 | 505 | snowflake_t userId = static_cast(&client)->GetUserFromArg(channel.guildId(), args, 2); 506 | 507 | if (userId == 0) { 508 | client.createMessage(message.channelId(), "Could not find user!"); 509 | return; 510 | } 511 | 512 | LevelModule::GuildLevelData* levelData; 513 | 514 | for (int i = 0; i < exp.size(); i++) 515 | { 516 | if (exp[i].user == userId) 517 | levelData = &exp[i]; 518 | } 519 | 520 | ExpLevelData userRes = ExpToLevel(channel.guildId(), levelData->exp); 521 | 522 | int finalExp = levelData->exp; 523 | int addedExp = 0; 524 | 525 | if (userRes.level == s.maximumLevel) { 526 | client.createMessage(message.channelId(), "User is already maximum level!"); 527 | return; 528 | } 529 | 530 | if (args[1].endsWith("L")) 531 | { 532 | QStringRef substring(&args[1], 0, args[1].size() - 1); 533 | unsigned int levels = substring.toUInt(); 534 | 535 | if (userRes.level + levels >= s.maximumLevel) { 536 | finalExp = s.expRequirement * (qPow(s.growthRate, s.maximumLevel) - 1) / (s.growthRate - 1); 537 | } 538 | else 539 | { 540 | for (unsigned int i = 0; i < levels; i++) { 541 | addedExp += userRes.xpRequirement; 542 | userRes.xpRequirement *= s.growthRate; 543 | } 544 | finalExp += addedExp; 545 | } 546 | 547 | } 548 | else 549 | { 550 | addedExp += args[1].toUInt(); 551 | ExpLevelData newRes = ExpToLevel(channel.guildId(), addedExp + finalExp); 552 | if (newRes.level >= s.maximumLevel) { 553 | finalExp = s.expRequirement * (pow(s.growthRate, s.maximumLevel) - 1) / (s.growthRate - 1); 554 | } 555 | } 556 | levelData->exp = finalExp; 557 | 558 | client.createMessage(message.channelId(), "Succesfully given " + static_cast(&client)->GetName(channel.guildId(), userId) + " " + QString::number(addedExp) + " exp"); 559 | 560 | }); 561 | } 562 | else 563 | printHelp(); 564 | }); 565 | 566 | RegisterCommand(Commands::LEVEL_MODULE_EXP_TAKE, "takexp", 567 | [this](Client& client, const Message& message, const Channel& channel) 568 | { 569 | GuildSetting s = GuildSettings::GetGuildSetting(channel.guildId()); 570 | QString prefix = s.prefix; 571 | QStringList args = message.content().split(' '); 572 | 573 | auto printHelp = [&client, prefix, message]() 574 | { 575 | UmikoBot* bot = reinterpret_cast(&client); 576 | Embed embed; 577 | embed.setColor(qrand() % 16777216); 578 | embed.setTitle("Help takexp"); 579 | QString description = bot->GetCommandHelp("takexp", prefix); 580 | embed.setDescription(description); 581 | bot->createMessage(message.channelId(), embed); 582 | }; 583 | 584 | if (args.size() >= 3) 585 | { 586 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 3, args, false, [this, &client, channel, message, args, s]() 587 | { 588 | auto& exp = m_exp[channel.guildId()]; 589 | 590 | snowflake_t userId = static_cast(&client)->GetUserFromArg(channel.guildId(), args, 2); 591 | 592 | if (userId == 0) { 593 | client.createMessage(message.channelId(), "Could not find user!"); 594 | return; 595 | } 596 | 597 | LevelModule::GuildLevelData* levelData; 598 | 599 | for (int i = 0; i < exp.size(); i++) 600 | { 601 | if (exp[i].user == userId) 602 | levelData = &exp[i]; 603 | } 604 | 605 | ExpLevelData userRes = ExpToLevel(channel.guildId(), levelData->exp); 606 | 607 | int finalExp = levelData->exp; 608 | int subtractedExp = 0; 609 | 610 | if (userRes.level == 0) { 611 | client.createMessage(message.channelId(), "User is already minimum level!"); 612 | return; 613 | } 614 | 615 | if (args[1].endsWith("L")) 616 | { 617 | QStringRef substring(&args[1], 0, args[1].size() - 1); 618 | unsigned int levels = substring.toUInt(); 619 | 620 | if (userRes.level - levels < 0) 621 | { 622 | subtractedExp = levelData->exp; 623 | finalExp = 0; 624 | } 625 | else { 626 | userRes.xpRequirement /= s.growthRate; 627 | for (unsigned int i = 0; i < levels; i++) 628 | { 629 | subtractedExp += userRes.xpRequirement; 630 | userRes.xpRequirement /= s.growthRate; 631 | } 632 | } 633 | 634 | } 635 | else 636 | { 637 | subtractedExp += args[1].toUInt(); 638 | } 639 | if (finalExp - subtractedExp < 0) 640 | finalExp = 0; 641 | else 642 | finalExp -= subtractedExp; 643 | 644 | levelData->exp = finalExp; 645 | 646 | client.createMessage(message.channelId(), "Succesfully taken " + QString::number(subtractedExp) + " exp from " + static_cast(&client)->GetName(channel.guildId(), userId)); 647 | 648 | }); 649 | } 650 | else 651 | printHelp(); 652 | }); 653 | 654 | RegisterCommand(Commands::LEVEL_MODULE_BLOCK_EXP, "blockxp", 655 | [this](Client& client, const Message& message, const Channel& channel) 656 | { 657 | GuildSetting s = GuildSettings::GetGuildSetting(channel.guildId()); 658 | QString prefix = s.prefix; 659 | QStringList args = message.content().split(' '); 660 | 661 | auto printHelp = [&client, prefix, message]() 662 | { 663 | UmikoBot* bot = reinterpret_cast(&client); 664 | Embed embed; 665 | embed.setColor(qrand() % 16777216); 666 | embed.setTitle("Help blockxp"); 667 | QString description = bot->GetCommandHelp("blockxp", prefix); 668 | embed.setDescription(description); 669 | bot->createMessage(message.channelId(), embed); 670 | }; 671 | 672 | if (args.size() > 1 && args[1] == "whitelist") 673 | { 674 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, &client, channel, message, args]() 675 | { 676 | GuildSetting& s = GuildSettings::GetGuildSetting(channel.guildId()); 677 | for (int i = 0; i < s.levelBlacklistedChannels.size(); i++) { 678 | if (s.levelBlacklistedChannels[i] == channel.id()) 679 | { 680 | s.levelBlacklistedChannels.erase(s.levelBlacklistedChannels.begin() + i); 681 | client.createMessage(message.channelId(), "Channel removed from blacklisted!"); 682 | return; 683 | } 684 | } 685 | 686 | for (int i = 0; i < s.levelWhitelistedChannels.size(); i++) { 687 | if (s.levelWhitelistedChannels[i] == channel.id()) 688 | { 689 | client.createMessage(message.channelId(), "Channel is already whitelisted!"); 690 | return; 691 | } 692 | } 693 | 694 | s.levelWhitelistedChannels.push_back(channel.id()); 695 | client.createMessage(message.channelId(), "Channel has been whitelisted!"); 696 | }); 697 | } 698 | else if (args.size() > 1 && args[1] == "blacklist") 699 | { 700 | UmikoBot::VerifyAndRunAdminCmd(client, message, channel, 2, args, false, [this, &client, channel, message, args]() 701 | { 702 | GuildSetting& s = GuildSettings::GetGuildSetting(channel.guildId()); 703 | for (int i = 0; i < s.levelWhitelistedChannels.size(); i++) { 704 | if (s.levelWhitelistedChannels[i] == channel.id()) 705 | { 706 | s.levelWhitelistedChannels.erase(s.levelWhitelistedChannels.begin() + i); 707 | client.createMessage(message.channelId(), "Channel removed from whitelisted!"); 708 | return; 709 | } 710 | } 711 | 712 | for (int i = 0; i < s.levelBlacklistedChannels.size(); i++) { 713 | if (s.levelBlacklistedChannels[i] == channel.id()) 714 | { 715 | client.createMessage(message.channelId(), "Channel is already blacklisted!"); 716 | return; 717 | } 718 | } 719 | 720 | s.levelBlacklistedChannels.push_back(channel.id()); 721 | client.createMessage(message.channelId(), "Channel has been blacklisted!"); 722 | }); 723 | } 724 | else 725 | printHelp(); 726 | }); 727 | } 728 | 729 | void LevelModule::OnSave(QJsonDocument& doc) const 730 | { 731 | QJsonObject json; 732 | QJsonObject levels; 733 | QJsonObject backups; 734 | 735 | for (auto it = m_exp.begin(); it != m_exp.end(); it++) 736 | for (int i = 0; i < it.value().size(); i++) 737 | if (m_client->GetName(it.key(), it.value()[i].user) == "") 738 | { 739 | bool found = false; 740 | for (GuildLevelData& data : m_backupexp[it.key()]) 741 | if (data.user == it.value()[i].user) { 742 | data.exp += it.value()[i].exp; 743 | found = true; 744 | break; 745 | } 746 | 747 | if (!found) 748 | m_backupexp[it.key()].append(it.value()[i]); 749 | 750 | it.value().erase(it.value().begin() + i); 751 | } 752 | 753 | for (auto it = m_backupexp.begin(); it != m_backupexp.end(); it++) 754 | for (int i = 0; i < it.value().size(); i++) 755 | if (m_client->GetName(it.key(), it.value()[i].user) != "") 756 | { 757 | bool found = false; 758 | for (GuildLevelData& data : m_exp[it.key()]) 759 | if (data.user == it.value()[i].user) { 760 | data.exp += it.value()[i].exp; 761 | found = true; 762 | break; 763 | } 764 | 765 | if (!found) 766 | m_exp[it.key()].append(it.value()[i]); 767 | 768 | it.value().erase(it.value().begin() + i); 769 | } 770 | 771 | if(m_exp.size() > 0) 772 | for (auto it = m_exp.begin(); it != m_exp.end(); it++) 773 | { 774 | QJsonObject level; 775 | 776 | for (int i = 0; i < it.value().size(); i++) 777 | level[QString::number(it.value()[i].user)] = it.value()[i].exp; 778 | 779 | levels[QString::number(it.key())] = level; 780 | } 781 | 782 | if(m_backupexp.size() > 0) 783 | for (auto it = m_backupexp.begin(); it != m_backupexp.end(); it++) 784 | { 785 | QJsonObject backup; 786 | 787 | for (int i = 0; i < it.value().size(); i++) 788 | backup[QString::number(it.value()[i].user)] = it.value()[i].exp; 789 | 790 | backups[QString::number(it.key())] = backup; 791 | } 792 | 793 | json["levels"] = levels; 794 | json["backups"] = backups; 795 | doc.setObject(json); 796 | } 797 | 798 | void LevelModule::OnLoad(const QJsonDocument& doc) 799 | { 800 | QJsonObject json = doc.object(); 801 | 802 | QJsonObject backups = json["backups"].toObject(); 803 | QJsonObject levels = json["levels"].toObject(); 804 | 805 | QStringList guildIds = levels.keys(); 806 | 807 | for (const QString& guild : guildIds) 808 | { 809 | snowflake_t guildId = guild.toULongLong(); 810 | 811 | QJsonObject level = levels[guild].toObject(); 812 | QJsonObject backup = backups[guild].toObject(); 813 | 814 | for (const QString& user : level.keys()) 815 | m_exp[guildId].append({ user.toULongLong(), level[user].toInt(), 0 }); 816 | 817 | for (const QString& user : backup.keys()) 818 | m_backupexp[guildId].append({ user.toULongLong(), backup[user].toInt(), 0 }); 819 | } 820 | } 821 | 822 | ExpLevelData LevelModule::ExpToLevel(snowflake_t guild, unsigned int exp) 823 | { 824 | GuildSetting s = GuildSettings::GetGuildSetting(guild); 825 | 826 | ExpLevelData res; 827 | 828 | res.xpRequirement = s.expRequirement; 829 | res.level = 1; 830 | res.exp = exp; 831 | 832 | while (res.exp > res.xpRequirement && res.level < s.maximumLevel) 833 | { 834 | res.level++; 835 | res.exp -= res.xpRequirement; 836 | res.xpRequirement *= s.growthRate; 837 | } 838 | 839 | if (res.level >= s.maximumLevel) 840 | { 841 | res.exp = 0; 842 | res.level = s.maximumLevel; 843 | //res.xpRequirement = 0; 844 | } 845 | 846 | return res; 847 | } 848 | 849 | void LevelModule::StatusCommand(QString& result, snowflake_t guild, snowflake_t user) 850 | { 851 | GuildSetting s = GuildSettings::GetGuildSetting(guild); 852 | 853 | unsigned int xp = GetData(guild, user).exp; 854 | 855 | ExpLevelData res = ExpToLevel(guild, xp); 856 | 857 | // Sort to get leaderboard index 858 | auto& exp = m_exp[guild]; 859 | qSort(exp.begin(), exp.end(), 860 | [](const LevelModule::GuildLevelData& v1, const LevelModule::GuildLevelData& v2) 861 | { 862 | return v1.exp > v2.exp; 863 | }); 864 | int leaderboardIndex = -1; 865 | for (int i = 0; i < exp.size(); ++i) 866 | { 867 | if (exp.at(i).user == user) 868 | { 869 | leaderboardIndex = i; 870 | break; 871 | } 872 | } 873 | 874 | unsigned int rankLevel = res.level; 875 | QString rank = ""; 876 | if (s.ranks.size() > 0) { 877 | for (int i = 0; i < s.ranks.size() - 1; i++) 878 | { 879 | if (rankLevel >= s.ranks[i].minimumLevel && rankLevel < s.ranks[i + 1].minimumLevel) 880 | { 881 | rank = s.ranks[i].name; 882 | break; 883 | } 884 | } 885 | if (rank == "") 886 | rank = s.ranks[s.ranks.size() - 1].name; 887 | 888 | if (leaderboardIndex >= 0) 889 | result += "Rank: " + rank + " (#" + QString::number(leaderboardIndex + 1) + ")\n"; 890 | else 891 | result += "Rank: " + rank + "\n"; 892 | } 893 | else if (leaderboardIndex >= 0) { 894 | result += "Rank: #" + QString::number(leaderboardIndex + 1) + "\n"; 895 | } 896 | 897 | result += "Level: " + QString::number(res.level) + "\n"; 898 | result += "Total XP: " + QString::number(GetData(guild, user).exp) + "\n"; 899 | if(res.level < s.maximumLevel) 900 | result += QString("XP until next level: %1\n").arg(res.xpRequirement - res.exp); 901 | result += "\n"; 902 | } 903 | 904 | void LevelModule::OnMessage(Client& client, const Message& message) 905 | { 906 | Module::OnMessage(client, message); 907 | 908 | client.getChannel(message.channelId()).then( 909 | [this, message](const Channel& channel) 910 | { 911 | auto& exp = m_exp[channel.guildId()]; 912 | 913 | if (!GuildSettings::IsModuleEnabled(channel.guildId(), GetName(), IsEnabledByDefault())) 914 | return; 915 | 916 | if (message.author().bot()) 917 | return; 918 | 919 | if(!GuildSettings::ExpAllowed(channel.guildId(), channel.id())) 920 | return; 921 | 922 | QList commands = UmikoBot::Instance().GetAllCommands(); 923 | GuildSetting setting = GuildSettings::GetGuildSetting(channel.guildId()); 924 | 925 | for (Command& command : commands) 926 | { 927 | if (message.content().startsWith(setting.prefix + command.name)) 928 | return; 929 | } 930 | 931 | for (GuildLevelData& data : exp) { 932 | if (data.user == message.author().id()) { 933 | data.messageCount++; 934 | return; 935 | } 936 | } 937 | 938 | exp.append({ message.author().id(), 0, 1 }); 939 | }); 940 | } 941 | 942 | LevelModule::GuildLevelData LevelModule::GetData(snowflake_t guild, snowflake_t user) 943 | { 944 | for (GuildLevelData data : m_exp[guild]) 945 | { 946 | if (data.user == user) { 947 | return data; 948 | } 949 | } 950 | return { user, 0,0 }; 951 | } 952 | --------------------------------------------------------------------------------