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