diff --git a/CMakeLists.txt b/CMakeLists.txt index 56656d2..a0ea70c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,8 @@ add_library(tg-cli-lib src/controllers/AuthController.cpp src/controllers/GetChatsController.cpp src/controllers/SendMessageController.cpp + src/controllers/ChatHistoryController.cpp + src/facade/TgClientFacade.cpp ) target_include_directories(tg-cli-lib diff --git a/main/main.cpp b/main/main.cpp index 0e4fa38..8897cf2 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,25 +1,20 @@ -#include "../src/tgClient/ITgClient.hpp" -#include "../src/tgClient/TgClientTdlib.hpp" +// main/main.cpp #include -int main() { - TgClientTdlib a = TgClientTdlib(); - auto temp = a.check_status(); +#include "../src/tgClient/TgClientTdlib.hpp" +#include "../src/facade/TgClientFacade.h" + +int main(int argc, char** argv) { + try { + TgClientTdlib client; + TgClientFacade facade(client); - std::cout << "waitingPhone " << (temp == TgClientTdlib::AuthState::WaitingPhone) << std::endl; - std::cout << "WaitingCode " << (temp == TgClientTdlib::AuthState::WaitingCode) << std::endl; - std::cout << "WaitingPassword " << (temp == TgClientTdlib::AuthState::WaitingPassword) << std::endl; - std::cout << "WaitingParameters " << (temp == TgClientTdlib::AuthState::WaitingTdlibParameters) << std::endl; - std::cout << "Ready " << (temp == TgClientTdlib::AuthState::Ready) << std::endl; - std::cout << "LogginOut " << (temp == TgClientTdlib::AuthState::LoggingOut) << std::endl; - std::cout << "error " << (temp == TgClientTdlib::AuthState::Error) << std::endl; - enum class AuthState { - WaitingPhone, - WaitingCode, - WaitingPassword, - WaitingTdlibParameters, - Ready, - LoggingOut, - Error, - }; + return facade.run(argc, argv); + } catch (const std::exception& e) { + std::cerr << "[tgcli] fatal error: " << e.what() << "\n"; + return 1; + } catch (...) { + std::cerr << "[tgcli] fatal error: unknown exception\n"; + return 1; + } } diff --git a/src/controllers/AuthController.cpp b/src/controllers/AuthController.cpp index ee56ff2..7038551 100644 --- a/src/controllers/AuthController.cpp +++ b/src/controllers/AuthController.cpp @@ -2,6 +2,7 @@ #include "AuthController.h" #include #include +#include // Конструктор AuthController::AuthController(ITgClient& client) @@ -11,6 +12,8 @@ AuthController::AuthController(ITgClient& client) update_status(); } +AuthController::~AuthController() = default; + void AuthController::update_status() { try { current_state_ = client_.check_status(); @@ -28,83 +31,66 @@ bool AuthController::is_authorized() const { return current_state_ == ITgClient::AuthState::Ready; } -bool AuthController::login(const std::string& phone_number, const std::string& code) { - - // 1. Проверяем текущее состояние +void AuthController::enter_phone(const std::string& phone_number) { update_status(); - // 2. Если уже авторизованы - ничего не делаем - if (is_authorized()) { - std::cerr << "[AuthController] Already authorized\n"; - return true; + if (current_state_ != ITgClient::AuthState::WaitingPhone) { + throw std::logic_error("[AuthController] Cannot enter phone in current state"); } - - // 3. Обрабатываем в зависимости от состояния + + if (phone_number.empty()) { + throw std::invalid_argument("Phone number is required"); + } + try { - switch (current_state_) { - case ITgClient::AuthState::WaitingPhone: - // Требуется номер телефона - if (phone_number.empty()) { - throw std::invalid_argument("Phone number is required"); - } - - // if (!validatePhoneNumber(phone_number)) { - // throw std::invalid_argument("Invalid phone number format"); - // } - - std::cout << "[AuthController] Sending phone number...\n"; - client_.enter_phone_number(phone_number); - break; - - case ITgClient::AuthState::WaitingCode: - // Требуется код подтверждения - if (code.empty()) { - throw std::invalid_argument("Auth code is required"); - } - - // if (!validateCode(code)) { - // throw std::invalid_argument("Invalid code format"); - // } - - std::cout << "[AuthController] Sending auth code...\n"; - client_.enter_message_code(code); - break; - - case ITgClient::AuthState::Ready: - return true; - - case ITgClient::AuthState::LoggingOut: - throw std::runtime_error("Cannot login while logging out"); - - case ITgClient::AuthState::Error: - throw std::runtime_error("In error state, cannot login"); - - default: - throw std::runtime_error("Unknown auth state"); - } - - // 4. Обновляем состояние после отправки + std::cout << "[AuthController] Sending phone number...\n"; + client_.enter_phone_number(phone_number); + update_status(); + } catch (const std::exception& e) { + std::cerr << "[AuthController] enter_phone error: " << e.what() << "\n"; + current_state_ = ITgClient::AuthState::Error; + throw; + } +} + +void AuthController::enter_code(const std::string& code) { + update_status(); + + if (current_state_ != ITgClient::AuthState::WaitingCode) { + throw std::logic_error("[AuthController] Cannot enter code in current state"); + } + + if (code.empty()) { + throw std::invalid_argument("Auth code is required"); + } + + try { + std::cout << "[AuthController] Sending auth code...\n"; + client_.enter_message_code(code); update_status(); - return true; - } catch (const std::exception& e) { - std::cerr << "[AuthController] Login error: " << e.what() << "\n"; + std::cerr << "[AuthController] enter_code error: " << e.what() << "\n"; current_state_ = ITgClient::AuthState::Error; - return false; + throw; } } void AuthController::logout() { update_status(); try { - if (!is_authorized() && get_auth_state() != ITgClient::AuthState::LoggingOut) { + if (current_state_ == ITgClient::AuthState::LoggingOut) { + std::cout << "[AuthController] Already logging out\n"; + return; + } + if (!is_authorized()) { std::cout << "[AuthController] Not authorized, nothing to logout\n"; return; } + client_.log_out(); update_status(); } catch (const std::exception& e) { - std::cerr << "[AuthController] Login error: " << e.what() << "\n"; + std::cerr << "[AuthController] Logout error: " << e.what() << "\n"; current_state_ = ITgClient::AuthState::Error; return; } diff --git a/src/controllers/AuthController.h b/src/controllers/AuthController.h index c966e93..804bd42 100644 --- a/src/controllers/AuthController.h +++ b/src/controllers/AuthController.h @@ -19,14 +19,14 @@ class AuthController { public: explicit AuthController(ITgClient& client); - // Запрет копирования - AuthController(const AuthController&) = delete; - AuthController& operator=(const AuthController&) = delete; + //AuthController(const AuthController&) = default + //AuthController& operator=(const AuthController&) = default; - ~AuthController() = default; + ~AuthController(); + + void enter_phone(const std::string& phone_number); + void enter_code(const std::string& code); - // Основные методы - bool login(const std::string& phone_number, const std::string& code = ""); void logout(); bool is_authorized() const; ITgClient::AuthState get_auth_state() const; diff --git a/src/controllers/ChatHistoryController.cpp b/src/controllers/ChatHistoryController.cpp new file mode 100644 index 0000000..7367f4d --- /dev/null +++ b/src/controllers/ChatHistoryController.cpp @@ -0,0 +1,29 @@ + + +#include "ChatHistoryController.h" + +ChatHistoryController::ChatHistoryController(ITgClient& client) + : tgClient(client), target_chat_id("") {} + +void ChatHistoryController::set_target_chat_id(const std::string& chatId) { + target_chat_id = chatId; +} + +std::string ChatHistoryController::get_target_chat_id() const { + return target_chat_id; +} + +void ChatHistoryController::clear_target_chat_id() { + target_chat_id.clear(); +} + +std::vector ChatHistoryController::get_target_chat_history(int limit) { + if (target_chat_id.empty()) { + return {}; + } + return tgClient.get_chat_history(target_chat_id, limit); +} + +std::vector ChatHistoryController::get_chat_history(const std::string& chatId, int limit) { + return tgClient.get_chat_history(chatId, limit); +} diff --git a/src/controllers/ChatHistoryController.h b/src/controllers/ChatHistoryController.h new file mode 100644 index 0000000..e52d126 --- /dev/null +++ b/src/controllers/ChatHistoryController.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include "../tgClient/ITgClient.hpp" + +class ChatHistoryController { +private: + ITgClient& tgClient; + std::string target_chat_id; + +public: + explicit ChatHistoryController(ITgClient& client); + + ChatHistoryController(const ChatHistoryController&) = delete; + ChatHistoryController& operator=(const ChatHistoryController) = delete; + + ~ChatHistoryController() = default; + + void set_target_chat_id(const std::string& chatId); + + std::string get_target_chat_id() const; + + void clear_target_chat_id(); + + std::vector get_target_chat_history(int limit = 20); + + std::vector get_chat_history(const std::string& chatId, int limit = 20); + +}; \ No newline at end of file diff --git a/src/controllers/GetChatsController.cpp b/src/controllers/GetChatsController.cpp index 02edb8d..6bb1516 100644 --- a/src/controllers/GetChatsController.cpp +++ b/src/controllers/GetChatsController.cpp @@ -2,25 +2,23 @@ #include "GetChatsController.h" #include #include -#include #include +#include +#include ChatsController::ChatsController(ITgClient& client) : client_(client) , last_cache_update_(0) , last_error_("") {} -// Получить список чатов std::vector ChatsController::get_chats(int limit) { clear_error(); if (limit <= 0) { - limit = 10; // значение по умолчанию + limit = 10; } - // Можно использовать кэш, если он актуален if (!cached_chats_.empty() && !should_refresh_cache()) { - // Возвращаем ограниченное количество из кэша int return_count = std::min(static_cast(cached_chats_.size()), limit); return std::vector( cached_chats_.begin(), @@ -28,11 +26,9 @@ std::vector ChatsController::get_chats(int limit) { ); } - // Получаем свежие данные try { std::vector chats = client_.get_chats(limit); - // Обновляем кэш update_cache(chats); std::cout << "[ChatsController] Retrieved " << chats.size() @@ -43,7 +39,6 @@ std::vector ChatsController::get_chats(int limit) { } catch (const std::exception& e) { handle_error(std::string("Failed to get chats: ") + e.what()); - // Если есть кэш, вернем его даже если устарел if (!cached_chats_.empty()) { std::cerr << "[ChatsController] Returning cached data due to error\n"; int return_count = std::min(static_cast(cached_chats_.size()), limit); @@ -57,12 +52,11 @@ std::vector ChatsController::get_chats(int limit) { } } -// Обновить кэш чатов void ChatsController::refresh_chats() { clear_error(); try { - std::vector chats = client_.get_chats(100); // больше для кэша + std::vector chats = client_.get_chats(100); update_cache(chats); @@ -79,7 +73,6 @@ void ChatsController::refresh_chats() { void ChatsController::update_cache(const std::vector& chats) { cached_chats_ = chats; - // Обновляем кэш заголовков chat_titles_.clear(); for (const auto& chat : chats) { chat_titles_[chat.chatId] = chat.title; @@ -89,7 +82,6 @@ void ChatsController::update_cache(const std::vector& chats) { } bool ChatsController::should_refresh_cache() const { - // Обновляем кэш каждые 5 минут const int CACHE_TTL_SECONDS = 5 * 60; if (cached_chats_.empty()) { @@ -100,7 +92,6 @@ bool ChatsController::should_refresh_cache() const { return (now - last_cache_update_) > CACHE_TTL_SECONDS; } -// Поиск чатов по названию std::vector ChatsController::search_chats( const std::string& query, int limit) { @@ -108,11 +99,9 @@ std::vector ChatsController::search_chats( clear_error(); if (query.empty()) { - // Если запрос пустой, возвращаем все чаты return get_chats(limit); } - // Используем кэш для поиска if (cached_chats_.empty()) { refresh_chats(); } @@ -120,7 +109,6 @@ std::vector ChatsController::search_chats( std::vector result; std::string query_lower = query; - // Приводим к нижнему регистру для регистронезависимого поиска std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), ::tolower); @@ -133,7 +121,6 @@ std::vector ChatsController::search_chats( std::transform(title_lower.begin(), title_lower.end(), title_lower.begin(), ::tolower); - // Ищем вхождение подстроки if (title_lower.find(query_lower) != std::string::npos) { result.push_back(chat); } @@ -145,7 +132,6 @@ std::vector ChatsController::search_chats( return result; } -// Получить информацию о конкретном чате ITgClient::Chat ChatsController::get_chat_info(const std::string& chat_id) { clear_error(); @@ -154,17 +140,14 @@ ITgClient::Chat ChatsController::get_chat_info(const std::string& chat_id) { return ITgClient::Chat{"", ""}; } - // Сначала проверяем кэш for (const auto& chat : cached_chats_) { if (chat.chatId == chat_id) { return chat; } } - // Если нет в кэше, обновляем кэш refresh_chats(); - // Ищем снова for (const auto& chat : cached_chats_) { if (chat.chatId == chat_id) { return chat; @@ -175,64 +158,6 @@ ITgClient::Chat ChatsController::get_chat_info(const std::string& chat_id) { return ITgClient::Chat{"", ""}; } -// Получить название чата по ID -std::string ChatsController::get_chat_title(const std::string& chat_id) { - if (chat_id.empty()) { - return ""; - } - - // Сначала проверяем кэш заголовков - auto it = chat_titles_.find(chat_id); - if (it != chat_titles_.end()) { - return it->second; - } - - // Если нет в кэше, ищем в основном кэше - for (const auto& chat : cached_chats_) { - if (chat.chatId == chat_id) { - chat_titles_[chat_id] = chat.title; - return chat.title; - } - } - - // Если все еще не нашли, обновляем кэш - refresh_chats(); - - for (const auto& chat : cached_chats_) { - if (chat.chatId == chat_id) { - chat_titles_[chat_id] = chat.title; - return chat.title; - } - } - - return "Unknown chat"; -} - -// Получить ID чата по названию -std::string ChatsController::get_chat_id_by_title(const std::string& title) { - if (title.empty()) { - return ""; - } - - for (const auto& chat : cached_chats_) { - if (chat.title == title) { - return chat.chatId; - } - } - - // Попробуем обновить кэш - refresh_chats(); - - for (const auto& chat : cached_chats_) { - if (chat.title == title) { - return chat.chatId; - } - } - - return ""; -} - -// Проверить существование чата bool ChatsController::chat_exists(const std::string& chat_id) { if (chat_id.empty()) { return false; @@ -247,17 +172,6 @@ bool ChatsController::chat_exists(const std::string& chat_id) { return false; } -// Получить количество чатов -size_t ChatsController::get_chats_count() const { - return cached_chats_.size(); -} - -// Время последнего обновления -time_t ChatsController::get_last_update_time() const { - return last_cache_update_; -} - -// Очистить кэш void ChatsController::clear_cache() { cached_chats_.clear(); chat_titles_.clear(); @@ -265,7 +179,6 @@ void ChatsController::clear_cache() { std::cout << "[ChatsController] Cache cleared\n"; } -// Получить последнюю ошибку const std::string& ChatsController::get_last_error() const { return last_error_; } diff --git a/src/controllers/GetChatsController.h b/src/controllers/GetChatsController.h index feebdeb..8c575ec 100644 --- a/src/controllers/GetChatsController.h +++ b/src/controllers/GetChatsController.h @@ -3,13 +3,13 @@ #include #include #include +#include #include "../tgClient/ITgClient.hpp" class ChatsController { private: ITgClient& client_; - // Кэшированные чаты (опционально) std::vector cached_chats_; std::map chat_titles_; // chat_id -> title time_t last_cache_update_; @@ -17,45 +17,24 @@ class ChatsController { public: explicit ChatsController(ITgClient& client); - // Запрет копирования ChatsController(const ChatsController&) = delete; ChatsController& operator=(const ChatsController&) = delete; ~ChatsController() = default; - // Получить список чатов std::vector get_chats(int limit = 10); - // Обновить кэш чатов void refresh_chats(); - // Поиск чатов по названию std::vector search_chats( const std::string& query, int limit = 10 ); - // Получить информацию о конкретном чате ITgClient::Chat get_chat_info(const std::string& chat_id); - - // Получить название чата по ID (из кэша) - std::string get_chat_title(const std::string& chat_id); - - // Получить ID чата по названию (из кэша) - std::string get_chat_id_by_title(const std::string& title); - - // === Вспомогательные методы === - - // Проверить, существует ли чат + bool chat_exists(const std::string& chat_id); - - // Получить количество чатов - size_t get_chats_count() const; - - // Время последнего обновления - time_t get_last_update_time() const; - - // Очистить кэш + void clear_cache(); const std::string& get_last_error() const; @@ -63,7 +42,6 @@ class ChatsController { private: std::string last_error_; - // Внутренние методы void handle_error(const std::string& error); void clear_error(); void update_cache(const std::vector& chats); diff --git a/src/controllers/SendMessageController.cpp b/src/controllers/SendMessageController.cpp index 412496d..534e374 100644 --- a/src/controllers/SendMessageController.cpp +++ b/src/controllers/SendMessageController.cpp @@ -2,23 +2,21 @@ #include "SendMessageController.h" #include #include +#include MessageController::MessageController(ITgClient& client) : client_(client) , last_error_("") {} -// Отправить сообщение bool MessageController::send_message(const std::string& chat_id, const std::string& text) { clear_error(); - - // 1. Валидация + if (chat_id.empty()) { handle_error("Chat ID cannot be empty"); return false; } - // 2. Отправка try { client_.send_message(chat_id, text); std::cout << "[MessageController] Message sent to chat " << chat_id << "\n"; @@ -30,7 +28,6 @@ bool MessageController::send_message(const std::string& chat_id, } } -// Получить последнюю ошибку const std::string& MessageController::get_last_error() const { return last_error_; } diff --git a/src/controllers/SendMessageController.h b/src/controllers/SendMessageController.h index 4dcc0ee..8cd3e5b 100644 --- a/src/controllers/SendMessageController.h +++ b/src/controllers/SendMessageController.h @@ -1,6 +1,7 @@ // controllers/messagecontroller.h #pragma once #include +#include #include #include "../tgClient/ITgClient.hpp" diff --git a/src/facade/TgClientFacade.cpp b/src/facade/TgClientFacade.cpp new file mode 100644 index 0000000..182befd --- /dev/null +++ b/src/facade/TgClientFacade.cpp @@ -0,0 +1,446 @@ +#include +#include "TgClientFacade.h" + +#include +#include +#include +#include +#include + +TgClientFacade::TgClientFacade(ITgClient& client) : client_(client) { + client_.send_tdlib_parameters(); + while (client_.check_status() == ITgClient::AuthState::Error) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + auth_controller_ = std::make_unique(client_); + chats_controller_ = std::make_unique(client_); + history_controller_ = std::make_unique(client_); + message_controller_ = std::make_unique(client_); +} + +std::string TgClientFacade::auth_state_to_string(ITgClient::AuthState state) { + using AuthState = ITgClient::AuthState; + switch (state) { + case AuthState::WaitingPhone: + return "WaitingPhone"; + case AuthState::WaitingCode: + return "WaitingCode"; + case AuthState::Ready: + return "Ready"; + case AuthState::LoggingOut: + return "LoggingOut"; + case AuthState::Error: + default: + return "Error"; + } +} + +void TgClientFacade::print_usage() { + std::cout + << "Usage:\n" + << " tgcli auth-status\n" + << " tgcli login-phone \n" + << " tgcli login-code \n" + << " tgcli logout\n" + << " tgcli chats\n" + << " tgcli search-chats \n" + << " tgcli chat-info \n" + << " tgcli history [limit]\n" + << " tgcli set-target \n" + << " tgcli get-target-history [limit]\n" + << " tgcli send \n" + << std::endl; +} + +int TgClientFacade::run(int argc, char** argv) { + if (argc < 2) { + std::cerr << "[tgcli] No command specified\n"; + print_usage(); + return 1; + } + + client_.check_status(); + + const std::string command = argv[1]; + + try { + if (command == "auth-status") { + return handle_auth_status(); + } else if (command == "login-phone") { + if (argc < 3) { + std::cerr << "[tgcli] login-phone: phone is required\n"; + return 1; + } + client_.enter_phone_number(argv[2]); + return 0; + } else if (command == "login-code") { + if (argc < 3) { + std::cerr << "[tgcli] login-code: code is required\n"; + return 1; + } + return handle_login_code(argv[2]); + } else if (command == "logout") { + return handle_logout(); + } else if (command == "chats") { + int limit = 20; + try { + if (argc < 3) { + limit = std::stoi(argv[2]); + } + } catch (...) { + std::cerr << "[tgcli] Invalid limit, using default 20\n"; + } + return handle_get_chats(limit); + } else if (command == "search-chats") { + if (argc < 3) { + std::cerr << "[tgcli] search-chats: query is required\n"; + return 1; + } + return handle_search_chats(argv[2]); + } else if (command == "chat-info") { + if (argc < 3) { + std::cerr << "[tgcli] chat-info: chat_id is required\n"; + return 1; + } + return handle_chat_info(argv[2]); + } else if (command == "history") { + if (argc < 3) { + std::cerr << "[tgcli] history: chat_id is required\n"; + return 1; + } + int limit = 20; + if (argc >= 4) { + try { + limit = std::stoi(argv[3]); + } catch (...) { + std::cerr << "[tgcli] Invalid limit, using default 20\n"; + } + } + return handle_history(argv[2], limit); + } else if (command == "set-target") { + if (argc < 3) { + std::cerr << "[tgcli] set-target: chat_id is required\n"; + return 1; + } + return handle_set_target_chat(argv[2]); + } else if (command == "get-target-history") { + int limit = 20; + if (argc >= 3) { + try { + limit = std::stoi(argv[2]); + } catch (...) { + std::cerr << "[tgcli] Invalid limit, using default 20\n"; + } + } + return handle_get_target_history(limit); + } else if (command == "send") { + if (argc < 4) { + std::cerr << "[tgcli] send: not enough arguments\n"; + std::cerr << "Usage: tgcli send \n"; + return 1; + } + + const std::string chat_id = argv[2]; + + std::ostringstream oss; + for (int i = 3; i < argc; ++i) { + if (i > 3) { + oss << ' '; + } + oss << argv[i]; + } + const std::string message = oss.str(); + + return handle_send(chat_id, message); + } else { + std::cerr << "[tgcli] Unknown command: " << command << "\n"; + print_usage(); + return 1; + } + } catch (const std::exception& e) { + std::cerr << "[tgcli] Unhandled error: " << e.what() << "\n"; + return 1; + } +} + +// ===================== +// AUTH команды +// ===================== + +int TgClientFacade::handle_auth_status() { + auto state = auth_controller_->get_auth_state(); + bool authorized = auth_controller_->is_authorized(); + + std::cout << "[tgcli] Auth state: " << auth_state_to_string(state) + << " (authorized=" << (authorized ? "true" : "false") << ")\n"; + + return 0; +} + +int TgClientFacade::handle_login_phone(const std::string& phone) { + try { + auth_controller_->enter_phone(phone); + } catch (const std::exception& e) { + std::cerr << "[tgcli] login-phone error: " << e.what() << "\n"; + return 1; + } + + auto state = auth_controller_->get_auth_state(); + std::cout << "[tgcli] login-phone: new state = " + << auth_state_to_string(state) << "\n"; + + return 0; +} + +int TgClientFacade::handle_login_code(const std::string& code) { + try { + auth_controller_->enter_code(code); + } catch (const std::exception& e) { + std::cerr << "[tgcli] login-code error: " << e.what() << "\n"; + return 1; + } + + auto state = auth_controller_->get_auth_state(); + std::cout << "[tgcli] login-code: new state = " + << auth_state_to_string(state) << "\n"; + + if (auth_controller_->is_authorized()) { + std::cout << "[tgcli] Authorization completed\n"; + } + + return 0; +} + +int TgClientFacade::handle_logout() { + auth_controller_->logout(); + + auto state = auth_controller_->get_auth_state(); + std::cout << "[tgcli] logout: new state = " + << auth_state_to_string(state) << "\n"; + + return 0; +} + +// ===================== +// CHATS команды +// ===================== + +int TgClientFacade::handle_get_chats(int limit = 20) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + auto chats = chats_controller_->get_chats(limit); + + if (chats.empty()) { + std::cout << "[tgcli] No chats found\n"; + return 0; + } + + std::cout << "[tgcli] Found " << chats.size() << " chats:\n"; + std::cout << "========================================\n"; + + for (size_t i = 0; i < chats.size(); ++i) { + const auto& chat = chats[i]; + std::cout << std::setw(3) << (i + 1) << ". " + << "ID: " << chat.chatId + << " | Title: " << chat.title << "\n"; + } + + std::cout << "========================================\n"; + + } catch (const std::exception& e) { + std::cerr << "[tgcli] get-chats error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + +int TgClientFacade::handle_search_chats(const std::string& query) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + auto chats = chats_controller_->search_chats(query, 50); + + if (chats.empty()) { + std::cout << "[tgcli] No chats found for query: " << query << "\n"; + return 0; + } + + std::cout << "[tgcli] Found " << chats.size() + << " chats matching '" << query << "':\n"; + std::cout << "========================================\n"; + + for (size_t i = 0; i < chats.size(); ++i) { + const auto& chat = chats[i]; + std::cout << std::setw(3) << (i + 1) << ". " + << "ID: " << chat.chatId + << " | Title: " << chat.title << "\n"; + } + + std::cout << "========================================\n"; + + } catch (const std::exception& e) { + std::cerr << "[tgcli] search-chats error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + +int TgClientFacade::handle_chat_info(const std::string& chat_id) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + auto chat = chats_controller_->get_chat_info(chat_id); + + if (chat.chatId.empty()) { + std::cerr << "[tgcli] Chat not found: " << chat_id << "\n"; + return 1; + } + + std::cout << "[tgcli] Chat info:\n"; + std::cout << " ID: " << chat.chatId << "\n"; + std::cout << " Title: " << chat.title << "\n"; + + } catch (const std::exception& e) { + std::cerr << "[tgcli] chat-info error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + +// ===================== +// HISTORY команды +// ===================== + +int TgClientFacade::handle_history(const std::string& chat_id, int limit) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + auto messages = history_controller_->get_chat_history(chat_id, limit); + + if (messages.empty()) { + std::cout << "[tgcli] No messages found in chat: " << chat_id << "\n"; + return 0; + } + + std::cout << "[tgcli] History for chat " << chat_id + << " (" << messages.size() << " messages):\n"; + std::cout << "========================================\n"; + + for (const auto& msg : messages) { + std::cout << "[" << msg.sender << "]: " << msg.text << "\n"; + } + + std::cout << "========================================\n"; + + } catch (const std::exception& e) { + std::cerr << "[tgcli] history error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + +int TgClientFacade::handle_set_target_chat(const std::string& chat_id) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + history_controller_->set_target_chat_id(chat_id); + + // Проверим, существует ли такой чат + auto chat = chats_controller_->get_chat_info(chat_id); + if (chat.chatId.empty()) { + std::cout << "[tgcli] Warning: chat " << chat_id + << " not found, but target set anyway\n"; + } else { + std::cout << "[tgcli] Target chat set to: " << chat.title + << " (" << chat_id << ")\n"; + } + + } catch (const std::exception& e) { + std::cerr << "[tgcli] set-target error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + +int TgClientFacade::handle_get_target_history(int limit) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + auto messages = history_controller_->get_target_chat_history(limit); + + if (messages.empty()) { + std::cout << "[tgcli] No messages in target chat or target not set\n"; + return 0; + } + + std::cout << "[tgcli] Target chat history (" << messages.size() << " messages):\n"; + std::cout << "========================================\n"; + + for (const auto& msg : messages) { + std::cout << "[" << msg.sender << "]: " << msg.text << "\n"; + } + + std::cout << "========================================\n"; + + } catch (const std::exception& e) { + std::cerr << "[tgcli] get-target-history error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + + +// ===================== +// SEND команда +// ===================== + +int TgClientFacade::handle_send(const std::string& chat_id, + const std::string& message) { + if (!auth_controller_->is_authorized()) { + std::cerr << "[tgcli] Not authorized. Use login-phone/login-code first.\n"; + return 1; + } + + try { + bool success = message_controller_->send_message(chat_id, message); + + if (success) { + std::cout << "[tgcli] Message sent to chat " << chat_id << "\n"; + return 0; + } else { + std::cerr << "[tgcli] Failed to send message: " + << message_controller_->get_last_error() << "\n"; + return 1; + } + + } catch (const std::exception& e) { + std::cerr << "[tgcli] send error: " << e.what() << "\n"; + return 1; + } +} \ No newline at end of file diff --git a/src/facade/TgClientFacade.h b/src/facade/TgClientFacade.h new file mode 100644 index 0000000..a24141c --- /dev/null +++ b/src/facade/TgClientFacade.h @@ -0,0 +1,40 @@ +#pragma once +#include "../controllers/AuthController.h" +#include "../controllers/SendMessageController.h" +#include "../controllers/GetChatsController.h" +#include "../controllers/ChatHistoryController.h" + +class TgClientFacade { +private: + std::unique_ptr auth_controller_; + std::unique_ptr message_controller_; + std::unique_ptr history_controller_; + std::unique_ptr chats_controller_; + ITgClient& client_; + + static std::vector collect_args(int argc, char** argv); + static void print_usage(); + + + int handle_auth_status(); + int handle_login_phone(const std::string& phone); + int handle_login_code(const std::string& code); + int handle_logout(); + + int handle_send(const std::string& chat_id, const std::string& message); + + int handle_get_chats(int limit); + int handle_search_chats(const std::string& query); + int handle_chat_info(const std::string& chat_id); + + int handle_history(const std::string& chat_id, int limit); + int handle_set_target_chat(const std::string& chat_id); + int handle_get_target_history(int limit); + +public: + explicit TgClientFacade(ITgClient& client); + ~TgClientFacade() = default; + static std::string auth_state_to_string(ITgClient::AuthState state); + + int run(int argc, char** argv); +}; \ No newline at end of file diff --git a/tests/controllers/AuthController.cpp b/tests/controllers/AuthController.cpp new file mode 100644 index 0000000..37e08b3 --- /dev/null +++ b/tests/controllers/AuthController.cpp @@ -0,0 +1,278 @@ +#include + +#include "../src/controllers/AuthController.h" +#include "../src/tgClient/ITgClient.hpp" + +#include +#include +#include + +// Фейковый клиент для тестов +class FakeTgClient : public ITgClient { +public: + AuthState state = AuthState::WaitingPhone; + + int check_status_calls = 0; + bool throw_in_check_status = false; + + bool enter_phone_called = false; + std::string last_phone; + bool throw_in_enter_phone = false; + + bool enter_code_called = false; + std::string last_code; + bool throw_in_enter_code = false; + + bool log_out_called = false; + bool throw_in_log_out = false; + + // ITgClient interface + + AuthState check_status() override { + ++check_status_calls; + if (throw_in_check_status) { + throw std::runtime_error("check_status failed"); + } + return state; + } + + void enter_phone_number(std::string phone) override { + if (throw_in_enter_phone) { + throw std::runtime_error("enter_phone_number failed"); + } + enter_phone_called = true; + last_phone = std::move(phone); + // Эмулируем переход в ожидание кода + state = AuthState::WaitingCode; + } + + void enter_message_code(std::string code) override { + if (throw_in_enter_code) { + throw std::runtime_error("enter_message_code failed"); + } + enter_code_called = true; + last_code = std::move(code); + // Эмулируем успешную авторизацию + state = AuthState::Ready; + } + + void log_out() override { + log_out_called = true; + if (throw_in_log_out) { + throw std::runtime_error("log_out failed"); + } + // Эмулируем выход + state = AuthState::LoggingOut; + } + + std::vector get_chats(int) override { + return {}; + } + + std::vector get_chat_history(std::string, int) override { + return {}; + } + + void send_message(std::string, std::string) override { + // не нужно для AuthController + } +}; + +// ---------------- ТЕСТЫ ---------------- + +using AuthState = ITgClient::AuthState; + +// Конструктор: подтягивает состояние из клиента +TEST(AuthControllerTests, ConstructorInitializesStateFromClient) { + FakeTgClient fake; + fake.state = AuthState::WaitingPhone; + + AuthController controller(fake); + + EXPECT_EQ(fake.check_status_calls, 1); + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); + EXPECT_FALSE(controller.is_authorized()); +} + +// Если check_status кидает — состояние Error +TEST(AuthControllerTests, UpdateStatusOnExceptionSetsError) { + FakeTgClient fake; + fake.throw_in_check_status = true; + + AuthController controller(fake); + + EXPECT_EQ(controller.get_auth_state(), AuthState::Error); + EXPECT_FALSE(controller.is_authorized()); +} + +// enter_phone: нормальный поток +TEST(AuthControllerTests, EnterPhoneInWaitingPhoneSendsPhoneAndMovesToWaitingCode) { + FakeTgClient fake; + fake.state = AuthState::WaitingPhone; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); + + controller.enter_phone("+71234567890"); + + EXPECT_TRUE(fake.enter_phone_called); + EXPECT_EQ(fake.last_phone, "+71234567890"); + // FakeTgClient перевёл state в WaitingCode, update_status это подтянул + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingCode); +} + +// enter_phone: пустой номер -> invalid_argument, состояние не меняется +TEST(AuthControllerTests, EnterPhoneEmptyNumberThrowsInvalidArgumentAndKeepsState) { + FakeTgClient fake; + fake.state = AuthState::WaitingPhone; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); + + EXPECT_THROW(controller.enter_phone(""), std::invalid_argument); + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); + EXPECT_FALSE(fake.enter_phone_called); +} + +// enter_phone: вызов в неправильном состоянии -> logic_error +TEST(AuthControllerTests, EnterPhoneInWrongStateThrowsLogicError) { + FakeTgClient fake; + fake.state = AuthState::WaitingCode; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingCode); + + EXPECT_THROW(controller.enter_phone("+7123"), std::logic_error); + EXPECT_FALSE(fake.enter_phone_called); + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingCode); +} + +// enter_phone: если клиент кидает — контроллер ставит Error и пробрасывает дальше +TEST(AuthControllerTests, EnterPhoneClientThrowsSetsErrorAndRethrows) { + FakeTgClient fake; + fake.state = AuthState::WaitingPhone; + fake.throw_in_enter_phone = true; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); + + EXPECT_THROW(controller.enter_phone("+71234567890"), std::runtime_error); + EXPECT_EQ(controller.get_auth_state(), AuthState::Error); +} + +// enter_code: нормальный поток +TEST(AuthControllerTests, EnterCodeInWaitingCodeSendsCodeAndBecomesReady) { + FakeTgClient fake; + fake.state = AuthState::WaitingCode; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingCode); + + controller.enter_code("12345"); + + EXPECT_TRUE(fake.enter_code_called); + EXPECT_EQ(fake.last_code, "12345"); + EXPECT_EQ(controller.get_auth_state(), AuthState::Ready); + EXPECT_TRUE(controller.is_authorized()); +} + +// enter_code: пустой код -> invalid_argument, состояние не меняется +TEST(AuthControllerTests, EnterCodeEmptyThrowsInvalidArgumentAndKeepsState) { + FakeTgClient fake; + fake.state = AuthState::WaitingCode; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingCode); + + EXPECT_THROW(controller.enter_code(""), std::invalid_argument); + EXPECT_FALSE(fake.enter_code_called); + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingCode); +} + +// enter_code: вызов в неправильном состоянии -> logic_error +TEST(AuthControllerTests, EnterCodeInWrongStateThrowsLogicError) { + FakeTgClient fake; + fake.state = AuthState::WaitingPhone; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); + + EXPECT_THROW(controller.enter_code("12345"), std::logic_error); + EXPECT_FALSE(fake.enter_code_called); + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); +} + +// enter_code: если клиент кидает — контроллер ставит Error и пробрасывает дальше +TEST(AuthControllerTests, EnterCodeClientThrowsSetsErrorAndRethrows) { + FakeTgClient fake; + fake.state = AuthState::WaitingCode; + fake.throw_in_enter_code = true; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::WaitingCode); + + EXPECT_THROW(controller.enter_code("12345"), std::runtime_error); + EXPECT_EQ(controller.get_auth_state(), AuthState::Error); +} + +// logout: уже LoggingOut — ничего не делает +TEST(AuthControllerTests, LogoutWhenAlreadyLoggingOutDoesNothing) { + FakeTgClient fake; + fake.state = AuthState::LoggingOut; + + AuthController controller(fake); + ASSERT_EQ(controller.get_auth_state(), AuthState::LoggingOut); + + fake.log_out_called = false; + + controller.logout(); + + EXPECT_FALSE(fake.log_out_called); + EXPECT_EQ(controller.get_auth_state(), AuthState::LoggingOut); +} + +// logout: не авторизованы и не LoggingOut — ничего не делает +TEST(AuthControllerTests, LogoutWhenNotAuthorizedDoesNothing) { + FakeTgClient fake; + fake.state = AuthState::WaitingPhone; + + AuthController controller(fake); + ASSERT_FALSE(controller.is_authorized()); + + fake.log_out_called = false; + + controller.logout(); + + EXPECT_FALSE(fake.log_out_called); + EXPECT_EQ(controller.get_auth_state(), AuthState::WaitingPhone); +} + +// logout: авторизованы — вызывает client.log_out и переходит в LoggingOut +TEST(AuthControllerTests, LogoutWhenAuthorizedCallsClientAndMovesToLoggingOut) { + FakeTgClient fake; + fake.state = AuthState::Ready; + + AuthController controller(fake); + ASSERT_TRUE(controller.is_authorized()); + + fake.log_out_called = false; + + controller.logout(); + + EXPECT_TRUE(fake.log_out_called); + EXPECT_EQ(controller.get_auth_state(), AuthState::LoggingOut); + EXPECT_FALSE(controller.is_authorized()); +} + +// logout: если клиент кидает — контроллер ставит Error, исключение не пробрасывается +TEST(AuthControllerTests, LogoutClientThrowsSetsErrorButDoesNotRethrow) { + FakeTgClient fake; + fake.state = AuthState::Ready; + fake.throw_in_log_out = true; + + AuthController controller(fake); + ASSERT_TRUE(controller.is_authorized()); + + EXPECT_NO_THROW(controller.logout()); + EXPECT_EQ(controller.get_auth_state(), AuthState::Error); +}