Skip to content

Commit

Permalink
docs: new webhook example
Browse files Browse the repository at this point in the history
  • Loading branch information
braindigitalis committed Dec 13, 2024
1 parent 686344a commit 1cd4de9
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .clang-tidy
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# TODO: Discuss about -readability-identifier-length, -readability-avoid-const-params-in-decls
Checks: "-*,bugprone-*,cert-*,clang-analyzer-*,concurrency-*,cppcoreguidelines-*,llvm-namespace-comment,modernize-*,performance-*,portability-*,readability-*,-bugprone-implicit-widening-of-multiplication-result, -bugprone-easily-swappable-parameters,-readability-identifier-length,-portability-restrict-system-includes,-modernize-use-trailing-return-type,-cppcoreguidelines-non-private-member-variables-in-classes,-readability-avoid-const-params-in-decls,-cppcoreguidelines-owning-memory,-readability-function-cognitive-complexity"
Checks: "-*,bugprone-*,cert-*,clang-analyzer-*,concurrency-*,cppcoreguidelines-*,llvm-namespace-comment,modernize-*,performance-*,portability-*,readability-*,-bugprone-implicit-widening-of-multiplication-result,-bugprone-easily-swappable-parameters,-readability-identifier-length,-portability-restrict-system-includes,-modernize-use-trailing-return-type,-cppcoreguidelines-non-private-member-variables-in-classes,-readability-avoid-const-params-in-decls,-cppcoreguidelines-owning-memory,-readability-function-cognitive-complexity,-cppcoreguidelines-avoid-do-while"
18 changes: 14 additions & 4 deletions docpages/example_code/webhooks.cpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
#include <dpp/dpp.h>
#include <chrono>
#include <thread>

int main()
{
dpp::cluster bot(""); /* Normally, you put your bot token in here, but its not required for webhooks. */
/* If you just want to fire webhooks, you can instantiate a cluster with no token */
dpp::cluster bot;

bot.on_log(dpp::utility::cout_logger());
/* Start the cluster in its own thread */
bot.start(dpp::st_return);

/* Construct a webhook object using the URL you got from Discord */
dpp::webhook wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE");

/* Send a message with this webhook */
bot.execute_webhook_sync(wh, dpp::message("Have a great time here :smile:"));
/* Send a message with this webhook asynchronously */
bot.execute_webhook(wh, dpp::message("Have a great time here :smile:"));

/* Wait here for the webhook to complete, but we could do anything we need here */
while (bot.active_requests() > 0) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}

/* When we return here, the cluster will be terminated */
return 0;
}
7 changes: 5 additions & 2 deletions docpages/example_programs/the_basics/webhooks.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
\page webhooks Webhooks

Webhooks are a simple way to post messages from other apps and websites into Discord. They allow getting automated messages and data updates sent to a text channel in your server. [Read more](https://support.discord.com/hc/en-us/articles/228383668) in this article about Webhooks.
Webhooks are a simple way to post messages from other apps and websites into Discord.
They allow getting automated messages and data updates sent to a text channel in your server. [Read more](https://support.discord.com/hc/en-us/articles/228383668) in this article about Webhooks.

The following code shows how to send messages in a channel using a webhook.

\include{cpp} webhooks.cpp

The above is just a very simple example. You can also send embed messages. All you have to do is to add an embed to the message you want to send. If you want to, you can also send it into a thread.
\note For just sending a webhook, the example above is overkill. If you are here because you searched for 'how to send a Discord webhook in C++', you'll quickly learn that D++ can do much more than just send webhooks! The
above is just a very simple example. You can also send embed messages. All you have to do is to add an embed to the message you want to send. If you want to, you can also send it into a thread. For further examples, check
the rest of the site.
20 changes: 20 additions & 0 deletions include/dpp/cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@

namespace dpp {

constexpr uint32_t NO_SHARDS = ~0U;

/**
* @brief Types of startup for cluster::start()
*/
Expand Down Expand Up @@ -267,6 +269,16 @@ class DPP_EXPORT cluster {
*/
std::unique_ptr<socket_engine_base> socketengine;

/**
* @brief Constructor for creating a cluster without a token.
* A cluster created without a token has no shards, and just runs the event loop. You can use this to make asynchronous
* HTTP requests via e.g. dpp::cluster::request without having to connect to a websocket to receive shard events.
* @param pool_threads The number of threads to allocate for the thread pool. This defaults to half your system concurrency and if set to a number less than 4, will default to 4.
* All callbacks and events are placed into the thread pool. The bigger you make this pool (but generally no bigger than your number of cores), the more your bot will scale.
* @throw dpp::exception Thrown on windows, if WinSock fails to initialise, or on any other system if a dpp::request_queue fails to construct
*/
cluster(uint32_t pool_threads = std::thread::hardware_concurrency() / 2);

/**
* @brief Constructor for creating a cluster. All but the token are optional.
* @param token The bot token to use for all HTTP commands and websocket connections
Expand Down Expand Up @@ -524,6 +536,14 @@ class DPP_EXPORT cluster {
*/
bool register_command(const std::string& name, const slashcommand_handler_t handler);

/**
* @brief Get the number of currently active HTTP(S) requests active in the cluster.
* This total includes all in-flight API requests and calls to dpp::cluster::request().
* Note that once a request is passed to the thread pool it is no longer counted here.
* @return Total active request count
*/
size_t active_requests();

#ifdef DPP_CORO
/**
* @brief Register a coroutine-based slash command handler.
Expand Down
2 changes: 2 additions & 0 deletions include/dpp/queues.h
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,8 @@ class DPP_EXPORT request_queue {
* @return true if globally rate limited
*/
bool is_globally_ratelimited() const;

size_t get_active_request_count() const;
};

}
182 changes: 99 additions & 83 deletions src/dpp/cluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ template bool DPP_EXPORT validate_configuration<build_type::release>();

template bool DPP_EXPORT validate_configuration<build_type::universal>();

cluster::cluster(uint32_t pool_threads) : cluster("", 0, NO_SHARDS, 1, 1, false, cache_policy::cpol_none, pool_threads)
{
}

cluster::cluster(const std::string &_token, uint32_t _intents, uint32_t _shards, uint32_t _cluster_id, uint32_t _maxclusters, bool comp, cache_policy_t policy, uint32_t pool_threads)
: default_gateway("gateway.discord.gg"), rest(nullptr), raw_rest(nullptr), compressed(comp), start_time(0), token(_token), last_identify(time(nullptr) - 5), intents(_intents),
numshards(_shards), cluster_id(_cluster_id), maxclusters(_maxclusters), rest_ping(0.0), cache_policy(policy), ws_mode(ws_json)
Expand Down Expand Up @@ -206,7 +210,7 @@ void cluster::add_reconnect(uint32_t shard_id) {
void cluster::start(start_type return_after) {

auto event_loop = [this]() -> void {
auto reconnect_monitor = start_timer([this](auto t) {
auto reconnect_monitor = numshards != NO_SHARDS ? start_timer([this](auto t) {
time_t now = time(nullptr);
for (auto reconnect = reconnections.begin(); reconnect != reconnections.end(); ++reconnect) {
auto shard_id = reconnect->first;
Expand Down Expand Up @@ -258,11 +262,13 @@ void cluster::start(start_type return_after) {
log(ll_trace, "Shard " + std::to_string(shard_id) + " not ready to reconnect yet.");
}
}
}, 5);
}, 5) : 0;
while (!this->terminating && socketengine.get()) {
socketengine->process_events();
}
stop_timer(reconnect_monitor);
if (reconnect_monitor) {
stop_timer(reconnect_monitor);
}
};

if (on_guild_member_add && !(intents & dpp::i_guild_members)) {
Expand All @@ -281,100 +287,106 @@ void cluster::start(start_type return_after) {
log(ll_warning, "You have attached an event to cluster::on_presence_update() but have not specified the privileged intent dpp::i_guild_presences. This event will not fire.");
}

/* Start up all shards */
get_gateway_bot([this, return_after](const auto& response) {
if (numshards != NO_SHARDS) {
/* Start up all shards */
get_gateway_bot([this, return_after](const auto &response) {

auto throw_if_not_threaded = [this, return_after](exception_error_code error_id, const std::string& msg) {
log(ll_critical, msg);
if (return_after == st_wait) {
throw dpp::connection_exception(error_id, msg);
}
};
auto throw_if_not_threaded = [this, return_after](exception_error_code error_id, const std::string &msg) {
log(ll_critical, msg);
if (return_after == st_wait) {
throw dpp::connection_exception(error_id, msg);
}
};

if (response.is_error()) {
if (response.http_info.status == 401) {
throw_if_not_threaded(err_unauthorized, "Invalid bot token (401: Unauthorized when getting gateway shard count)");
} else {
throw_if_not_threaded(err_auto_shard, "get_gateway_bot: " + response.http_info.body);
}
return;
}
auto g = std::get<gateway>(response.value);
log(ll_debug, "Cluster: " + std::to_string(g.session_start_remaining) + " of " + std::to_string(g.session_start_total) + " session starts remaining");
if (g.session_start_remaining < g.shards || g.shards == 0) {
throw_if_not_threaded(err_no_sessions_left, "Discord indicates you cannot start enough sessions to boot this cluster! Cluster startup aborted. Try again later.");
return;
} else if (g. session_start_max_concurrency == 0) {
throw_if_not_threaded(err_auto_shard, "Cluster: Could not determine concurrency, startup aborted!");
return;
} else if (g.session_start_max_concurrency > 1) {
log(ll_debug, "Cluster: Large bot sharding; Using session concurrency: " + std::to_string(g.session_start_max_concurrency));
} else if (numshards == 0) {
if (g.shards) {
log(ll_info, "Auto Shard: Bot requires " + std::to_string(g.shards) + std::string(" shard") + ((g.shards > 1) ? "s" : ""));
} else {
throw_if_not_threaded(err_auto_shard, "Auto Shard: Cannot determine number of shards. Cluster startup aborted. Check your connection.");
if (response.is_error()) {
if (response.http_info.status == 401) {
throw_if_not_threaded(err_unauthorized, "Invalid bot token (401: Unauthorized when getting gateway shard count)");
} else {
throw_if_not_threaded(err_auto_shard, "get_gateway_bot: " + response.http_info.body);
}
return;
}
numshards = g.shards;
}
start_time = time(nullptr);
log(ll_debug, "Starting with " + std::to_string(numshards) + " shards...");

for (uint32_t s = 0; s < numshards; ++s) {
/* Filter out shards that aren't part of the current cluster, if the bot is clustered */
if (s % maxclusters == cluster_id) {
/* Each discord_client is inserted into the socket engine when we call run() */
try {
this->shards[s] = new discord_client(this, s, numshards, token, intents, compressed, ws_mode);
this->shards[s]->run();
}
catch (const std::exception &e) {
throw_if_not_threaded(err_cant_start_shard, "Could not start shard " + std::to_string(s) + ": " + std::string(e.what()));
auto g = std::get<gateway>(response.value);
log(ll_debug, "Cluster: " + std::to_string(g.session_start_remaining) + " of " + std::to_string(g.session_start_total) + " session starts remaining");
if (g.session_start_remaining < g.shards || g.shards == 0) {
throw_if_not_threaded(err_no_sessions_left, "Discord indicates you cannot start enough sessions to boot this cluster! Cluster startup aborted. Try again later.");
return;
} else
if (g.session_start_max_concurrency == 0) {
throw_if_not_threaded(err_auto_shard, "Cluster: Could not determine concurrency, startup aborted!");
return;
}
/* Stagger the shard startups, pausing every 'session_start_max_concurrency' shards for 5 seconds.
* This means that for bots that don't have large bot sharding, any number % 1 is always 0,
* so it will pause after every shard. For any with non-zero concurrency it'll pause 5 seconds
* after every batch.
*/
if (((s + 1) % g.session_start_max_concurrency) == 0) {
size_t wait_time = 5;
} else
if (g.session_start_max_concurrency > 1) {
/* If large bot sharding, be sure to give the batch of shards time to settle */
bool all_connected = true;
do {
all_connected = true;
for (auto& shard : this->shards) {
if (!shard.second->ready) {
all_connected = false;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
break;
}
log(ll_debug, "Cluster: Large bot sharding; Using session concurrency: " + std::to_string(g.session_start_max_concurrency));
} else
if (numshards == 0) {
if (g.shards) {

Check notice on line 323 in src/dpp/cluster.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/dpp/cluster.cpp#L323

Condition 'g.shards' is always true
log(ll_info, "Auto Shard: Bot requires " + std::to_string(g.shards) + std::string(" shard") + ((g.shards > 1) ? "s" : ""));
} else {
throw_if_not_threaded(err_auto_shard, "Auto Shard: Cannot determine number of shards. Cluster startup aborted. Check your connection.");
return;
}
} while (all_connected);
numshards = g.shards;
}
log(ll_debug, "Starting with " + std::to_string(numshards) + " shards...");
start_time = time(nullptr);

for (uint32_t s = 0; s < numshards; ++s) {
/* Filter out shards that aren't part of the current cluster, if the bot is clustered */
if (s % maxclusters == cluster_id) {
/* Each discord_client is inserted into the socket engine when we call run() */
try {
this->shards[s] = new discord_client(this, s, numshards, token, intents, compressed, ws_mode);
this->shards[s]->run();
}
catch (const std::exception &e) {
throw_if_not_threaded(err_cant_start_shard, "Could not start shard " + std::to_string(s) + ": " + std::string(e.what()));
return;
}
/* Stagger the shard startups, pausing every 'session_start_max_concurrency' shards for 5 seconds.
* This means that for bots that don't have large bot sharding, any number % 1 is always 0,
* so it will pause after every shard. For any with non-zero concurrency it'll pause 5 seconds
* after every batch.
*/
if (((s + 1) % g.session_start_max_concurrency) == 0) {
size_t wait_time = 5;
if (g.session_start_max_concurrency > 1) {
/* If large bot sharding, be sure to give the batch of shards time to settle */
bool all_connected = true;
do {
all_connected = true;
for (auto &shard: this->shards) {
if (!shard.second->ready) {
all_connected = false;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
break;
}
}
} while (all_connected);
}
std::this_thread::sleep_for(std::chrono::seconds(wait_time));
}
std::this_thread::sleep_for(std::chrono::seconds(wait_time));
}
}
}

/* Get all active DM channels and map them to user id -> dm id */
current_user_get_dms([this](const dpp::confirmation_callback_t& completion) {
if (completion.is_error()) {
log(dpp::ll_debug, "Failed to get bot DM list");
return;
}
dpp::channel_map dmchannels = std::get<channel_map>(completion.value);
for (auto & c : dmchannels) {
for (auto & u : c.second.recipients) {
set_dm_channel(u, c.second.id);
/* Get all active DM channels and map them to user id -> dm id */
current_user_get_dms([this](const dpp::confirmation_callback_t &completion) {
if (completion.is_error()) {
log(dpp::ll_debug, "Failed to get bot DM list");
return;
}
}
dpp::channel_map dmchannels = std::get<channel_map>(completion.value);
for (auto &c: dmchannels) {
for (auto &u: c.second.recipients) {
set_dm_channel(u, c.second.id);
}
}
});

log(ll_debug, "Shards started.");
});

log(ll_debug, "Shards started.");
});
}

if (return_after == st_return) {
engine_thread = std::thread([event_loop]() {
Expand Down Expand Up @@ -583,4 +595,8 @@ bool cluster::unregister_command(const std::string &name) {
return named_commands.erase(name) == 1;
}

size_t cluster::active_requests() {
return rest->get_active_request_count() + raw_rest->get_active_request_count();
}

};
2 changes: 2 additions & 0 deletions src/dpp/discordclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ constexpr int LARGE_THRESHOLD = 250;
discord_client::discord_client(discord_client &old, uint64_t sequence, const std::string& session_id)
: websocket_client(old.owner, old.resume_gateway_url, "443", old.compressed ? (old.protocol == ws_json ? PATH_COMPRESSED_JSON : PATH_COMPRESSED_ETF) : (old.protocol == ws_json ? PATH_UNCOMPRESSED_JSON : PATH_UNCOMPRESSED_ETF)),
compressed(old.compressed),
zlib(nullptr),
connect_time(0),
ping_start(0.0),
etf(nullptr),
Expand All @@ -79,6 +80,7 @@ discord_client::discord_client(discord_client &old, uint64_t sequence, const std
discord_client::discord_client(dpp::cluster* _cluster, uint32_t _shard_id, uint32_t _max_shards, const std::string &_token, uint32_t _intents, bool comp, websocket_protocol_t ws_proto)
: websocket_client(_cluster, _cluster->default_gateway, "443", comp ? (ws_proto == ws_json ? PATH_COMPRESSED_JSON : PATH_COMPRESSED_ETF) : (ws_proto == ws_json ? PATH_UNCOMPRESSED_JSON : PATH_UNCOMPRESSED_ETF)),
compressed(comp),
zlib(nullptr),
connect_time(0),
ping_start(0.0),
etf(nullptr),
Expand Down
15 changes: 11 additions & 4 deletions src/dpp/queues.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,17 +456,24 @@ inline uint32_t hash(const char *s)
}

/* Post a http_request into a request queue */
request_queue& request_queue::post_request(std::unique_ptr<http_request> req)
{
request_queue& request_queue::post_request(std::unique_ptr<http_request> req) {
if (!terminating) {
requests_in[hash(req->endpoint.c_str()) % in_queue_pool_size]->post_request(std::move(req));
}
return *this;
}

bool request_queue::is_globally_ratelimited() const
{
bool request_queue::is_globally_ratelimited() const {
return this->globally_ratelimited;
}

size_t request_queue::get_active_request_count() const {
size_t total{};
for (auto& pool : requests_in) {
std::scoped_lock lock(pool->in_mutex);
total += pool->requests_in.size();
}
return total;
}

}
6 changes: 2 additions & 4 deletions src/dpp/zlibcontext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@

namespace dpp {

zlibcontext::zlibcontext() {
d_stream = new z_stream();
zlibcontext::zlibcontext() : d_stream(new z_stream()) {
std::memset(d_stream, 0, sizeof(z_stream));
int error = inflateInit(d_stream);
if (error != Z_OK) {
Expand All @@ -51,8 +50,7 @@ exception_error_code zlibcontext::decompress(const std::string& buffer, std::str
d_stream->avail_out = DECOMP_BUFFER_SIZE;
int ret = inflate(d_stream, Z_NO_FLUSH);
size_t have = DECOMP_BUFFER_SIZE - d_stream->avail_out;
switch (ret)
{
switch (ret) {
case Z_NEED_DICT:
case Z_STREAM_ERROR:
return err_compression_stream;
Expand Down

0 comments on commit 1cd4de9

Please sign in to comment.