/************************************************************************************ * * 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 #include #include #include #include #include #include #include #include #include #include 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 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 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 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 file_name; /** * @brief Upload file contents (binary). */ std::vector file_content; /** * @brief Upload file mime types. * application/octet-stream if unspecified. */ std::vector file_mimetypes; /** * @brief Request mime type. */ std::string mimetype; /** * @brief Request headers (non-discord requests only). */ std::multimap 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 &filename = {}, const std::vector &filecontent = {}, const std::vector &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 &_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 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 buckets; /** * @brief Queue of requests to be made. Sorted by http_request::endpoint. */ std::vector> requests_in; /** * @brief Requests to remove after a set amount of time has passed */ std::vector> 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 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 request; /** * @brief Response to the request */ std::unique_ptr 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> 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 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 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; }; }