initial webhook infrastructure
This commit is contained in:
@@ -60,7 +60,11 @@ auto configure_sasl(const Settings &settings) -> std::unique_ptr<SaslMechanism>
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static auto start(boost::asio::io_context &io, const Settings &settings) -> void
|
||||
static auto start(
|
||||
boost::asio::io_context &io,
|
||||
const Settings &settings,
|
||||
std::shared_ptr<GithubWebhook> webhook
|
||||
) -> void
|
||||
{
|
||||
Ref<X509> tls_cert;
|
||||
if (settings.use_tls && not settings.tls_cert_file.empty())
|
||||
@@ -95,13 +99,18 @@ static auto start(boost::asio::io_context &io, const Settings &settings) -> void
|
||||
}
|
||||
}
|
||||
|
||||
client->sig_registered.connect([connection, webhook]() {
|
||||
webhook->set_connection(connection);
|
||||
});
|
||||
|
||||
// On disconnect reconnect in 5 seconds
|
||||
// connection is captured in the disconnect handler so it can keep itself alive
|
||||
connection->sig_disconnect.connect(
|
||||
[&io, &settings, connection]() {
|
||||
[&io, &settings, connection, webhook]() {
|
||||
webhook->clear_connection();
|
||||
auto timer = std::make_shared<boost::asio::steady_timer>(io);
|
||||
timer->expires_after(5s);
|
||||
timer->async_wait([&io, &settings, timer](auto) { start(io, settings); });
|
||||
timer->async_wait([&io, &settings, timer, webhook](auto) { start(io, settings, webhook); });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -141,12 +150,15 @@ auto main(int argc, char *argv[]) -> int
|
||||
{
|
||||
//boost::log::core::get()->set_filter(boost::log::trivial::severity >= boost::log::trivial::warning);
|
||||
|
||||
if (argc != 2) {
|
||||
if (argc != 3) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Bad arguments";
|
||||
return 1;
|
||||
}
|
||||
const auto settings = get_settings(argv[1]);
|
||||
auto io = boost::asio::io_context{};
|
||||
start(io, settings);
|
||||
|
||||
auto webhooks = start_webhook(io, argv[2]);
|
||||
|
||||
start(io, settings, webhooks);
|
||||
io.run();
|
||||
}
|
||||
|
262
driver/web.cpp
262
driver/web.cpp
@@ -1 +1,263 @@
|
||||
#include "web.hpp"
|
||||
|
||||
#include <boost/beast.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
namespace beast = boost::beast; // from <boost/beast.hpp>
|
||||
namespace http = beast::http; // from <boost/beast/http.hpp>
|
||||
namespace net = boost::asio; // from <boost/asio.hpp>
|
||||
namespace websocket = beast::websocket;
|
||||
using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace {
|
||||
|
||||
auto report_error(std::exception_ptr eptr) -> void
|
||||
{
|
||||
if (eptr)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(eptr);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "An error occurred: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <class Body, class Allocator>
|
||||
auto handle_request(
|
||||
std::shared_ptr<GithubWebhook> self,
|
||||
http::request<Body, http::basic_fields<Allocator>> &&req
|
||||
) -> http::message_generator
|
||||
{
|
||||
self->add_event({"project", "message"});
|
||||
|
||||
http::response<http::string_body> res{http::int_to_status(200), req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.keep_alive(req.keep_alive());
|
||||
|
||||
std::string reply_text = "Hello, world!";
|
||||
res.content_length(reply_text.size());
|
||||
res.body() = std::move(reply_text);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
auto read_loop(tcp::socket socket, std::shared_ptr<GithubWebhook> self) -> boost::asio::awaitable<void>
|
||||
{
|
||||
beast::tcp_stream stream{std::move(socket)};
|
||||
beast::flat_buffer buffer;
|
||||
http::request<http::string_body> req;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
req.clear();
|
||||
stream.expires_after(30s);
|
||||
|
||||
boost::system::error_code ec;
|
||||
co_await http::async_read(stream, buffer, req, net::redirect_error(net::use_awaitable, ec));
|
||||
if (ec == http::error::end_of_stream)
|
||||
{
|
||||
stream.socket().shutdown(tcp::socket::shutdown_send, ec);
|
||||
co_return;
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto msg = handle_request(self, std::move(req));
|
||||
const auto keep_alive = msg.keep_alive();
|
||||
|
||||
co_await beast::async_write(stream, std::move(msg), net::use_awaitable);
|
||||
|
||||
if (!keep_alive)
|
||||
{
|
||||
stream.socket().shutdown(tcp::socket::shutdown_send, ec);
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto accept_loop(
|
||||
tcp::acceptor acceptor,
|
||||
std::shared_ptr<GithubWebhook> self
|
||||
) -> boost::asio::awaitable<void>
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
auto socket = co_await acceptor.async_accept(net::use_awaitable);
|
||||
boost::asio::co_spawn(
|
||||
acceptor.get_executor(),
|
||||
read_loop(std::move(socket), self),
|
||||
report_error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
auto spawn_webhook(
|
||||
boost::asio::io_context &io,
|
||||
const std::shared_ptr<GithubWebhook> webhook
|
||||
) -> boost::asio::awaitable<void>
|
||||
{
|
||||
tcp::resolver resolver{io};
|
||||
auto results = co_await resolver.async_resolve(webhook->settings_.host, webhook->settings_.service, tcp::resolver::passive, boost::asio::use_awaitable);
|
||||
|
||||
for (auto &&result : results)
|
||||
{
|
||||
const auto endpoint = result.endpoint();
|
||||
BOOST_LOG_TRIVIAL(info) << "HTTP: Listening on " << endpoint;
|
||||
tcp::acceptor acceptor{io};
|
||||
acceptor.open(endpoint.protocol());
|
||||
acceptor.set_option(net::socket_base::reuse_address(true));
|
||||
acceptor.bind(endpoint);
|
||||
acceptor.listen(net::socket_base::max_listen_connections);
|
||||
boost::asio::co_spawn(io, accept_loop(std::move(acceptor), webhook), report_error);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto start_webhook(
|
||||
boost::asio::io_context &io,
|
||||
const char * webhook_settings_filename
|
||||
) -> std::shared_ptr<GithubWebhook>
|
||||
{
|
||||
std::ifstream webhook_settings_file{webhook_settings_filename};
|
||||
if (!webhook_settings_file)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file";
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
auto webhook_settings = toml::parse(webhook_settings_file);
|
||||
WebhookSettings settings = WebhookSettings::from_toml(webhook_settings);
|
||||
BOOST_LOG_TRIVIAL(info) << "Webhook settings: " << settings.to_toml();
|
||||
auto webhook = std::make_shared<GithubWebhook>(std::move(settings));
|
||||
boost::asio::co_spawn(io, spawn_webhook(io, webhook), report_error);
|
||||
return webhook;
|
||||
}
|
||||
|
||||
auto GithubWebhook::write_event(WebhookEvent event) -> void
|
||||
{
|
||||
connection_->send_notice("glguy", event.channel + ": " + event.message);
|
||||
}
|
||||
|
||||
auto ProjectSettings::from_toml(const toml::table &v) -> ProjectSettings
|
||||
{
|
||||
ProjectSettings result;
|
||||
result.channel = v["channel"].value_or(""s);
|
||||
result.credential_name = v["credential_name"].value_or(""s);
|
||||
result.enabled = v["enabled"].value_or(false);
|
||||
|
||||
if (const auto events = v["events"].as_array())
|
||||
{
|
||||
for (const auto &event : *events)
|
||||
{
|
||||
result.events.insert(event.value_or(""s));
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto accounts = v["authorized_accounts"].as_array())
|
||||
{
|
||||
for (const auto &account : *accounts)
|
||||
{
|
||||
result.authorized_accounts.insert(account.value_or(""s));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto ProjectSettings::to_toml() const -> toml::table
|
||||
{
|
||||
toml::array events_array;
|
||||
for (const auto &event : events)
|
||||
{
|
||||
events_array.emplace_back(event);
|
||||
}
|
||||
|
||||
toml::array authorized_accounts_array;
|
||||
for (const auto &account : authorized_accounts)
|
||||
{
|
||||
authorized_accounts_array.emplace_back(account);
|
||||
}
|
||||
\
|
||||
return toml::table{
|
||||
{"channel", channel},
|
||||
{"credential_name", credential_name},
|
||||
{"enabled", enabled},
|
||||
{"events", std::move(events_array)},
|
||||
{"authorized_accounts", std::move(authorized_accounts_array)}
|
||||
};
|
||||
}
|
||||
|
||||
auto WebhookSettings::from_toml(const toml::table &v) -> WebhookSettings
|
||||
{
|
||||
WebhookSettings result;
|
||||
result.host = v["host"].value_or(""s);
|
||||
result.service = v["service"].value_or("http"s);
|
||||
|
||||
if (const auto credentials = v["credentials"].as_array())
|
||||
{
|
||||
for (const auto &credential : *credentials)
|
||||
{
|
||||
if (auto credential_table = credential.as_table())
|
||||
{
|
||||
result.credentials.emplace(
|
||||
(*credential_table)["name"].value_or(""s),
|
||||
(*credential_table)["key"].value_or(""s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto projects = v["projects"].as_array())
|
||||
{
|
||||
for (const auto &project : *projects)
|
||||
{
|
||||
if (auto project_table = project.as_table())
|
||||
{
|
||||
result.projects.emplace(
|
||||
(*project_table)["name"].value_or(""s),
|
||||
ProjectSettings::from_toml(*project_table));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
auto WebhookSettings::to_toml() const -> toml::table
|
||||
{
|
||||
toml::array credential_tables;
|
||||
for (const auto &[name, key] : credentials)
|
||||
{
|
||||
credential_tables.emplace_back(toml::table {
|
||||
{"name", name},
|
||||
{"key", key}
|
||||
});
|
||||
}
|
||||
|
||||
toml::array project_tables;
|
||||
for (const auto &[name, project] : projects)
|
||||
{
|
||||
auto tab = project.to_toml();
|
||||
tab.emplace("name", name);
|
||||
project_tables.emplace_back(std::move(tab));
|
||||
}
|
||||
|
||||
return toml::table{
|
||||
{"host", host},
|
||||
{"service", service},
|
||||
{"credentials", std::move(credential_tables)},
|
||||
{"projects", std::move(project_tables)}
|
||||
};
|
||||
}
|
@@ -1 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <myirc/connection.hpp>
|
||||
|
||||
#include <toml++/toml.hpp>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/signals2.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
struct ProjectSettings {
|
||||
// *** Administrative settings ***
|
||||
|
||||
// IRC channel to announce to
|
||||
std::string channel;
|
||||
|
||||
// name extracted from notify/$user
|
||||
std::string credential_name;
|
||||
|
||||
// Authorized accounts can edit the event list
|
||||
std::set<std::string> authorized_accounts;
|
||||
|
||||
// *** User settings ***
|
||||
|
||||
// Events to announce
|
||||
std::set<std::string> events;
|
||||
|
||||
// Whether to announce events
|
||||
bool enabled;
|
||||
|
||||
auto to_toml() const -> toml::table;
|
||||
static auto from_toml(const toml::table &v) -> ProjectSettings;
|
||||
};
|
||||
|
||||
struct WebhookSettings {
|
||||
std::string host;
|
||||
std::string service;
|
||||
|
||||
std::map<std::string, std::string> credentials;
|
||||
std::map<std::string, ProjectSettings> projects;
|
||||
|
||||
auto to_toml() const -> toml::table;
|
||||
static auto from_toml(const toml::table &v) -> WebhookSettings;
|
||||
};
|
||||
|
||||
struct WebhookEvent {
|
||||
std::string channel;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
class GithubWebhook {
|
||||
// IRC connection to announce on; could be empty
|
||||
std::shared_ptr<myirc::Connection> connection_;
|
||||
|
||||
// Buffered events in case connection was inactive when event was received
|
||||
std::vector<WebhookEvent> events_;
|
||||
|
||||
|
||||
// Actually write the event to the connection.
|
||||
// Only call when there is a connection.
|
||||
auto write_event(WebhookEvent event) -> void;
|
||||
|
||||
public:
|
||||
WebhookSettings settings_;
|
||||
|
||||
GithubWebhook(WebhookSettings settings)
|
||||
: settings_(std::move(settings))
|
||||
{
|
||||
}
|
||||
|
||||
// Either emit the event now or save it until a connection is set
|
||||
auto add_event(WebhookEvent event) -> void
|
||||
{
|
||||
if (connection_) {
|
||||
write_event(std::move(event));
|
||||
} else {
|
||||
events_.emplace_back(std::move(event));
|
||||
}
|
||||
}
|
||||
|
||||
auto set_connection(std::shared_ptr<myirc::Connection> connection) -> void
|
||||
{
|
||||
connection_ = std::move(connection);
|
||||
for (auto &&event : events_)
|
||||
{
|
||||
write_event(event);
|
||||
}
|
||||
events_.clear();
|
||||
}
|
||||
|
||||
auto clear_connection() -> void
|
||||
{
|
||||
connection_.reset();
|
||||
}
|
||||
};
|
||||
|
||||
auto start_webhook(boost::asio::io_context &io, const char *) -> std::shared_ptr<GithubWebhook>;
|
||||
|
Reference in New Issue
Block a user