Last active
August 14, 2019 15:53
-
-
Save markhc/da32e507e488989e06541178d892851b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "tenshi/gateway.hpp" | |
#include <fmt/format.h> | |
#include <spdlog/spdlog.h> | |
#include <boost/beast/core/buffers_to_string.hpp> | |
#include <boost/bind.hpp> | |
#include <nlohmann/json.hpp> | |
namespace tenshi | |
{ | |
using namespace std::literals; | |
using json = nlohmann::json; | |
static void fail(beast::error_code ec, char const* what) { | |
spdlog::error("{}: {}", what, ec.message()); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
Gateway::Gateway(net::io_context& ioc, ssl::context& ctx) | |
: ws_(net::make_strand(ioc), ctx), | |
heartbeatTimer_(net::make_strand(ioc)), | |
resolver_(net::make_strand(ioc)), | |
host_("gateway.discord.gg"s), | |
port_("443"s), | |
target_("/?v=6&encoding=json"s) { | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::async_connect() { | |
SPDLOG_DEBUG("Starting gateway connection..."); | |
ws_.next_layer().set_verify_mode(ssl::verify_none); | |
SPDLOG_DEBUG("Resolving hostname"); | |
resolver_.async_resolve(host_, port_, make_handler(&Gateway::onResolve)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onResolve(boost::system::error_code ec, tcp::resolver::results_type results) { | |
if (ec) { | |
return fail(ec, "resolve"); | |
} | |
// Set a timeout on the operation | |
beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); | |
SPDLOG_DEBUG("Connecting to host"); | |
beast::get_lowest_layer(ws_).async_connect(results, make_handler(&Gateway::onConnect)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onConnect(boost::system::error_code ec, | |
[[maybe_unused]] tcp::resolver::results_type::endpoint_type endpoint) { | |
if (ec) { | |
return fail(ec, "connect"); | |
} | |
SPDLOG_DEBUG( | |
"Succesfully connected to {} on port {}", endpoint.address().to_string(), endpoint.port()); | |
// Set a timeout on the operation | |
beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); | |
SPDLOG_DEBUG("Performing SSL handshake"); | |
ws_.next_layer().async_handshake(ssl::stream_base::client, make_handler(&Gateway::onSslHandshake)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onSslHandshake(beast::error_code ec) { | |
if (ec) { | |
return fail(ec, "ssl_handshake"); | |
} | |
SPDLOG_DEBUG("SSL handshake successful"); | |
// Turn off the timeout on the tcp_stream, because | |
// the websocket stream has its own timeout system. | |
beast::get_lowest_layer(ws_).expires_never(); | |
// Set suggested timeout settings for the websocket | |
ws_.set_option(ws::stream_base::timeout::suggested(beast::role_type::client)); | |
// Set a decorator to change the User-Agent of the handshake | |
ws_.set_option(ws::stream_base::decorator( | |
[](ws::request_type& req) { req.set(http::field::user_agent, TENSHI_USER_AGENT); })); | |
SPDLOG_DEBUG("Performing Websocket handshake"); | |
ws_.async_handshake(host_, target_, make_handler(&Gateway::onHandshake)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onHandshake(boost::system::error_code ec) { | |
if (ec) { | |
return fail(ec, "handshake"); | |
} | |
SPDLOG_DEBUG("Websocket handshake successful"); | |
SPDLOG_DEBUG("Waiting for server Hello"); | |
// Send the message | |
ws_.async_read(buffer_, make_handler(&Gateway::onHello)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onHello(beast::error_code ec, std::size_t nbytes) { | |
boost::ignore_unused(nbytes); | |
if (ec) { | |
return fail(ec, "onHello"); | |
} | |
SPDLOG_DEBUG("Received Hello message"); | |
auto const response = json::parse(boost::beast::buffers_to_string(buffer_.data())); | |
TENSHI_ASSERT(response["op"].is_number_integer()); | |
TENSHI_ASSERT(response["op"].get<int>() == Opcode::Hello); | |
auto const& payload = response["d"].get<json>(); | |
heartbeatInterval_ = payload["heartbeat_interval"].get<std::uint32_t>(); | |
heartbeatTimer_.expires_after(std::chrono::milliseconds(1000)); | |
heartbeatTimer_.async_wait(boost::bind(&Gateway::onHeartbeatDeadline, this)); | |
SPDLOG_DEBUG("Heartbeat interval {}", heartbeatInterval_); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onHeartbeatDeadline() { | |
SPDLOG_DEBUG("Sending heartbeat"); | |
dispatchWrite(make_heartbeat()); | |
heartbeatTimer_.expires_after(std::chrono::milliseconds(1000)); | |
heartbeatTimer_.async_wait(boost::bind(&Gateway::onHeartbeatDeadline, this)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onRead(beast::error_code ec, std::size_t nbytes) { | |
boost::ignore_unused(nbytes); | |
if (ec) { | |
return fail(ec, "onRead"); | |
} | |
SPDLOG_DEBUG("Received message: {}", boost::beast::buffers_to_string(buffer_.data())); | |
ws_.async_read(buffer_, make_handler(&Gateway::onRead)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::onWrite(beast::error_code ec, std::size_t nbytes) { | |
boost::ignore_unused(nbytes); | |
SPDLOG_DEBUG("onWrite"); | |
if (ec) { | |
return fail(ec, "onWrite"); | |
} | |
if (!writeQueue_.empty()) { | |
std::string item; | |
writeQueue_.pop(item); | |
SPDLOG_DEBUG("Sending message: {}", item); | |
ws_.async_write(net::buffer(item), make_handler(&Gateway::onWrite)); | |
} | |
} | |
// ------------------------------------------------------------------------------------------------- | |
void Gateway::dispatchWrite(std::string item) { | |
if (writeQueue_.empty()) { | |
writeQueue_.push(std::move(item)); | |
} else { | |
writeQueue_.push(std::move(item)); | |
onWrite(beast::error_code(), 0); | |
} | |
} | |
// ------------------------------------------------------------------------------------------------- | |
std::string Gateway::make_heartbeat() const { | |
using std::to_string; | |
return fmt::format(R"( {{ "op": {}, "d": {} }} )", | |
Opcode::Heartbeat, | |
sequenceNumber_ == -1 ? "null"s : to_string(sequenceNumber_)); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
} // namespace tenshi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment