From bf0e55ed6860e7f1a60085dc7936fd52ce5ef432 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris <sideris.konstantin@gmail.com> Date: Fri, 18 May 2018 01:23:16 +0300 Subject: [PATCH] Add another small example --- CMakeLists.txt | 2 + examples/simple_bot.cpp | 235 +++++++++++++++++++++++++++++ include/mtxclient/http/client.hpp | 35 +---- include/mtxclient/http/session.hpp | 28 +++- lib/http/client.cpp | 17 +-- lib/http/session.cpp | 5 +- 6 files changed, 276 insertions(+), 46 deletions(-) create mode 100644 examples/simple_bot.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a341caddb..71bc7e137 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,7 +146,9 @@ endif() if (BUILD_LIB_EXAMPLES) add_executable(room_feed examples/room_feed.cpp) + add_executable(simple_bot examples/simple_bot.cpp) target_link_libraries(room_feed matrix_client ${MATRIX_STRUCTS_LIBRARY}) + target_link_libraries(simple_bot matrix_client ${MATRIX_STRUCTS_LIBRARY}) endif() if (BUILD_LIB_TESTS) diff --git a/examples/simple_bot.cpp b/examples/simple_bot.cpp new file mode 100644 index 000000000..a7725227f --- /dev/null +++ b/examples/simple_bot.cpp @@ -0,0 +1,235 @@ +#include <boost/algorithm/string/predicate.hpp> +#include <boost/beast.hpp> + +#include <iostream> +#include <json.hpp> +#include <unistd.h> +#include <variant.hpp> + +#include <mtx.hpp> +#include <mtx/identifiers.hpp> + +#include "mtxclient/http/client.hpp" +#include "mtxclient/http/errors.hpp" + +// +// Simple example bot that will accept any invite. +// + +using namespace std; +using namespace mtx::client; +using namespace mtx::http; +using namespace mtx::events; +using namespace mtx::identifiers; + +using TimelineEvent = mtx::events::collections::TimelineEvents; + +namespace { +std::shared_ptr<Client> client = nullptr; +} + +void +print_errors(RequestErr err) +{ + if (err->status_code != boost::beast::http::status::unknown) + cout << err->status_code << "\n"; + if (!err->matrix_error.error.empty()) + cout << err->matrix_error.error << "\n"; + if (err->error_code) + cout << err->error_code.message() << "\n"; +} + +// Check if the given event has a textual representation. +bool +is_room_message(const TimelineEvent &event) +{ + return mpark::holds_alternative<mtx::events::RoomEvent<msg::Audio>>(event) || + mpark::holds_alternative<mtx::events::RoomEvent<msg::Emote>>(event) || + mpark::holds_alternative<mtx::events::RoomEvent<msg::File>>(event) || + mpark::holds_alternative<mtx::events::RoomEvent<msg::Image>>(event) || + mpark::holds_alternative<mtx::events::RoomEvent<msg::Notice>>(event) || + mpark::holds_alternative<mtx::events::RoomEvent<msg::Text>>(event) || + mpark::holds_alternative<mtx::events::RoomEvent<msg::Video>>(event); +} + +// Retrieves the fallback body value from the event. +std::string +get_body(const TimelineEvent &event) +{ + if (mpark::holds_alternative<RoomEvent<msg::Audio>>(event)) + return mpark::get<RoomEvent<msg::Audio>>(event).content.body; + else if (mpark::holds_alternative<RoomEvent<msg::Emote>>(event)) + return mpark::get<RoomEvent<msg::Emote>>(event).content.body; + else if (mpark::holds_alternative<RoomEvent<msg::File>>(event)) + return mpark::get<RoomEvent<msg::File>>(event).content.body; + else if (mpark::holds_alternative<RoomEvent<msg::Image>>(event)) + return mpark::get<RoomEvent<msg::Image>>(event).content.body; + else if (mpark::holds_alternative<RoomEvent<msg::Notice>>(event)) + return mpark::get<RoomEvent<msg::Notice>>(event).content.body; + else if (mpark::holds_alternative<RoomEvent<msg::Text>>(event)) + return mpark::get<RoomEvent<msg::Text>>(event).content.body; + else if (mpark::holds_alternative<RoomEvent<msg::Video>>(event)) + return mpark::get<RoomEvent<msg::Video>>(event).content.body; + + return ""; +} + +// Retrieves the sender of the event. +std::string +get_sender(const TimelineEvent &event) +{ + return mpark::visit([](auto e) { return e.sender; }, event); +} + +void +parse_messages(const mtx::responses::Sync &res, bool parse_repeat_cmd = false) +{ + for (const auto room : res.rooms.invite) { + auto room_id = parse<Room>(room.first); + + printf("joining room %s\n", room_id.to_string().c_str()); + client->join_room(room_id, [room_id](const nlohmann::json &obj, RequestErr e) { + if (e) { + print_errors(e); + printf("failed to join room %s\n", room_id.to_string().c_str()); + return; + } + + printf("joined room \n%s\n", obj.dump(2).c_str()); + + mtx::events::msg::Text text; + text.body = "Thanks for the invitation!"; + + client->send_room_message<mtx::events::msg::Text, + mtx::events::EventType::RoomMessage>( + room_id, text, [room_id](const mtx::responses::EventId &, RequestErr e) { + if (e) { + print_errors(e); + return; + } + + printf("sent message to %s\n", room_id.to_string().c_str()); + }); + }); + } + + if (!parse_repeat_cmd) + return; + + for (const auto room : res.rooms.join) { + const std::string repeat_cmd = "!repeat"; + const std::string room_id = room.first; + + for (const auto &e : room.second.timeline.events) { + if (!is_room_message(e)) + continue; + + auto body = get_body(e); + if (!boost::starts_with(body, repeat_cmd)) + continue; + + auto word = std::string(body.begin() + repeat_cmd.size(), body.end()); + auto user = get_sender(e); + + mtx::events::msg::Text text; + text.body = user + ": " + word; + + client->send_room_message<mtx::events::msg::Text, + mtx::events::EventType::RoomMessage>( + parse<Room>(room_id), + text, + [room_id](const mtx::responses::EventId &, RequestErr e) { + if (e) { + print_errors(e); + return; + } + + printf("sent message to %s\n", room_id.c_str()); + }); + } + } +} + +// Callback to executed after a /sync request completes. +void +sync_handler(const mtx::responses::Sync &res, RequestErr err) +{ + SyncOpts opts; + + if (err) { + cout << "sync error:\n"; + print_errors(err); + opts.since = client->next_batch_token(); + client->sync(opts, &sync_handler); + return; + } + + parse_messages(res, true); + + opts.since = res.next_batch; + client->set_next_batch_token(res.next_batch); + client->sync(opts, &sync_handler); +} + +// Callback to executed after the first (initial) /sync request completes. +void +initial_sync_handler(const mtx::responses::Sync &res, RequestErr err) +{ + SyncOpts opts; + + if (err) { + cout << "error during initial sync:\n"; + print_errors(err); + + if (err->status_code != boost::beast::http::status::ok) { + cout << "retrying initial sync ...\n"; + opts.timeout = 0; + client->sync(opts, &initial_sync_handler); + } + + return; + } + + parse_messages(res); + + opts.since = res.next_batch; + client->set_next_batch_token(res.next_batch); + client->sync(opts, &sync_handler); +} + +void +login_handler(const mtx::responses::Login &, RequestErr err) +{ + if (err) { + printf("login error\n"); + print_errors(err); + return; + } + + printf("user_id: %s\n", client->user_id().to_string().c_str()); + printf("device_id: %s\n", client->device_id().c_str()); + + SyncOpts opts; + opts.timeout = 0; + client->sync(opts, &initial_sync_handler); +} + +int +main() +{ + std::string username, server, password; + + cout << "username: "; + std::getline(std::cin, username); + + cout << "server: "; + std::getline(std::cin, server); + + password = getpass("password: "); + + client = std::make_shared<Client>(server); + client->login(username, password, login_handler); + client->close(); + + return 0; +} diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp index c272825f4..76decf710 100644 --- a/include/mtxclient/http/client.hpp +++ b/include/mtxclient/http/client.hpp @@ -69,8 +69,6 @@ public: //! Wait for the client to close. void close(); - //! Make a new request. - void do_request(std::shared_ptr<Session> session); //! Add an access token. void set_access_token(const std::string &token) { access_token_ = token; } //! Retrieve the access token. @@ -270,13 +268,10 @@ private: void setup_auth(Session *session, bool auth); boost::asio::io_service ios_; - //! Used to prevent the event loop from shutting down. - boost::optional<boost::asio::io_service::work> work_; + boost::optional<boost::asio::io_context::work> work_{ios_}; //! Worker threads for the requests. boost::thread_group thread_group_; - //! Used to resolve DNS names. - boost::asio::ip::tcp::resolver resolver_; //! SSL context for requests. boost::asio::ssl::context ssl_ctx_{boost::asio::ssl::context::sslv23_client}; //! The homeserver to connect to. @@ -313,7 +308,7 @@ mtx::http::Client::post(const std::string &endpoint, setup_headers<Request, boost::beast::http::verb::post>( session.get(), req, endpoint, content_type); - do_request(std::move(session)); + session->run(); } // put function for the PUT HTTP requests that send responses @@ -334,7 +329,7 @@ mtx::http::Client::put(const std::string &endpoint, setup_headers<Request, boost::beast::http::verb::put>( session.get(), req, endpoint, "application/json"); - do_request(std::move(session)); + session->run(); } // provides PUT functionality for the endpoints which dont respond with a body @@ -366,7 +361,7 @@ mtx::http::Client::get(const std::string &endpoint, setup_auth(session.get(), requires_auth); setup_headers<std::string, boost::beast::http::verb::get>(session.get(), {}, endpoint); - do_request(std::move(session)); + session->run(); } template<class Response> @@ -374,9 +369,10 @@ std::shared_ptr<mtx::http::Session> mtx::http::Client::create_session(HeadersCallback<Response> callback) { auto session = std::make_shared<Session>( - ios_, - ssl_ctx_, + std::ref(ios_), + std::ref(ssl_ctx_), server_, + port_, client::utils::random_token(), [callback](RequestID, const boost::beast::http::response<boost::beast::http::string_body> &response, @@ -438,23 +434,6 @@ mtx::http::Client::create_session(HeadersCallback<Response> callback) callback(response_data, {}, client_error); }); - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(session->socket.native_handle(), server_.c_str())) { - boost::system::error_code ec{static_cast<int>(::ERR_get_error()), - boost::asio::error::get_ssl_category()}; - std::cerr << ec.message() << "\n"; - - Response response_data; - - mtx::http::ClientError client_error; - client_error.error_code = ec; - - callback(response_data, {}, client_error); - - // Initialization failed. - return nullptr; - } - return std::move(session); } diff --git a/include/mtxclient/http/session.hpp b/include/mtxclient/http/session.hpp index 2cdf7f91c..d59a47ea5 100644 --- a/include/mtxclient/http/session.hpp +++ b/include/mtxclient/http/session.hpp @@ -4,6 +4,7 @@ #include <boost/asio/ssl.hpp> #include <boost/beast.hpp> +#include "mtxclient/http/errors.hpp" #include "mtxclient/utils.hpp" namespace mtx { @@ -28,14 +29,19 @@ struct Session : public std::enable_shared_from_this<Session> Session(boost::asio::io_service &ios, boost::asio::ssl::context &ssl_ctx, const std::string &host, + uint16_t port, RequestID id, SuccessCallback on_success, FailureCallback on_failure); + //! DNS resolver. + boost::asio::ip::tcp::resolver resolver_; //! Socket used for communication. boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket; //! Remote host. std::string host; + //! Remote port. + uint16_t port; //! Buffer where the response will be stored. boost::beast::flat_buffer output_buf; //! Parser that will the response data. @@ -52,10 +58,28 @@ struct Session : public std::enable_shared_from_this<Session> //! Function to be called when the request fails. FailureCallback on_failure; - void on_resolve(boost::system::error_code ec, - boost::asio::ip::tcp::resolver::results_type results); + void run() + { + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(socket.native_handle(), host.c_str())) { + boost::system::error_code ec{static_cast<int>(::ERR_get_error()), + boost::asio::error::get_ssl_category()}; + std::cerr << ec.message() << "\n"; + + return on_failure(id, ec); + } + + resolver_.async_resolve(host, + std::to_string(port), + std::bind(&Session::on_resolve, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2)); + } private: + void on_resolve(boost::system::error_code ec, + boost::asio::ip::tcp::resolver::results_type results); void on_close(boost::system::error_code ec); void on_connect(const boost::system::error_code &ec); void on_handshake(const boost::system::error_code &ec); diff --git a/lib/http/client.cpp b/lib/http/client.cpp index 094ab5ef3..ad5f3bae6 100644 --- a/lib/http/client.cpp +++ b/lib/http/client.cpp @@ -12,13 +12,10 @@ using namespace mtx::http; using namespace boost::beast; Client::Client(const std::string &server, uint16_t port) - : resolver_{ios_} - , server_{server} + : server_{server} , port_{port} { using namespace boost::asio; - work_ = boost::in_place<io_service::work>(io_service::work(ios_)); - const auto threads_num = std::max(1U, std::thread::hardware_concurrency()); for (unsigned int i = 0; i < threads_num; ++i) @@ -31,22 +28,12 @@ Client::close() // Destroy work object. This allows the I/O thread to // exit the event loop when there are no more pending // asynchronous operations. - work_ = boost::none; + work_.reset(); // Wait for the worker threads to exit. thread_group_.join_all(); } -void -Client::do_request(std::shared_ptr<Session> s) -{ - resolver_.async_resolve( - server_, - std::to_string(port_), - std::bind( - &Session::on_resolve, std::move(s), std::placeholders::_1, std::placeholders::_2)); -} - void Client::setup_auth(Session *session, bool auth) { diff --git a/lib/http/session.cpp b/lib/http/session.cpp index a310c3cd1..4754afaa5 100644 --- a/lib/http/session.cpp +++ b/lib/http/session.cpp @@ -5,11 +5,14 @@ using namespace mtx::http; Session::Session(boost::asio::io_service &ios, boost::asio::ssl::context &ssl_ctx, const std::string &host, + uint16_t port, RequestID id, SuccessCallback on_success, FailureCallback on_failure) - : socket(ios, ssl_ctx) + : resolver_(ios) + , socket(ios, ssl_ctx) , host(std::move(host)) + , port{port} , id(std::move(id)) , on_success(std::move(on_success)) , on_failure(std::move(on_failure)) -- GitLab