640 lines
17 KiB
C++

/************************************************************************************
*
* D++, A Lightweight C++ library for Discord
*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2021 Craig Edwards and D++ contributors
* (https://github.com/brainboxdotcc/DPP/graphs/contributors)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
************************************************************************************/
#pragma once
#include <dpp/export.h>
#include <unordered_map>
#include <string>
#include <queue>
#include <map>
#include <shared_mutex>
#include <memory>
#include <vector>
#include <functional>
#include <atomic>
#include <dpp/httpsclient.h>
namespace dpp {
/**
* @brief Error values. Most of these are currently unused in https_client.
*/
enum http_error : uint8_t {
/**
* @brief Request successful.
*/
h_success = 0,
/**
* @brief Status unknown.
*/
h_unknown,
/**
* @brief Connect failed.
*/
h_connection,
/**
* @brief Invalid local ip address.
*/
h_bind_ip_address,
/**
* @brief Read error.
*/
h_read,
/**
* @brief Write error.
*/
h_write,
/**
* @brief Too many 30x redirects.
*/
h_exceed_redirect_count,
/**
* @brief Request cancelled.
*/
h_canceled,
/**
* @brief SSL connection error.
*/
h_ssl_connection,
/**
* @brief SSL cert loading error.
*/
h_ssl_loading_certs,
/**
* @brief SSL server verification error.
*/
h_ssl_server_verification,
/**
* @brief Unsupported multipart boundary characters.
*/
h_unsupported_multipart_boundary_chars,
/**
* @brief Compression error.
*/
h_compression,
};
/**
* @brief The result of any HTTP request. Contains the headers, vital
* rate limit figures, and returned request body.
*/
struct DPP_EXPORT http_request_completion_t {
/**
* @brief HTTP headers of response.
*/
std::multimap<std::string, std::string> headers;
/**
* @brief HTTP status.
* e.g. 200 = OK, 404 = Not found, 429 = Rate limited, etc.
*/
uint16_t status = 0;
/**
* @brief Error status.
* e.g. if the request could not connect at all.
*/
http_error error = h_success;
/**
* @brief Ratelimit bucket.
*/
std::string ratelimit_bucket;
/**
* @brief Ratelimit limit of requests.
*/
uint64_t ratelimit_limit = 0;
/**
* @brief Ratelimit remaining requests.
*/
uint64_t ratelimit_remaining = 0;
/**
* @brief Ratelimit reset after (seconds).
*/
uint64_t ratelimit_reset_after = 0;
/**
* @brief Ratelimit retry after (seconds).
*/
uint64_t ratelimit_retry_after = 0;
/**
* @brief True if this request has caused us to be globally rate limited.
*/
bool ratelimit_global = false;
/**
* @brief Reply body.
*/
std::string body;
/**
* @brief Ping latency.
*/
double latency;
};
/**
* @brief Results of HTTP requests are called back to these std::function types.
*
* @note Returned http_completion_events are called ASYNCHRONOUSLY in your
* code which means they execute in a separate thread, results for the requests going
* into a dpp::thread_pool. Completion events may not arrive in order depending on if
* one request takes longer than another. Using the callbacks or using coroutines
* correctly ensures that the order they arrive in the queue does not negatively affect
* your code.
*/
typedef std::function<void(const http_request_completion_t&)> http_completion_event;
/**
* @brief Various types of http method supported by the Discord API
*/
enum http_method {
/**
* @brief GET.
*/
m_get,
/**
* @brief POST.
*/
m_post,
/**
* @brief PUT.
*/
m_put,
/**
* @brief PATCH.
*/
m_patch,
/**
* @brief DELETE.
*/
m_delete
};
/**
* @brief A HTTP request.
*
* You should instantiate one of these objects via its constructor,
* and pass a pointer to it into an instance of request_queue. Although you can
* directly call the run() method of the object and it will make a HTTP call, be
* aware that if you do this, it will be a **BLOCKING call** (not asynchronous) and
* will not respect rate limits, as both of these functions are managed by the
* request_queue class.
*/
class DPP_EXPORT http_request {
/**
* @brief Completion callback.
*/
http_completion_event complete_handler;
/**
* @brief True if request has been made.
*/
bool completed;
/**
* @brief True for requests that are not going to discord (rate limits code skipped).
*/
bool non_discord;
/**
* @brief HTTPS client
*/
std::unique_ptr<https_client> cli;
public:
/**
* @brief Endpoint name
* e.g. /api/users.
*/
std::string endpoint;
/**
* @brief Major and minor parameters.
*/
std::string parameters;
/**
* @brief Postdata for POST and PUT.
*/
std::string postdata;
/**
* @brief HTTP method for request.
*/
http_method method;
/**
* @brief Audit log reason for Discord requests, if non-empty.
*/
std::string reason;
/**
* @brief Upload file name (server side).
*/
std::vector<std::string> file_name;
/**
* @brief Upload file contents (binary).
*/
std::vector<std::string> file_content;
/**
* @brief Upload file mime types.
* application/octet-stream if unspecified.
*/
std::vector<std::string> file_mimetypes;
/**
* @brief Request mime type.
*/
std::string mimetype;
/**
* @brief Request headers (non-discord requests only).
*/
std::multimap<std::string, std::string> req_headers;
/**
* @brief Waiting for rate limit to expire.
*/
bool waiting;
/**
* @brief HTTP protocol.
*/
std::string protocol;
/**
* @brief Constructor. When constructing one of these objects it should be passed to request_queue::post_request().
* @param _endpoint The API endpoint, e.g. /api/guilds
* @param _parameters Major and minor parameters for the endpoint e.g. a user id or guild id
* @param completion completion event to call when done
* @param _postdata Data to send in POST and PUT requests
* @param method The HTTP method to use from dpp::http_method
* @param audit_reason Audit log reason to send, empty to send none
* @param filename The filename (server side) of any uploaded file
* @param filecontent The binary content of any uploaded file for the request
* @param filemimetype The MIME type of any uploaded file for the request
* @param http_protocol HTTP protocol
*/
http_request(const std::string &_endpoint, const std::string &_parameters, http_completion_event completion, const std::string &_postdata = "", http_method method = m_get, const std::string &audit_reason = "", const std::string &filename = "", const std::string &filecontent = "", const std::string &filemimetype = "", const std::string &http_protocol = "1.1");
/**
* @brief Constructor. When constructing one of these objects it should be passed to request_queue::post_request().
* @param _endpoint The API endpoint, e.g. /api/guilds
* @param _parameters Major and minor parameters for the endpoint e.g. a user id or guild id
* @param completion completion event to call when done
* @param _postdata Data to send in POST and PUT requests
* @param method The HTTP method to use from dpp::http_method
* @param audit_reason Audit log reason to send, empty to send none
* @param filename The filename (server side) of any uploaded file
* @param filecontent The binary content of any uploaded file for the request
* @param filemimetypes The MIME type of any uploaded file for the request
* @param http_protocol HTTP protocol
*/
http_request(const std::string &_endpoint, const std::string &_parameters, http_completion_event completion, const std::string &_postdata = "", http_method method = m_get, const std::string &audit_reason = "", const std::vector<std::string> &filename = {}, const std::vector<std::string> &filecontent = {}, const std::vector<std::string> &filemimetypes = {}, const std::string &http_protocol = "1.1");
/**
* @brief Constructor. When constructing one of these objects it should be passed to request_queue::post_request().
* @param _url Raw HTTP url
* @param completion completion event to call when done
* @param method The HTTP method to use from dpp::http_method
* @param _postdata Data to send in POST and PUT requests
* @param _mimetype POST data mime type
* @param _headers HTTP headers to send
* @param http_protocol HTTP protocol
*/
http_request(const std::string &_url, http_completion_event completion, http_method method = m_get, const std::string &_postdata = "", const std::string &_mimetype = "text/plain", const std::multimap<std::string, std::string> &_headers = {}, const std::string &http_protocol = "1.1");
/**
* @brief Destroy the http request object
*/
~http_request();
/**
* @brief Call the completion callback, if the request is complete.
* @param c callback to call
*/
void complete(const http_request_completion_t &c);
/**
* @brief Execute the HTTP request and mark the request complete.
* @param processor Request concurrency queue this request was created by
* @param owner creating cluster
*/
http_request_completion_t run(class request_concurrency_queue* processor, class cluster* owner);
/**
* @brief Returns true if the request is complete
* @return True if completed
* */
bool is_completed();
/**
* @brief Get the HTTPS client used to perform this request, or nullptr if there is none
* @return https_client object
*/
https_client* get_client() const;
};
/**
* @brief A rate limit bucket. The library builds one of these for
* each endpoint.
*/
struct DPP_EXPORT bucket_t {
/**
* @brief Request limit.
*/
uint64_t limit;
/**
* @brief Requests remaining.
*/
uint64_t remaining;
/**
* @brief Rate-limit of this bucket resets after this many seconds.
*/
uint64_t reset_after;
/**
* @brief Rate-limit of this bucket can be retried after this many seconds.
*/
uint64_t retry_after;
/**
* @brief Timestamp this buckets counters were updated.
*/
time_t timestamp;
};
/**
* @brief Represents a timer instance in a pool handling requests to HTTP(S) servers.
* There are several of these, the total defined by a constant in cluster.cpp, and each
* one will always receive requests for the same rate limit bucket based on its endpoint
* portion of the url. This makes rate limit handling reliable and easy to manage.
* Each of these also has its own mutex, making it thread safe to call and use these
* from anywhere in the code.
*/
class DPP_EXPORT request_concurrency_queue {
public:
/**
* @brief Queue index
*/
int in_index{0};
/**
* @brief True if ending.
*/
std::atomic<bool> terminating;
/**
* @brief Request queue that owns this request_concurrency_queue.
*/
class request_queue* requests;
/**
* @brief The cluster that owns this request_concurrency_queue.
*/
class cluster* creator;
/**
* @brief Inbound queue mutex thread safety.
*/
std::shared_mutex in_mutex;
/**
* @brief Inbound queue timer. The timer is called every second,
* and when it wakes up it checks for requests pending to be sent in the queue.
* If there are any requests and we are not waiting on rate limit, it will send them,
* else it will wait for the rate limit to expire.
*/
dpp::timer in_timer;
/**
* @brief Rate-limit bucket counters.
*/
std::map<std::string, bucket_t> buckets;
/**
* @brief Queue of requests to be made. Sorted by http_request::endpoint.
*/
std::vector<std::unique_ptr<http_request>> requests_in;
/**
* @brief Requests to remove after a set amount of time has passed
*/
std::vector<std::unique_ptr<http_request>> removals;
/**
* @brief Timer callback
* @param index Index ID for this timer
*/
void tick_and_deliver_requests(uint32_t index);
/**
* @brief Construct a new concurrency queue object
*
* @param owner Owning cluster
* @param req_q Owning request queue
* @param index Queue index number, uniquely identifies this queue for hashing
*/
request_concurrency_queue(class cluster* owner, class request_queue* req_q, uint32_t index);
/**
* @brief Destroy the concurrency queue object
* This will stop the timer.
*/
~request_concurrency_queue();
/**
* @brief Flags the queue as terminating
* This will set the internal atomic bool that indicates this queue is to accept no more requests
*/
void terminate();
/**
* @brief Post a http_request to this queue.
*
* @param req http_request to post. The pointer will be freed when it has
* been executed.
*/
void post_request(std::unique_ptr<http_request> req);
};
/**
* @brief The request_queue class manages rate limits and marshalls HTTP requests that have
* been built as http_request objects.
*
* It ensures asynchronous delivery of events and queueing of requests.
*
* It will spawn multiple timers to make outbound HTTP requests and then call the callbacks
* of those requests on completion within the dpp::thread_pool for the cluster.
* If the user decides to take a long time processing a reply in their callback it won't affect
* when other requests are sent, and if a HTTP request takes a long time due to latency, it won't
* hold up user processing.
*
* There are usually two request_queue objects in each dpp::cluster, one of which is used
* internally for the various REST methods to Discord such as sending messages, and the other
* used to support user REST calls via dpp::cluster::request(). They are separated so that the
* one for user requests can be specifically configured to never ever send the Discord token
* unless it is explicitly placed into the request, for security reasons.
*/
class DPP_EXPORT request_queue {
public:
/**
* @brief Required so request_concurrency_queue can access these member variables
*/
friend class request_concurrency_queue;
/**
* @brief The cluster that owns this request_queue
*/
class cluster* creator;
/**
* @brief A completed request. Contains both the request and the response
*/
struct completed_request {
/**
* @brief Request sent
*/
std::unique_ptr<http_request> request;
/**
* @brief Response to the request
*/
std::unique_ptr<http_request_completion_t> response;
};
/**
* @brief A vector of timers forming a pool.
*
* There are a set number of these defined by a constant in cluster.cpp. A request is always placed
* on the same element in this vector, based upon its url, so that two conditions are satisfied:
*
* 1) Any requests for the same ratelimit bucket are handled by the same concurrency queue in the pool so that
* they do not create unnecessary 429 errors,
* 2) Requests for different endpoints go into different buckets, so that they may be requested in parallel
* A global ratelimit event pauses all timers in the pool. These are few and far between.
*/
std::vector<std::unique_ptr<request_concurrency_queue>> requests_in;
/**
* @brief Set to true if the timers should terminate.
* When this is set to true no further requests are accepted to the queues.
*/
std::atomic<bool> terminating;
/**
* @brief True if globally rate limited
*
* When globally rate limited the concurrency queues associated with this request queue
* will not process any requests in their timers until the global rate limit expires.
*/
bool globally_ratelimited;
/**
* @brief When we are globally rate limited until (unix epoch)
*
* @note Only valid if globally_rate limited is true. If we are globally rate limited,
* queues in this class will not process requests until the current unix epoch time
* is greater than this time.
*/
time_t globally_limited_until;
/**
* @brief Number of request queues in the pool. This is the direct size of the requests_in
* vector.
*/
uint32_t in_queue_pool_size;
/**
* @brief constructor
* @param owner The creating cluster.
* @param request_concurrency The number of http request queues to allocate.
* Each request queue is a dpp::timer which ticks every second looking for new
* requests to run. The timer will hold back requests if we are waiting as to comply
* with rate limits. Adding a request to this class will cause the queue it is placed in
* to run immediately but this cannot override rate limits.
* By default eight concurrency queues are allocated.
* Side effects: Creates timers for the queue
*/
request_queue(class cluster* owner, uint32_t request_concurrency = 8);
/**
* @brief Get the request queue concurrency count
* @return uint32_t number of request queues that are active
*/
uint32_t get_request_queue_count() const;
/**
* @brief Destroy the request queue object.
* Side effects: Ends and deletes concurrency timers
*/
~request_queue();
/**
* @brief Put a http_request into the request queue.
* @note Will use a simple hash function to determine which of the 'in queues' to place
* this request onto.
* @param req request to add
* @return reference to self
*/
request_queue& post_request(std::unique_ptr<http_request> req);
/**
* @brief Returns true if the bot is currently globally rate limited
* @return true if globally rate limited
*/
bool is_globally_ratelimited() const;
/**
* @brief Returns the number of active requests on this queue
* @return Total number of active requests
*/
size_t get_active_request_count() const;
};
}