Skip to content

Instantly share code, notes, and snippets.

@markhc
Last active August 14, 2019 15:53
Show Gist options
  • Save markhc/da32e507e488989e06541178d892851b to your computer and use it in GitHub Desktop.
Save markhc/da32e507e488989e06541178d892851b to your computer and use it in GitHub Desktop.
#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