diff --git a/CMakeLists.txt b/CMakeLists.txt index b515e99..475c2db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,14 +10,37 @@ find_package(CURL REQUIRED) find_package(nlohmann_json 3.2.0 REQUIRED) # Определяем исполняемый файл -add_executable(tzeva_adom main.cpp) +add_executable(tzeva_adom main.cpp + models/AlertResponse.cpp + utils/image_downloader.cpp + utils/audio_play.cpp + locale/localization_get.cpp + utils/localization_manager.h + utils/localization_manager.h +) find_package(PkgConfig REQUIRED) pkg_check_modules(GLIB REQUIRED glib-2.0) pkg_check_modules(NOTIFY REQUIRED libnotify) +pkg_check_modules(SDL REQUIRED libnotify) find_package(fmt REQUIRED) +include_directories(lang) include_directories(${GLIB_INCLUDE_DIRS}) include_directories(${NOTIFY_INCLUDE_DIRS}) include_directories(${CURL_INCLUDE_DIRS}) + +find_package(SDL2 REQUIRED) +find_package(SDL2_mixer REQUIRED) +include_directories(${SDL2_INCLUDE_DIRS} ${SDL2_MIXER_INCLUDE_DIRS}) +find_package(Boost REQUIRED COMPONENTS filesystem) +include_directories(${Boost_INCLUDE_DIRS}) + + # Линкуем библиотеки -target_link_libraries(tzeva_adom PRIVATE CURL::libcurl nlohmann_json::nlohmann_json ${GLIB_LIBRARIES} fmt::fmt ${NOTIFY_LIBRARIES} ${CURL_LIBRARIES}) +target_include_directories(tzeva_adom PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} "lang/") +target_link_libraries(tzeva_adom PRIVATE SDL2 SDL2_mixer ${Boost_LIBRARIES} CURL::libcurl nlohmann_json::nlohmann_json ${GLIB_LIBRARIES} fmt::fmt ${NOTIFY_LIBRARIES} ${CURL_LIBRARIES}) + +add_custom_command(TARGET tzeva_adom POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/lang $/lang +) \ No newline at end of file diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..22ce652 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,19 @@ +{ + "threat_0": "Red alert", + "threat_1": "Hazardous Materials Incident", + "threat_2": "Fear of Terrorists infiltration", + "threat_3": "Earthquake", + "threat_4": "Fear of a tsunami", + "threat_5": "Hostile aircraft intrusion", + "threat_6": "Fear of a Radiological incident", + "threat_7": "Non-conventional missile", + "threat_8": "Alert", + "threat_9": "Home Front Command Drill", + "default": "Unnamed", + "drill": "Drill: ", + "true": "yes", + "false": "no", + "threat": "Threat: ", + "cities": "Cities: " + +} \ No newline at end of file diff --git a/lang/he.json b/lang/he.json new file mode 100644 index 0000000..a5b291e --- /dev/null +++ b/lang/he.json @@ -0,0 +1,18 @@ +{ + "threat_0": "צבע אדום", + "threat_1": "אירוע חומרים מסוכנים", + "threat_2": "חשש לחדירת מחבלים", + "threat_3": "רעידת אדמה", + "threat_4": "חשש לצונאמי", + "threat_5": "חדירת כלי טיס עוין", + "threat_6": "חשש לאירוע רדיולוגי", + "threat_7": "ירי בלתי קונבנציונלי", + "threat_8": "התרעה", + "threat_9": "תרגיל פיקוד העורף", + "default": "ללא שם", + "drill": "תרגיל: ", + "true": "כן", + "false": "לא", + "threat": "איום: ", + "cities": "הסדר: " +} \ No newline at end of file diff --git a/lang/ru.json b/lang/ru.json new file mode 100644 index 0000000..56b5b58 --- /dev/null +++ b/lang/ru.json @@ -0,0 +1,18 @@ +{ + "threat_0": "Цева адом", + "threat_1": "Утечка опасных веществ", + "threat_2": "Подозрение на проникновение террористов", + "threat_3": "Землетрясение", + "threat_4": "Угроза цунами", + "threat_5": "Проникновение беспилотного самолета", + "threat_6": "Радиоактивная опасность", + "threat_7": "Неконвенциональная ракета", + "threat_8": "предупреждение", + "threat_9": "Учения Службы Тыла", + "default": "Неназвано", + "drill": "упражнения: ", + "true": "да", + "false": "нет", + "threat": "Тип: ", + "cities": "Города: " +} \ No newline at end of file diff --git a/locale/localization_get.cpp b/locale/localization_get.cpp new file mode 100644 index 0000000..2d41dd2 --- /dev/null +++ b/locale/localization_get.cpp @@ -0,0 +1,3 @@ +// +// Created by yawaflua on 29/10/2024. +// diff --git a/main.cpp b/main.cpp index edf157b..2f293cb 100644 --- a/main.cpp +++ b/main.cpp @@ -4,16 +4,30 @@ #include #include #include -#include #include #include "spdlog/spdlog.h" #include #include +#include +#include +#include "utils/audio_play.cpp" +#include "models/AlertResponse.cpp" +#include "utils/image_downloader.cpp" +#include "utils/localization_manager.h" + +class AlertResponse; + +tzeva_adom::LocalizationManager localization_manager; using json = nlohmann::json; int16_t lastId = 0; json cities_n_areas_list; bool is_cities_loaded = false; +bool is_test = false; +std::string this_path = boost::filesystem::current_path().c_str(); +std::string lang = "en"; +std::vector test_alert_variable; + // Функция для записи данных от curl size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) { output->append(static_cast(contents), size * nmemb); @@ -38,91 +52,90 @@ void process_alert(const std::string& data) { } spdlog::debug("Get first object from answer"); - // Извлекаем первый объект из массива - const json& first_alert = response[0]; + auto* first_alert = new tzeva_adom::AlertResponseElement(response.at(0)); - spdlog::debug("Answer: {}", first_alert); + spdlog::debug("Answer: {}", first_alert->get_id()); - auto id = first_alert["id"].get(); + auto id = first_alert->get_id(); if (lastId == 0 || id == lastId) { lastId = id; } else { is_alert = !is_alert; + lastId = id; spdlog::debug("This is alert!"); } - if (is_alert) { - auto description = first_alert["description"].is_string() ? first_alert["description"].get() : ""; + if (is_alert || is_test) { + is_test = !is_test; + // auto description = first_alert["description"].is_string() ?"": first_alert["description"].get() ; + std::string description = ""; + const std::vector alerts_array = is_test ? test_alert_variable : first_alert->get_alerts(); + std::vector cities; + std::vector threats; - for (auto alerts: first_alert["alerts"]) { - auto time = alerts["time"].get(); - auto cities = alerts["cities"].get>(); - auto threat = alerts["threat"].get(); - auto isDrill = alerts["isDrill"].get(); + for (auto alerts: alerts_array) { + auto time = alerts.get_time(); + for (auto city: alerts.get_cities()) { + cities.insert(cities.begin(), city); + } - spdlog::debug("Time: {}", time); - spdlog::debug("Cities: {}", fmt::join(cities, ", ")); - - spdlog::debug("Threat Level: {}", threat); - spdlog::debug("Is Drill: {}", isDrill); + threats.insert(threats.begin(), alerts.get_threat()); + auto isDrill = alerts.get_is_drill(); - is_png = threat == 0; - std::string icon_url = fmt::format("https://www.tzevaadom.co.il/static/images/threat{}.{}", std::to_string(threat), is_png ? ".png" : ".svg"); - switch (threat) { - case 0: - type_of_threat = "Red Alert"; - break; - case 1: - break; - case 2: - type_of_threat = "Fear of Terrorists infiltration"; - break; - case 3: - type_of_threat = "Earthquakes warning"; - break; - case 4: - type_of_threat = "Tsunami warning"; - break; - case 5: - type_of_threat = "Hostile aircraft intrusion"; - break; - default: - type_of_threat = "Unnamed"; - } + spdlog::debug("Time: {}", time); + spdlog::debug("Cities: {}", fmt::join(cities, ", ")); - std::string localised_cities_names = ""; - for (auto city: cities) { - localised_cities_names += fmt::format("{} ", cities_n_areas_list["cities"][city]["ru"]); - } + spdlog::debug("Threat Level: {}", alerts.get_threat()); + spdlog::debug("Is Drill: {}", isDrill); + } - notify_init("Tzeva Adom!"); - NotifyNotification* n = notify_notification_new (type_of_threat.c_str(), - fmt::format( - "Cities: {}\n"\ - "Threat: {}\n"\ - "Is it drill: {}", - localised_cities_names, - type_of_threat, - std::to_string(is_drill) - ).c_str(), - icon_url.c_str() - ); - notify_notification_set_timeout(n, 10000); // 10 seconds + int threat = *std::max_element( threats.begin(), threats.end() ); + std::string icon_url = boost::filesystem::current_path().c_str()+fmt::format("/threat{}.{}", std::to_string(threat), threat == 0 ? "png" : "svg"); - if (!notify_notification_show(n, 0)) - { - std::cerr << "show has failed" << std::endl; - return; - } + spdlog::debug("Threat: {}", threat); + spdlog::debug("Threat name: {}", localization_manager.getString(fmt::format("threat_{}", threat))); + spdlog::debug("Language: {}", lang); + spdlog::debug("Icon path: {}", icon_url); + + std::string localised_cities_names = ""; + for (auto city: cities) { + localised_cities_names += fmt::format("{} ", cities_n_areas_list["cities"][city][lang]); + } + + spdlog::debug("Init notification"); + notify_init(localization_manager.getString(fmt::format("threat_{}", std::to_string(threat))).c_str()); + NotifyNotification* n = notify_notification_new (localization_manager.getString(fmt::format("threat_{}", std::to_string(threat))).c_str(), + fmt::format( + "{}: {}\n"\ + "{}: {}\n"\ + "{}: {}", + localization_manager.getString("cities"), + localised_cities_names, + localization_manager.getString("threat"), + localization_manager.getString(fmt::format("threat_{}", std::to_string(threat))), + localization_manager.getString("drill"), + localization_manager.getString(is_drill ? "true" : "false") + ).c_str(), + icon_url.c_str() + ); + tzeva_adom::playAudioAsync((this_path + std::string("/bell.mp3")).c_str()); + notify_notification_set_timeout(n, 10000); // 10 seconds + + if (!notify_notification_show(n, 0)) + { + std::cerr << "show has failed" << std::endl; return; } + return; } + } catch (const std::exception& e) { spdlog::error("Alert error: {}", e.what()); + } } @@ -150,7 +163,7 @@ void fetch_alerts_history(std::atomic& running) { } else { cities_n_areas_list = json::parse(readBuffer); } - curl_easy_cleanup(curl); + curl_global_cleanup(); } if (curl) { @@ -191,9 +204,35 @@ int main(int argc, char** argv) { spdlog::info("========================================================="); spdlog::set_level(spdlog::level::info); // Set global log level to info by default + std::string last_flag = ""; + bool is_accept_values = false; + for (int i = 0; i < argc; i++) { + if (argv[i] == "-d"sv || argv[i] == "--debug") spdlog::set_level(spdlog::level::debug); // Set global log level to debug if provided --debug + else if (argv[i] == "-t"sv || argv[i] == "--test") { + is_test = true; // Set tested variable is true + std::vector alert_to_array {tzeva_adom::Alert()}; + const auto p1 = std::chrono::system_clock::now(); + ulong t = std::chrono::duration_cast( + p1.time_since_epoch()).count(); + std::vector city_vectors{"אשדוד -יא,יב,טו,יז,מרינה,סיט", "אשדוד - ח,ט,י,יג,יד,טז"}; + alert_to_array[0].set_cities(city_vectors); + alert_to_array[0].set_time(t); + alert_to_array[0].set_threat(0); + test_alert_variable.push_back(alert_to_array[0]); + } + else if (argv[i] == "-l"sv || argv[i] == "--lang"sv) { + is_accept_values = true; + spdlog::debug("Take lang arg"); + } + else if ((last_flag == "-l"sv || last_flag == "--lang"sv) && is_accept_values == true) { + is_accept_values = false; + lang = argv[i]; + spdlog::debug("Language setted to {}", lang); + + } else if (argv[i] == "-h"sv || argv[i] == "--help"sv) { spdlog::info( "Tzeva-Adom PC 1.0 by yawaflua\n\n"\ @@ -201,9 +240,34 @@ int main(int argc, char** argv) { " -h --help: Show this message\n"\ " -d --debug: Show debug messages\n"\ " -t --test: Create test alert end exit\n"\ + " -l --lang: Choose language: ru, en, he," ""); return 0; } + last_flag = argv[i]; + } + + spdlog::debug("Path: {}", boost::filesystem::current_path().c_str()); + + for (int i = 0; i <= 5; i++) { + if (!boost::filesystem::exists(this_path+fmt::format("/threat{}.{}", std::to_string(i), i == 0 ? "png" : "svg"))) { + tzeva_adom::download_file( + fmt::format( + "https://www.tzevaadom.co.il/static/images/threat{}.{}", + std::to_string(i), i == 0 ? "png" : "svg"), + fmt::format("threat{}.{}", std::to_string(i), i == 0 ? "png" : "svg") + ); + + } + } + localization_manager = tzeva_adom::LocalizationManager(); + localization_manager.setCurrentLanguage(lang); + //Download tzeva-adom bell sound + if (!boost::filesystem::exists(this_path+fmt::format("/bell.mp3"))) { + tzeva_adom::download_file( + "https://www.tzevaadom.co.il/static/sounds/bell.mp3", + fmt::format("bell.mp3") + ); } // Флаг для контроля остановки потока diff --git a/models/AlertResponse.cpp b/models/AlertResponse.cpp new file mode 100644 index 0000000..b1e877b --- /dev/null +++ b/models/AlertResponse.cpp @@ -0,0 +1,124 @@ +// To parse this JSON data, first install +// +// Boost http://www.boost.org +// json.hpp https://github.com/nlohmann/json +// +// Then include this file, and then do +// +// AlertResponse data = nlohmann::json::parse(jsonString); + +#pragma once + +#include +#include +#include +#include + +namespace tzeva_adom { + using nlohmann::json; + + #ifndef NLOHMANN_UNTYPED_tzeva_adom_HELPER + #define NLOHMANN_UNTYPED_tzeva_adom_HELPER + inline json get_untyped(const json& j, const char * property) { + if (j.find(property) != j.end()) { + return j.at(property).get(); + } + return json(); + } + + inline json get_untyped(const json & j, std::string property) { + return get_untyped(j, property.data()); + } + #endif + + class Alert { + public: + Alert() = default; + virtual ~Alert() = default; + + private: + int64_t time; + std::vector cities; + int64_t threat; + bool is_drill; + + public: + const int64_t & get_time() const { return time; } + int64_t & get_mutable_time() { return time; } + void set_time(const int64_t & value) { this->time = value; } + + const std::vector & get_cities() const { return cities; } + std::vector & get_mutable_cities() { return cities; } + void set_cities(const std::vector & value) { this->cities = value; } + + const int64_t & get_threat() const { return threat; } + int64_t & get_mutable_threat() { return threat; } + void set_threat(const int64_t & value) { this->threat = value; } + + const bool & get_is_drill() const { return is_drill; } + bool & get_mutable_is_drill() { return is_drill; } + void set_is_drill(const bool & value) { this->is_drill = value; } + }; + + class AlertResponseElement { + public: + AlertResponseElement() = default; + virtual ~AlertResponseElement() = default; + + private: + int64_t id; + nlohmann::json description; + std::vector alerts; + + public: + const int64_t & get_id() const { return id; } + int64_t & get_mutable_id() { return id; } + void set_id(const int64_t & value) { this->id = value; } + + const nlohmann::json & get_description() const { return description; } + nlohmann::json & get_mutable_description() { return description; } + void set_description(const nlohmann::json & value) { this->description = value; } + + const std::vector & get_alerts() const { return alerts; } + std::vector & get_mutable_alerts() { return alerts; } + void set_alerts(const std::vector & value) { this->alerts = value; } + }; + + using AlertResponse = std::vector; +} + +namespace tzeva_adom { + void from_json(const json & j, Alert & x); + void to_json(json & j, const Alert & x); + + void from_json(const json & j, AlertResponseElement & x); + void to_json(json & j, const AlertResponseElement & x); + + inline void from_json(const json & j, Alert& x) { + x.set_time(j.at("time").get()); + x.set_cities(j.at("cities").get>()); + x.set_threat(j.at("threat").get()); + x.set_is_drill(j.at("isDrill").get()); + } + + inline void to_json(json & j, const Alert & x) { + j = json::object(); + j["time"] = x.get_time(); + j["cities"] = x.get_cities(); + j["threat"] = x.get_threat(); + j["isDrill"] = x.get_is_drill(); + } + + inline void from_json(const json & j, AlertResponseElement& x) { + x.set_id(j.at("id").get()); + x.set_description(get_untyped(j, "description")); + x.set_alerts(j.at("alerts").get>()); + } + + inline void to_json(json & j, const AlertResponseElement & x) { + j = json::object(); + j["id"] = x.get_id(); + j["description"] = x.get_description(); + j["alerts"] = x.get_alerts(); + } +} diff --git a/utils/audio_play.cpp b/utils/audio_play.cpp new file mode 100644 index 0000000..875070f --- /dev/null +++ b/utils/audio_play.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +namespace tzeva_adom { + inline std::pmr::string filename; + // Асинхронная функция для воспроизведения аудио + inline void playAudio() { + if (SDL_Init(SDL_INIT_AUDIO) < 0) { + std::cerr << "Ошибка инициализации SDL: " << SDL_GetError() << std::endl; + return; + } + + if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { + std::cerr << "Ошибка открытия аудио: " << Mix_GetError() << std::endl; + SDL_Quit(); + return; + } + + Mix_Music* music = Mix_LoadMUS(filename.c_str()); + if (!music) { + std::cerr << "Ошибка загрузки MP3: " << Mix_GetError() << std::endl; + } else { + Mix_PlayMusic(music, 1); + + // Ожидание завершения воспроизведения + while (Mix_PlayingMusic() != 0) { + SDL_Delay(100); // Пауза для проверки состояния воспроизведения + } + + Mix_FreeMusic(music); + } + + Mix_CloseAudio(); + SDL_Quit(); + } + + // Функция для запуска воспроизведения аудио в отдельном потоке + inline void playAudioAsync(std::pmr::string file) { + filename = file; + std::thread audioThread(playAudio); + audioThread.detach(); // Отделяем поток, чтобы он работал асинхронно + } +} diff --git a/utils/image_downloader.cpp b/utils/image_downloader.cpp new file mode 100644 index 0000000..8c834fc --- /dev/null +++ b/utils/image_downloader.cpp @@ -0,0 +1,34 @@ +// +// Created by yawaflua on 29/10/2024. +// +#pragma once + +#include +#include +#include + +namespace tzeva_adom { + inline size_t writeImageData(void* ptr, size_t size, size_t nmemb, FILE* stream) { + size_t written = fwrite(ptr, size, nmemb, stream); + return written; + } + + inline void download_file(std::string url, std::string filename) { + CURL* curl = curl_easy_init(); + if (curl) { + FILE* fp = fopen(filename.c_str(), "wb"); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeImageData); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + CURLcode res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + fclose(fp); + + if (res != CURLE_OK) { + std::cout << "Failed to download image: " << curl_easy_strerror(res) << std::endl; + } else { + std::cout << "Image downloaded successfully: " << filename << std::endl; + } + } + } +} \ No newline at end of file diff --git a/utils/localization_manager.h b/utils/localization_manager.h new file mode 100644 index 0000000..b34376e --- /dev/null +++ b/utils/localization_manager.h @@ -0,0 +1,58 @@ +#ifndef LOCALIZATION_MANAGER_H +#define LOCALIZATION_MANAGER_H + +#include +#include +#include +#include +#include // Подключение библиотеки JSON + +namespace tzeva_adom { + class LocalizationManager { + public: + LocalizationManager() : currentLanguage("en") {} + + // Загрузить языковой файл + bool loadLanguage(std::string lang) { + std::string path = std::filesystem::current_path().c_str() + fmt::format("/lang/{}.json", lang); + std::ifstream file(path); + if (!file.is_open()) { + std::cerr << "Failed to open localization file: " << lang << std::endl; + std::cerr << "Failed to open localization file: " << path << std::endl; + return false; + } + + nlohmann::json json; + file >> json; + translations[lang] = json; + return true; + } + + // Установить текущий язык + void setCurrentLanguage(const std::string& langCode) { + loadLanguage(langCode); + if (translations.find(langCode) != translations.end()) { + currentLanguage = langCode; + + } else { + std::cerr << "Language code not loaded: " << langCode << std::endl; + } + } + + // Получить переведенную строку по ключу + std::string getString(const std::string& key) const { + if (!translations.find(currentLanguage)->second.empty()) { + auto& langData = translations.at(currentLanguage); + if (langData.contains(key)) { + return langData[key]; + } + } + return "Translation not found"; + } + + private: + std::unordered_map translations; + std::string currentLanguage; + }; +} +#endif // LOCALIZATION_MANAGER_H