diff --git a/Source/commands.cppm b/Source/commands.cppm index 162c0d7..9fe9b6e 100644 --- a/Source/commands.cppm +++ b/Source/commands.cppm @@ -3,6 +3,7 @@ module; #include #include #include +#include #include #include @@ -29,11 +30,12 @@ constexpr std::array ttsTestMessages = { export struct Command { bool enabled; std::string callstr, description; - std::function func; + std::function func; + std::chrono::time_point lastExecuted; Command() = default; Command(const std::string &call, const std::string &desc, - const std::function &f) + const std::function &f) : enabled(true), callstr(call), description(desc), func(f) {} }; @@ -49,13 +51,17 @@ public: -> Result { if (m_commandsMap.empty()) { m_commandsMap["text_to_speech"] = - Command("tts", "TTS Notification", [](const std::string &msg) { - const std::string notifMsg = msg; - TTSHandler::voiceString(notifMsg); + Command("tts", "TTS Notification", [](const TwitchChatMessage &msg) { + const std::string notifMsg = msg.get_message(); + auto speakerID = -1; + if (global_users.contains(msg.user)) + speakerID = global_users[msg.user]->userVoice; + + TTSHandler::voiceString(notifMsg, speakerID); }); - m_commandsMap["custom_notification"] = - Command("cc", "Custom Notification", [launch_notification](const std::string &msg) { - std::string notifMsg = msg; + m_commandsMap["custom_notification"] = Command( + "cc", "Custom Notification", [launch_notification](const TwitchChatMessage &msg) { + std::string notifMsg = msg.get_message(); // Split to words (space-separated) const auto words = split_string(notifMsg, " "); @@ -81,8 +87,7 @@ public: } } // Play easter egg sounds - if (!sounds.empty()) - AudioPlayer::play_sequential(sounds); + if (!sounds.empty()) AudioPlayer::play_sequential(sounds); launch_notification(notifMsg); }); @@ -97,12 +102,10 @@ public: // Tests a command with random message from testMessages static void test_command(const std::string &command) { // If the command does not exist, skip - if (!m_commandsMap.contains(command)) - return; + if (!m_commandsMap.contains(command)) return; // Make sure the command is enabled - if (!m_commandsMap[command].enabled) - return; + if (!m_commandsMap[command].enabled) return; std::string testMsg; if (command == "text_to_speech") @@ -120,7 +123,15 @@ public: } // Execute the command with the test message - execute_command(command, testMsg); + execute_command(command, TwitchChatMessage("testUser", testMsg)); + } + + // Returns key for command with given call string + static auto get_command_key(const std::string &call) -> std::string { + for (const auto &[key, cmd] : m_commandsMap) + if (cmd.callstr == call) return key; + + return ""; } // Returns a non-modifiable command map @@ -131,8 +142,7 @@ public: // Sets whether command is enabled static void set_command_enabled(const std::string &key, const bool enabled) { // If the key does not exist, skip - if (!m_commandsMap.contains(key)) - return; + if (!m_commandsMap.contains(key)) return; m_commandsMap[key].enabled = enabled; } @@ -140,28 +150,35 @@ public: // Method for changing the call string of a command static void change_command_call(const std::string &key, const std::string &newCall) { // If the key does not exist, skip - if (!m_commandsMap.contains(key)) - return; + if (!m_commandsMap.contains(key)) return; // Make sure the new call is not empty - if (newCall.empty()) - return; + if (newCall.empty()) return; // Change the callstr m_commandsMap[key].callstr = newCall; } // Method for executing a command - static void execute_command(const std::string &key, const std::string &msg) { + static void execute_command(const std::string &key, const TwitchChatMessage &msg) { // If the command does not exist, skip - if (!m_commandsMap.contains(key)) - return; + if (!m_commandsMap.contains(key)) return; // Make sure the command is enabled - if (!m_commandsMap[key].enabled) - return; + if (!m_commandsMap[key].enabled) return; // Execute the command + m_commandsMap[key].lastExecuted = std::chrono::steady_clock::now(); m_commandsMap[key].func(msg); } + + // Method for returning time when command was last executed + static auto get_last_executed_time(const std::string &key) + -> std::chrono::time_point { + // If the key does not exist, return epoch + if (!m_commandsMap.contains(key)) + return std::chrono::time_point(); + + return m_commandsMap[key].lastExecuted; + } }; \ No newline at end of file diff --git a/Source/common.cppm b/Source/common.cppm index 7676c13..459c71a 100644 --- a/Source/common.cppm +++ b/Source/common.cppm @@ -1,11 +1,16 @@ module; +#include +#include #include +#include #include #include #include #include +#include #include +#include export module common; @@ -122,3 +127,181 @@ export struct Result { explicit operator bool() const { return code == 0; } }; + +// Template method for converting string to integer +export template +requires std::is_integral_v +constexpr auto integral_from_string(const std::string &str) -> T { + if constexpr (std::same_as) + return std::stoi(str); + else if constexpr (std::same_as) + return std::stoi(str); + else if constexpr (std::same_as) + return std::stoi(str); + else if constexpr (std::same_as) + return std::stoll(str); + else if constexpr (std::same_as) + return std::stoul(str); + else if constexpr (std::same_as) + return std::stoul(str); + else if constexpr (std::same_as) + return std::stoul(str); + else if constexpr (std::same_as) + return std::stoull(str); + else + static_assert(false, "Unsupported type for integral_from_string"); +} + +// Tempalte method for converting string to floating point +export template +requires std::is_floating_point_v +constexpr auto floating_from_string(const std::string &str) -> T { + if constexpr (std::same_as) + return std::stof(str); + else if constexpr (std::same_as) + return std::stod(str); + else if constexpr (std::same_as) + return std::stold(str); + else + static_assert(false, "Unsupported type for floating_from_string"); +} + +// Stringable concept for checking if a type is convertible to a string +export template +concept Stringable = + std::same_as || std::is_integral_v || std::is_floating_point_v || + std::same_as || std::is_integral_v> || + std::is_floating_point_v>; + +// Template method for converting a string to a specific type +// takes care of calling proper conversion +export template +requires Stringable +constexpr auto t_from_string(const std::string &str) -> T { + if constexpr (std::same_as) + return str == "true"; + else if constexpr (std::same_as) + return std::string(str); + else if constexpr (std::is_floating_point_v) + return floating_from_string(str); + else if constexpr (std::is_integral_v) + return integral_from_string(str); + else if constexpr (std::is_integral_v>) + return static_cast(integral_from_string>(str)); + else if constexpr (std::is_floating_point_v>) + return static_cast(floating_from_string>(str)); + else + static_assert(false, "Unsupported type for t_from_string"); +} + +// Template method for converting a specific type to a string +// takes care of calling proper conversion +export template +requires Stringable +constexpr auto t_to_string(const T &value) -> std::string { + if constexpr (std::same_as) + return value ? "true" : "false"; + else if constexpr (std::same_as) + return value; + else if constexpr (std::is_floating_point_v) + return std::to_string(value); + else if constexpr (std::is_integral_v) + return std::to_string(value); + else if constexpr (std::is_integral_v>) + return std::to_string(static_cast>(value)); + else if constexpr (std::is_floating_point_v>) + return std::to_string(static_cast>(value)); + else + static_assert(false, "Unsupported type for t_to_string"); +} + +// Templates to make enum classes work as bitmasks +// <3 https://voithos.io/articles/enum-class-bitmasks/ <3 +export template +struct FEnableBitmaskOperators { + static constexpr bool enable = false; +}; +export template +concept EnableBitmaskOperators = FEnableBitmaskOperators::enable; + +export template +requires EnableBitmaskOperators +constexpr auto operator|(E l, E r) -> E { + return static_cast(static_cast>(l) | + static_cast>(r)); +} +export template +requires EnableBitmaskOperators +constexpr auto operator&(E l, E r) -> bool { + return (static_cast>(l) & + static_cast>(r)) != 0; +} +export template +requires EnableBitmaskOperators +constexpr auto operator^(E l, E r) -> E { + return static_cast(static_cast>(l) ^ + static_cast>(r)); +} +export template +requires EnableBitmaskOperators +constexpr auto operator~(E e) -> E { + return static_cast(~static_cast>(e)); +} +export template +requires EnableBitmaskOperators +auto operator|=(E &l, E r) -> E & { + return l = l | r; +} +export template +requires EnableBitmaskOperators +auto operator&=(E &l, E r) -> E & { + return l = l & r; +} +export template +requires EnableBitmaskOperators +auto operator^=(E &l, E r) -> E & { + return l = l ^ r; +} +// operator* which gets the underlying value of the enum +export template +requires EnableBitmaskOperators +constexpr auto operator*(E &e) -> std::underlying_type_t & { + return reinterpret_cast &>(e); +} + +// Struct for Twitch message data +// TODO: Move to general module +export struct TwitchChatMessage { + std::string user; + std::string message; + std::chrono::time_point time; + + TwitchChatMessage(std::string user, std::string message) + : user(std::move(user)), message(std::move(message)), + time(std::chrono::steady_clock::now()) {} + + [[nodiscard]] auto is_command() const -> bool { return message.starts_with('!'); } + [[nodiscard]] auto get_command() const -> std::string { + return get_string_between(message, "!", " "); + } + + [[nodiscard]] auto get_message() const -> std::string { + return message.substr(message.find(' ') + 1); + } +}; + +// Struct for holding user data +// TODO: Move to general module +export struct TwitchUser { + std::string name; + bool bypassCooldown = false; + TwitchChatMessage lastMessage; + int userVoice = -1; + + explicit TwitchUser(std::string name, TwitchChatMessage lastMessage) + : name(std::move(name)), lastMessage(std::move(lastMessage)) {} +}; + +// Cache of users +// TODO: Move to general module +export std::map> global_users; diff --git a/Source/config.cppm b/Source/config.cppm index 5d76da8..94f8e61 100644 --- a/Source/config.cppm +++ b/Source/config.cppm @@ -12,7 +12,17 @@ import common; import assets; // Enum for cooldown types -export enum class CommandCooldownType { eNone, eGlobal, ePerUser }; +export enum class CommandCooldownType : unsigned int { + eNone = 0, + eGlobal = 1 << 0, + ePerUser = 1 << 1, + ePerCommand = 1 << 2, +}; +// Enable bitmask operators for CommandCooldownType +template <> +struct FEnableBitmaskOperators { + static constexpr bool enable = true; +}; // Struct for keeping configs across launches export struct Config { @@ -21,9 +31,9 @@ export struct Config { float notifEffectIntensity = 2.0f; float notifFontScale = 1.0f; float globalAudioVolume = 0.75f; - std::vector approvedUsers; - std::string twitchAuthToken, twitchAuthUser, twitchChannel; - CommandCooldownType cooldownType = CommandCooldownType::eGlobal; + std::vector approvedUsers = {}; + std::string twitchAuthToken = "", twitchAuthUser = "", twitchChannel = ""; + CommandCooldownType enabledCooldowns = CommandCooldownType::eGlobal; std::uint32_t cooldownTime = 5; std::uint32_t maxAudioTriggers = 3; //< How many audio triggers can a message cause float audioSequenceOffset = -0.5f; //< Offset for how long to wait between audio triggers @@ -40,7 +50,7 @@ export struct Config { json["twitchAuthToken"].set(twitchAuthToken); json["twitchAuthUser"].set(twitchAuthUser); json["twitchChannel"].set(twitchChannel); - json["cooldownType"].set(static_cast(cooldownType)); + json["enabledCooldowns"].set(enabledCooldowns); json["cooldownTime"].set(cooldownTime); json["maxAudioTriggers"].set(maxAudioTriggers); json["audioSequenceOffset"].set(audioSequenceOffset); @@ -60,21 +70,25 @@ export struct Config { JSONed::JSON json; if (!json.load(get_config_path())) return Result(1, "Failed to load config"); - notifAnimationLength = json["notifAnimationLength"].get(); - notifEffectSpeed = json["notifEffectSpeed"].get(); - notifEffectIntensity = json["notifEffectIntensity"].get(); - notifFontScale = json["notifFontScale"].get(); - globalAudioVolume = json["globalAudioVolume"].get(); - twitchAuthToken = json["twitchAuthToken"].get(); - twitchAuthUser = json["twitchAuthUser"].get(); - twitchChannel = json["twitchChannel"].get(); - cooldownType = static_cast(json["cooldownType"].get()); - cooldownTime = json["cooldownTime"].get(); - maxAudioTriggers = json["maxAudioTriggers"].get(); - audioSequenceOffset = json["audioSequenceOffset"].get(); - ttsVoiceSpeed = json["ttsVoiceSpeed"].get(); - ttsVoiceVolume = json["ttsVoiceVolume"].get(); - approvedUsers = json["approvedUsers"].get>(); + notifAnimationLength = + json["notifAnimationLength"].get().value_or(notifAnimationLength); + notifEffectSpeed = json["notifEffectSpeed"].get().value_or(notifEffectSpeed); + notifEffectIntensity = + json["notifEffectIntensity"].get().value_or(notifEffectIntensity); + notifFontScale = json["notifFontScale"].get().value_or(notifFontScale); + globalAudioVolume = json["globalAudioVolume"].get().value_or(globalAudioVolume); + twitchAuthToken = json["twitchAuthToken"].get().value_or(twitchAuthToken); + twitchAuthUser = json["twitchAuthUser"].get().value_or(twitchAuthUser); + twitchChannel = json["twitchChannel"].get().value_or(twitchChannel); + enabledCooldowns = json["enabledCooldowns"].get().value_or(enabledCooldowns); + cooldownTime = json["cooldownTime"].get().value_or(cooldownTime); + maxAudioTriggers = json["maxAudioTriggers"].get().value_or(maxAudioTriggers); + audioSequenceOffset = + json["audioSequenceOffset"].get().value_or(audioSequenceOffset); + ttsVoiceSpeed = json["ttsVoiceSpeed"].get().value_or(ttsVoiceSpeed); + ttsVoiceVolume = json["ttsVoiceVolume"].get().value_or(ttsVoiceVolume); + approvedUsers = + json["approvedUsers"].get>().value_or(approvedUsers); return Result(); } diff --git a/Source/effect.cppm b/Source/effect.cppm index 069d7a2..d1a8840 100644 --- a/Source/effect.cppm +++ b/Source/effect.cppm @@ -301,7 +301,7 @@ public: } }; -// Transition effect of coming from top to bottom of the screen +// Transition effect of scrolling past the screen export class TextEffectTransition final : public TextEffect { public: ~TextEffectTransition() override = default; @@ -312,9 +312,7 @@ public: -> TextEffectData override { auto newEffectData = effectData; - const auto transitionY = - std::lerp(-effectData.textSize.y, - ImGui::GetWindowHeight() / 2.0f - effectData.textSize.y / 2.0f, time); + const auto transitionY = std::lerp(-effectData.textSize.y, ImGui::GetWindowHeight(), time); newEffectData.position = ImVec2(0.0f, transitionY); return newEffectData; diff --git a/Source/gui.cppm b/Source/gui.cppm index 565017c..10c8a84 100644 --- a/Source/gui.cppm +++ b/Source/gui.cppm @@ -202,8 +202,8 @@ public: // Slider for notification effect intensity, float from 0.1 to 10.0 ImGui::Text("Notification effect intensity:"); - ImGui::SliderFloat("##effectIntensity", &global_config.notifEffectIntensity, 0.1f, 10.0f, - "%.1f"); + ImGui::SliderFloat("##effectIntensity", &global_config.notifEffectIntensity, 0.1f, + 10.0f, "%.1f"); // Slider for notification font scale, float from 0.5 to 2.0 ImGui::Text("Notification font scale:"); @@ -261,7 +261,8 @@ public: // Slider for TTS voice volume, which is a float from 0.0f to 1.0f ImGui::Text("TTS voice volume:"); - ImGui::SliderFloat("##ttsVoiceVolume", &global_config.ttsVoiceVolume, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat("##ttsVoiceVolume", &global_config.ttsVoiceVolume, 0.0f, 1.0f, + "%.2f"); // Add padding before separators ImGui::Dummy(ImVec2(0, 10)); @@ -274,18 +275,25 @@ public: ImGui::Text("Twitch Settings"); ImGui::Separator(); - // Dropdown for cooldown type - ImGui::Text("Command cooldown type:"); - constexpr std::array cooldownTypes = {"None", "Global", "Per User"}; - ImGui::Combo("##cooldownType", reinterpret_cast(&global_config.cooldownType), - cooldownTypes.data(), cooldownTypes.size()); + // Multiselect for enabledCooldowns bitmask (eNone, eGlobal, ePerUser, ePerCommand) + ImGui::Text("Enabled cooldowns:"); + ImGui::CheckboxFlags("Global", &*global_config.enabledCooldowns, + static_cast(CommandCooldownType::eGlobal)); + ImGui::SameLine(); + ImGui::CheckboxFlags("Per User", &*global_config.enabledCooldowns, + static_cast(CommandCooldownType::ePerUser)); + ImGui::SameLine(); + ImGui::CheckboxFlags("Per Command", &*global_config.enabledCooldowns, + static_cast(CommandCooldownType::ePerCommand)); // Input box for cooldown time - if (global_config.cooldownType != CommandCooldownType::eNone) { + ImGui::BeginDisabled(global_config.enabledCooldowns == CommandCooldownType::eNone); + { ImGui::Text("Cooldown time:"); ImGui::InputScalar("##cooldownTime", ImGuiDataType_U32, &global_config.cooldownTime); } + ImGui::EndDisabled(); // Padding ImGui::Dummy(ImVec2(0, 10)); @@ -454,11 +462,6 @@ public: m_notifications.emplace_back(std::make_unique(std::move(text))); } - // Method to return approved users - [[nodiscard]] static auto get_approved_users() -> const std::vector & { - return global_config.approvedUsers; - } - private: // Returns string depending on connection status and result static auto get_connection_status_string(const ConnectionStatus status, const Result &res) diff --git a/Source/jsoned.cppm b/Source/jsoned.cppm index 7fdcf3c..9bda09a 100644 --- a/Source/jsoned.cppm +++ b/Source/jsoned.cppm @@ -7,16 +7,19 @@ module; #include #include #include +#include #include #include +#include export module jsoned; import common; template -concept JsonableAsValue = std::same_as || std::is_integral_v || - std::same_as || std::is_floating_point_v; +concept JsonableAsValue = + std::same_as || std::is_integral_v || std::same_as || + std::is_floating_point_v || std::is_integral_v>; template concept JsonableAsArray = requires(T t) { @@ -38,30 +41,16 @@ export namespace JSONed { template requires(!JsonableAsArray) void set(const T &v) { - if constexpr (std::is_integral_v || std::is_floating_point_v || - std::same_as) - value = std::to_string(v); - else - value = v; + value = t_to_string(v); } template requires(!JsonableAsArray) - [[nodiscard]] auto get() const -> T { + [[nodiscard]] auto get() const -> std::optional { // Make sure the value is not empty - if (value.empty()) - return T{}; + if (value.empty()) return std::nullopt; - if constexpr (std::same_as) - return value; - else if constexpr (std::is_integral_v) - return std::stoi(value); - else if constexpr (std::is_floating_point_v) - return std::stof(value); - else if constexpr (std::same_as) - return value == "true"; - else - return T{}; + return std::make_optional(t_from_string(value)); } auto to_string() const -> std::string { return std::format("\"{}\"", value); } @@ -92,11 +81,10 @@ export namespace JSONed { requires(!JsonableAsValue) [[nodiscard]] auto get() const -> T { // Make sure the values is not empty - if (values.empty()) - return T{}; + if (values.empty()) return T{}; T array; - for (const auto &value : values) array.push_back(value.get()); + for (const auto &value : values) array.push_back(value.get().value()); return array; } @@ -115,8 +103,7 @@ export namespace JSONed { ArrayAccessor accessor; const auto trimmed = trim_string(str); const auto between = get_string_between(trimmed, "[", "]"); - if (between.empty()) - return accessor; + if (between.empty()) return accessor; for (const auto splitted = split_string(between, ","); const auto &elem : splitted) { auto trimmedElem = trim_string(elem); @@ -162,13 +149,13 @@ export namespace JSONed { template requires(!JsonableAsArray) - [[nodiscard]] auto get() const -> T { + [[nodiscard]] auto get() const -> std::optional { return std::get(m_accessor).get(); } template requires(JsonableAsArray) - [[nodiscard]] auto get() const -> T { + [[nodiscard]] auto get() const -> std::optional { return std::get(m_accessor).get(); } @@ -183,8 +170,7 @@ export namespace JSONed { public: // Accessor for JSON data operators auto operator[](const std::string &key) -> Accessor & { - if (!m_data.contains(key)) - m_data[key] = Accessor(); + if (!m_data.contains(key)) m_data[key] = Accessor(); return m_data[key]; } @@ -192,8 +178,7 @@ export namespace JSONed { // Save JSON to file auto save(const std::filesystem::path &path) -> bool { std::ofstream file(path); - if (!file.is_open()) - return false; + if (!file.is_open()) return false; file << "{\n"; for (const auto &[key, value] : m_data) { @@ -211,8 +196,7 @@ export namespace JSONed { // Load JSON from file auto load(const std::filesystem::path &path) -> bool { std::ifstream file(path); - if (!file.is_open()) - return false; + if (!file.is_open()) return false; std::string line; while (std::getline(file, line)) { @@ -220,13 +204,11 @@ export namespace JSONed { auto trimmed = trim_string(line); trimmed = trim_string(trimmed, "\""); trimmed = trim_string(trimmed, ","); - if (trimmed.empty()) - continue; + if (trimmed.empty()) continue; // Split key and value const auto splitted = split_string(trimmed, ":"); - if (splitted.size() != 2) - continue; + if (splitted.size() != 2) continue; auto key = splitted[0]; key = trim_string(key); diff --git a/Source/main.cpp b/Source/main.cpp index 36b6015..3498dc1 100644 --- a/Source/main.cpp +++ b/Source/main.cpp @@ -17,8 +17,7 @@ import tts; // Method for printing Result errors void print_error(const Result &res) { - if (!res) - std::cerr << "Error: " << res.message << std::endl; + if (!res) std::cerr << "Error: " << res.message << std::endl; } void twc_callback_handler(const TwitchChatMessage &msg); @@ -97,23 +96,19 @@ auto main(int argc, char **argv) -> int { } void twc_callback_handler(const TwitchChatMessage &msg) { - // If message does not begin with "!", skip - if (msg.message[0] != '!') - return; - // Check that the user is in the approvedUsers list, if empty just allow it - const auto approvedUsers = NotifierGUI::get_approved_users(); - if (approvedUsers.empty() || std::ranges::any_of(approvedUsers, [&](const auto &user) { + if (global_config.approvedUsers.empty() || + std::ranges::any_of(global_config.approvedUsers, [&](const auto &user) { return lowercase(user) == lowercase(msg.user); })) { // Check for command and execute for (const auto &[key, command] : CommandHandler::get_commands_map()) { // Command with the "!" prefix to match the message const auto fullCommand = std::format("!{}", command.callstr); + if (fullCommand.empty()) continue; // Make sure the message is long enough to contain the command - if (msg.message.size() < fullCommand.size()) - continue; + if (msg.message.size() < fullCommand.size()) continue; // Extract the command from the message // so until whitespace is met after command or end of string is reached @@ -127,15 +122,11 @@ void twc_callback_handler(const TwitchChatMessage &msg) { }); // If command is shorter than the extracted command, skip, prevents partial matches - if (fullCommand.size() < extractedCommand.size()) - continue; + if (fullCommand.size() < extractedCommand.size()) continue; // Strict comparison of the command, to prevent partial matches if (lowercase(extractedCommand).find(fullCommand) != std::string::npos) { - // Cut off the command from the message + space if there is one - const auto msgWithoutCommand = msg.message.substr( - fullCommand.size() + (msg.message[fullCommand.size()] == ' ')); - CommandHandler::execute_command(key, msgWithoutCommand); + CommandHandler::execute_command(key, msg); break; } } diff --git a/Source/tts.cppm b/Source/tts.cppm index 3003bd2..fbac140 100644 --- a/Source/tts.cppm +++ b/Source/tts.cppm @@ -69,10 +69,12 @@ public: } } - static void voiceString(const std::string &text) { + static auto get_num_voices() -> std::int32_t { return SherpaOnnxOfflineTtsNumSpeakers(m_tts); } + + static void voiceString(const std::string &text, std::int32_t speakerID = -1) { // Do in separate thread - m_threads.emplace_back([text]() { - const auto speakerID = random_int(0, 108); + m_threads.emplace_back([text, &speakerID]() { + if (speakerID == -1) speakerID = random_int(0, get_num_voices() - 1); const auto audio = SherpaOnnxOfflineTtsGenerate(m_tts, text.c_str(), speakerID, global_config.ttsVoiceSpeed); const std::vector audiodata(audio->samples, audio->samples + audio->n); diff --git a/Source/twitch.cppm b/Source/twitch.cppm index 33caaa6..b607697 100644 --- a/Source/twitch.cppm +++ b/Source/twitch.cppm @@ -2,8 +2,8 @@ module; #include #include -#include #include +#include #include #include @@ -12,10 +12,13 @@ export module twitch; import config; import common; +import commands; +import tts; -// Struct for Twitch message data -export struct TwitchChatMessage { - std::string user, message; +// We use bitmask operators for CommandCooldownType here +template <> +struct FEnableBitmaskOperators { + static constexpr bool enable = true; }; // Enum class of connection status @@ -31,10 +34,8 @@ export class TwitchChatConnector { static inline TwitchChatMessageCallback m_onMessage; - // Keep track of user -> last command time for cooldowns + // Global cooldown time static inline std::chrono::time_point m_lastCommandTime; - static inline std::map> - m_lastCommandTimes; public: // Initializes the connector resources, with given callback @@ -46,15 +47,13 @@ public: // Cleans up resources used by the connector, disconnecting first if connected static void cleanup() { - if (m_connStatus > ConnectionStatus::eDisconnected) - disconnect(); + if (m_connStatus > ConnectionStatus::eDisconnected) disconnect(); } // Connects to the given channel's chat static auto connect() -> Result { // If already connected or any parameter is empty, return - if (m_connStatus > ConnectionStatus::eDisconnected) - return Result(1, "Already connected"); + if (m_connStatus > ConnectionStatus::eDisconnected) return Result(1, "Already connected"); // Set to connecting m_connStatus = ConnectionStatus::eConnecting; @@ -80,8 +79,7 @@ public: // Disconnects existing connection static void disconnect() { // If not connected, return - if (m_connStatus == ConnectionStatus::eDisconnected) - return; + if (m_connStatus == ConnectionStatus::eDisconnected) return; m_client.close(); m_connStatus = ConnectionStatus::eDisconnected; @@ -130,29 +128,57 @@ private: // Handlers std::erase(chat, '\r'); std::erase(chat, '\t'); - // Check cooldown (global_config.cooldownType + global_config.cooldownTime) before - // calling the callback - switch (global_config.cooldownType) { - case CommandCooldownType::eGlobal: { - if (std::chrono::steady_clock::now() - m_lastCommandTime < - std::chrono::seconds(global_config.cooldownTime)) + const auto chatMsg = TwitchChatMessage(user, chat); + if (!chatMsg.is_command()) { + if (!global_users.contains(user)) { + const auto twUser = std::make_shared(user, chatMsg); + twUser->userVoice = random_int(0, TTSHandler::get_num_voices() - 1); + global_users[user] = twUser; + } else + global_users[user]->lastMessage = chatMsg; + + return; + } + + // Check cooldowns (global_config.enabledCooldowns, global_config.cooldownTime) + // before calling the callback + if (global_config.enabledCooldowns & CommandCooldownType::eGlobal) { + const auto now = std::chrono::steady_clock::now(); + if (now - m_lastCommandTime < std::chrono::seconds(global_config.cooldownTime)) return; - m_lastCommandTime = std::chrono::steady_clock::now(); - break; + + m_lastCommandTime = now; } - case CommandCooldownType::ePerUser: { - if (std::chrono::steady_clock::now() - m_lastCommandTimes[user] < - std::chrono::seconds(global_config.cooldownTime)) + if (global_config.enabledCooldowns & CommandCooldownType::ePerUser) { + if (global_users.contains(user)) { + if (const auto now = std::chrono::steady_clock::now(); + now - global_users[user]->lastMessage.time < + std::chrono::seconds(global_config.cooldownTime) && + !global_users[user]->bypassCooldown) + return; + } + } + if (global_config.enabledCooldowns & CommandCooldownType::ePerCommand) { + if (!chatMsg.is_command()) return; + // Get the command from the message + const auto extractedCommand = chatMsg.get_command(); + const auto cmdLastExec = CommandHandler::get_last_executed_time( + CommandHandler::get_command_key(extractedCommand)); + if (const auto now = std::chrono::steady_clock::now(); + now - cmdLastExec < std::chrono::seconds(global_config.cooldownTime)) return; - m_lastCommandTimes[user] = std::chrono::steady_clock::now(); - break; - case CommandCooldownType::eNone: - default: - break; } + + if (chatMsg.is_command()) { + if (!global_users.contains(user)) { + const auto twUser = std::make_shared(user, chatMsg); + twUser->userVoice = random_int(0, TTSHandler::get_num_voices() - 1); + global_users[user] = twUser; + } else + global_users[user]->lastMessage = chatMsg; } - m_onMessage({user, chat}); + m_onMessage(chatMsg); } }