2025-02-01 11:04:33 -08:00
|
|
|
#include "web.hpp"
|
2025-02-01 20:57:57 -08:00
|
|
|
|
|
|
|
#include <boost/beast.hpp>
|
2025-02-02 12:40:55 -08:00
|
|
|
#include <boost/json.hpp>
|
2025-02-01 20:57:57 -08:00
|
|
|
#include <boost/log/trivial.hpp>
|
2025-02-02 12:40:55 -08:00
|
|
|
#include <boost/system/system_error.hpp>
|
|
|
|
|
|
|
|
#include <openssl/hmac.h>
|
2025-02-01 20:57:57 -08:00
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
#include <fstream>
|
2025-02-02 12:40:55 -08:00
|
|
|
#include <sstream>
|
2025-02-01 20:57:57 -08:00
|
|
|
#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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-02 12:40:55 -08:00
|
|
|
static auto simple_response(http::status status, unsigned version, bool keep_alive) -> http::message_generator
|
|
|
|
{
|
|
|
|
http::response<http::string_body> res{status, version};
|
|
|
|
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
|
|
|
res.keep_alive(keep_alive);
|
|
|
|
res.content_length(0);
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
static auto compute_signature(const std::string_view secret, const std::string_view body) -> std::string
|
|
|
|
{
|
|
|
|
unsigned int digest_length = EVP_MAX_MD_SIZE;
|
|
|
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
|
|
|
|
|
|
|
HMAC(EVP_sha256(), secret.data(), secret.size(), reinterpret_cast<const unsigned char *>(body.data()), body.size(), digest, &digest_length);
|
|
|
|
|
|
|
|
std::stringstream ss;
|
|
|
|
ss << "sha256=";
|
|
|
|
ss << std::hex << std::setfill('0');
|
|
|
|
for (unsigned int i = 0; i < digest_length; i++)
|
|
|
|
{
|
|
|
|
ss << std::setw(2) << static_cast<int>(digest[i]);
|
|
|
|
}
|
|
|
|
return ss.str();
|
|
|
|
}
|
|
|
|
|
|
|
|
static auto process_event(
|
|
|
|
std::shared_ptr<GithubWebhook> self,
|
|
|
|
const std::string_view notify_user,
|
|
|
|
const std::string_view event,
|
|
|
|
const boost::json::value &json
|
|
|
|
) -> void
|
|
|
|
{
|
|
|
|
auto &project = json.as_object();
|
|
|
|
|
|
|
|
std::string full_name;
|
|
|
|
if (project.contains("repository"))
|
|
|
|
{
|
|
|
|
full_name = std::string{project.at("repository").as_object().at("full_name").as_string()};
|
|
|
|
}
|
|
|
|
else if (project.contains("organization"))
|
|
|
|
{
|
|
|
|
full_name = std::string{project.at("organization").as_object().at("login").as_string()};
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "No repository or organization detected";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &settings = self->settings_.projects.at(full_name);
|
|
|
|
|
|
|
|
// Ensure that this sender is authorized to send for this project
|
|
|
|
if (settings.credential_name != notify_user)
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "Credential mismatch for " << full_name << " wanted: " << settings.credential_name << " got: " << notify_user;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (not settings.enabled)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto message = std::string{event} + " on " + full_name;
|
|
|
|
self->add_event({settings.channel, message});
|
|
|
|
}
|
|
|
|
|
2025-02-01 20:57:57 -08:00
|
|
|
template <class Body, class Allocator>
|
2025-02-02 12:40:55 -08:00
|
|
|
static auto handle_request(
|
2025-02-01 20:57:57 -08:00
|
|
|
std::shared_ptr<GithubWebhook> self,
|
|
|
|
http::request<Body, http::basic_fields<Allocator>> &&req
|
|
|
|
) -> http::message_generator
|
|
|
|
{
|
2025-02-02 12:40:55 -08:00
|
|
|
BOOST_LOG_TRIVIAL(info) << "HTTP request " << req.method_string() << " " << req.target();
|
2025-02-01 20:57:57 -08:00
|
|
|
|
2025-02-02 12:40:55 -08:00
|
|
|
if (not req.target().starts_with("/notify/"))
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "HTTP Bad target: " << req.target();
|
|
|
|
return simple_response(http::status::not_found, req.version(), req.keep_alive());
|
|
|
|
}
|
|
|
|
std::string notify_user = req.target().substr(8);
|
|
|
|
|
|
|
|
if (req.method() != http::verb::post)
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "HTTP Bad method: " << req.method_string();
|
|
|
|
return simple_response(http::status::method_not_allowed, req.version(), req.keep_alive());
|
|
|
|
}
|
2025-02-01 20:57:57 -08:00
|
|
|
|
2025-02-02 12:40:55 -08:00
|
|
|
const auto event = req["x-github-event"];
|
|
|
|
const auto signature = req["x-hub-signature-256"];
|
|
|
|
if (event.empty() || signature.empty())
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "HTTP Missing headers";
|
|
|
|
return simple_response(http::status::bad_request, req.version(), req.keep_alive());
|
|
|
|
}
|
2025-02-01 20:57:57 -08:00
|
|
|
|
2025-02-02 12:40:55 -08:00
|
|
|
const auto credential_cursor = self->settings_.credentials.find(notify_user);
|
|
|
|
if (credential_cursor == self->settings_.credentials.end())
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "HTTP Unknown user: " << notify_user;
|
|
|
|
return simple_response(http::status::unauthorized, req.version(), req.keep_alive());
|
|
|
|
}
|
|
|
|
const auto &secret = credential_cursor->second;
|
|
|
|
|
|
|
|
const auto expected_signature = compute_signature(secret, req.body());
|
|
|
|
if (signature != expected_signature)
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(warning) << "HTTP Bad signature: " << signature << " expected: " << expected_signature;
|
|
|
|
return simple_response(http::status::unauthorized, req.version(), req.keep_alive());
|
|
|
|
}
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
process_event(self, notify_user, event, boost::json::parse(req.body()));
|
|
|
|
}
|
|
|
|
catch (const boost::system::system_error &e)
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(error) << "HTTP Failed to process event: " << e.what();
|
|
|
|
return simple_response(http::status::internal_server_error, req.version(), req.keep_alive());
|
|
|
|
}
|
|
|
|
|
|
|
|
return simple_response(http::status::ok, req.version(), req.keep_alive());
|
2025-02-01 20:57:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
{
|
2025-02-02 12:40:55 -08:00
|
|
|
throw boost::system::system_error{ec};
|
2025-02-01 20:57:57 -08:00
|
|
|
}
|
|
|
|
|
2025-02-02 12:40:55 -08:00
|
|
|
const auto keep_alive = req.keep_alive();
|
2025-02-01 20:57:57 -08:00
|
|
|
auto msg = handle_request(self, std::move(req));
|
|
|
|
|
|
|
|
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,
|
2025-02-02 12:40:55 -08:00
|
|
|
const char *webhook_settings_filename
|
2025-02-01 20:57:57 -08:00
|
|
|
) -> 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();
|
2025-02-02 14:56:34 -08:00
|
|
|
auto webhook = std::make_shared<GithubWebhook>(std::move(settings), webhook_settings_filename);
|
2025-02-01 20:57:57 -08:00
|
|
|
boost::asio::co_spawn(io, spawn_webhook(io, webhook), report_error);
|
|
|
|
return webhook;
|
|
|
|
}
|
|
|
|
|
2025-02-02 14:56:34 -08:00
|
|
|
auto GithubWebhook::save_settings() const -> void
|
2025-02-01 20:57:57 -08:00
|
|
|
{
|
2025-02-02 14:56:34 -08:00
|
|
|
std::ofstream webhook_settings_file{settings_file};
|
|
|
|
if (!webhook_settings_file)
|
|
|
|
{
|
|
|
|
BOOST_LOG_TRIVIAL(error) << "Unable to open webhook settings file";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
webhook_settings_file << settings_.to_toml();
|
2025-02-01 20:57:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2025-02-02 12:40:55 -08:00
|
|
|
|
2025-02-01 20:57:57 -08:00
|
|
|
toml::array authorized_accounts_array;
|
|
|
|
for (const auto &account : authorized_accounts)
|
|
|
|
{
|
|
|
|
authorized_accounts_array.emplace_back(account);
|
|
|
|
}
|
2025-02-02 12:40:55 -08:00
|
|
|
|
2025-02-01 20:57:57 -08:00
|
|
|
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);
|
2025-02-02 12:40:55 -08:00
|
|
|
|
2025-02-01 20:57:57 -08:00
|
|
|
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),
|
2025-02-02 12:40:55 -08:00
|
|
|
(*credential_table)["key"].value_or(""s)
|
|
|
|
);
|
2025-02-01 20:57:57 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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),
|
2025-02-02 12:40:55 -08:00
|
|
|
ProjectSettings::from_toml(*project_table)
|
|
|
|
);
|
2025-02-01 20:57:57 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto WebhookSettings::to_toml() const -> toml::table
|
|
|
|
{
|
|
|
|
toml::array credential_tables;
|
|
|
|
for (const auto &[name, key] : credentials)
|
|
|
|
{
|
2025-02-02 12:40:55 -08:00
|
|
|
credential_tables.emplace_back(toml::table{
|
2025-02-01 20:57:57 -08:00
|
|
|
{"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)}
|
|
|
|
};
|
2025-02-02 12:40:55 -08:00
|
|
|
}
|
2025-02-02 14:56:34 -08:00
|
|
|
|
|
|
|
// Either emit the event now or save it until a connection is set
|
|
|
|
auto GithubWebhook::add_event(Notice notice) -> void
|
|
|
|
{
|
|
|
|
if (connection_)
|
|
|
|
{
|
|
|
|
connection_->send_notice(notice.target, notice.message);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
events_.emplace_back(std::move(notice));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto GithubWebhook::set_connection(std::shared_ptr<myirc::Connection> connection) -> void
|
|
|
|
{
|
|
|
|
connection_ = std::move(connection);
|
|
|
|
for (auto &&event : events_)
|
|
|
|
{
|
|
|
|
connection_->send_notice(event.target, event.message);
|
|
|
|
}
|
|
|
|
events_.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto GithubWebhook::clear_connection() -> void
|
|
|
|
{
|
|
|
|
connection_.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
static auto reply_to(std::shared_ptr<GithubWebhook> webhooks, const myirc::Bot::Command &cmd, std::string message) -> void
|
|
|
|
{
|
|
|
|
if (cmd.target.starts_with("#"))
|
|
|
|
{
|
|
|
|
webhooks->add_event({std::string{cmd.target}, std::move(message)});
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
webhooks->add_event({std::string{cmd.nick()}, std::move(message)});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static auto authorized_for_project(const ProjectSettings &project, const std::string_view nick) -> bool
|
|
|
|
{
|
|
|
|
return project.authorized_accounts.find(std::string{nick}) != project.authorized_accounts.end();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::map<std::string, void (*)(std::shared_ptr<GithubWebhook>, const myirc::Bot::Command &)> webhook_commands{
|
|
|
|
{"add-credential", [](std::shared_ptr<GithubWebhook> webhooks, const myirc::Bot::Command &cmd) {
|
|
|
|
//if (cmd.oper.empty())
|
|
|
|
//{
|
|
|
|
// return;
|
|
|
|
//}
|
|
|
|
std::istringstream iss{std::string{cmd.arguments}};
|
|
|
|
std::string name, key;
|
|
|
|
if (iss >> name >> key)
|
|
|
|
{
|
|
|
|
webhooks->settings_.credentials.insert_or_assign(name, key);
|
|
|
|
webhooks->save_settings();
|
|
|
|
reply_to(webhooks, cmd, "Added credential " + name);
|
|
|
|
}
|
|
|
|
}},
|
|
|
|
{"drop-credential", [](std::shared_ptr<GithubWebhook> webhooks, const myirc::Bot::Command &cmd) {
|
|
|
|
//if (cmd.oper.empty())
|
|
|
|
//{
|
|
|
|
// return;
|
|
|
|
//}
|
|
|
|
std::istringstream iss{std::string{cmd.arguments}};
|
|
|
|
std::string name;
|
|
|
|
if (iss >> name)
|
|
|
|
{
|
|
|
|
webhooks->settings_.credentials.erase(name);
|
|
|
|
webhooks->save_settings();
|
|
|
|
reply_to(webhooks, cmd, "Dropped credential " + name);
|
|
|
|
}
|
|
|
|
}},
|
|
|
|
{"enable-project", [](std::shared_ptr<GithubWebhook> webhooks, const myirc::Bot::Command &cmd) {
|
|
|
|
std::istringstream iss{std::string{cmd.arguments}};
|
|
|
|
std::string name;
|
|
|
|
if (iss >> name)
|
|
|
|
{
|
|
|
|
auto cursor = webhooks->settings_.projects.find(name);
|
|
|
|
if (cursor == webhooks->settings_.projects.end())
|
|
|
|
{
|
|
|
|
reply_to(webhooks, cmd, "Unknown project " + name);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto &project = cursor->second;
|
|
|
|
|
|
|
|
if (not authorized_for_project(project, cmd.account))
|
|
|
|
{
|
|
|
|
reply_to(webhooks, cmd, "Unauthorized to enable project " + name);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
project.enabled = true;
|
|
|
|
webhooks->save_settings();
|
|
|
|
reply_to(webhooks, cmd, "Enabled project " + name);
|
|
|
|
}
|
|
|
|
}},
|
|
|
|
{"disable-project", [](std::shared_ptr<GithubWebhook> webhooks, const myirc::Bot::Command &cmd) {
|
|
|
|
//if (cmd.oper.empty())
|
|
|
|
//{
|
|
|
|
// return;
|
|
|
|
//}
|
|
|
|
std::istringstream iss{std::string{cmd.arguments}};
|
|
|
|
std::string name;
|
|
|
|
if (iss >> name)
|
|
|
|
{
|
|
|
|
auto cursor = webhooks->settings_.projects.find(name);
|
|
|
|
if (cursor == webhooks->settings_.projects.end())
|
|
|
|
{
|
|
|
|
webhooks->add_event({std::string{cmd.nick()}, "Unknown project " + name});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto &project = cursor->second;
|
|
|
|
|
|
|
|
if (not authorized_for_project(project, cmd.account))
|
|
|
|
{
|
|
|
|
reply_to(webhooks, cmd, "Unauthorized to disable project " + name);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
project.enabled = false;
|
|
|
|
webhooks->save_settings();
|
|
|
|
reply_to(webhooks, cmd, "Disabled project " + name);
|
|
|
|
}
|
|
|
|
}},
|
|
|
|
};
|