mirror of
https://github.com/VCMP-SqMod/SqMod.git
synced 2024-11-08 08:47:17 +01:00
458 lines
15 KiB
C++
458 lines
15 KiB
C++
/************************************************************************************
|
|
*
|
|
* D++, A Lightweight C++ library for Discord
|
|
*
|
|
* 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 <thread>
|
|
#include <shared_mutex>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <condition_variable>
|
|
|
|
namespace dpp {
|
|
|
|
/**
|
|
* @brief Error values. Most of these are currently unused in https_client.
|
|
*/
|
|
enum http_error {
|
|
/// Request successful
|
|
h_success = 0,
|
|
/// Status unknown
|
|
h_unknown,
|
|
/// Connect failed
|
|
h_connection,
|
|
/// Invalid local ip address
|
|
h_bind_ip_address,
|
|
/// Read error
|
|
h_read,
|
|
/// Write error
|
|
h_write,
|
|
/// Too many 30x redirects
|
|
h_exceed_redirect_count,
|
|
/// Request cancelled
|
|
h_canceled,
|
|
/// SSL connection error
|
|
h_ssl_connection,
|
|
/// SSL cert loading error
|
|
h_ssl_loading_certs,
|
|
/// SSL server verification error
|
|
h_ssl_server_verification,
|
|
/// Unsupported multipart boundary characters
|
|
h_unsupported_multipart_boundary_chars,
|
|
/// 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::map<std::string, std::string> headers;
|
|
/** @brief HTTP status, e.g. 200 = OK, 404 = Not found, 429 = Rate limited */
|
|
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. The completion events
|
|
* arrive in order.
|
|
*/
|
|
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 {
|
|
/// GET
|
|
m_get,
|
|
/// POST
|
|
m_post,
|
|
/// PUT
|
|
m_put,
|
|
/// PATCH
|
|
m_patch,
|
|
/// 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;
|
|
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 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 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
|
|
*/
|
|
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 = "");
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
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 = {});
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
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 = {});
|
|
|
|
/**
|
|
* @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 owner creating cluster
|
|
*/
|
|
http_request_completion_t run(class cluster* owner);
|
|
|
|
/** @brief Returns true if the request is complete */
|
|
bool is_completed();
|
|
};
|
|
|
|
/**
|
|
* @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 Ratelimit of this bucket resets after this many seconds */
|
|
uint64_t reset_after;
|
|
/** @brief Ratelimit 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 thread in the thread pool handling requests to HTTP(S) servers.
|
|
* There are several of these, the total defined by a constant in queues.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, so that requests are less likely to block while
|
|
* waiting for internal containers to be usable.
|
|
*/
|
|
class DPP_EXPORT in_thread {
|
|
private:
|
|
/**
|
|
* @brief True if ending
|
|
*/
|
|
bool terminating;
|
|
|
|
/**
|
|
* @brief Request queue that owns this in_thread
|
|
*/
|
|
class request_queue* requests;
|
|
|
|
/**
|
|
* @brief The cluster that owns this in_thread
|
|
*/
|
|
class cluster* creator;
|
|
|
|
/**
|
|
* @brief Inbound queue mutex thread safety
|
|
*/
|
|
std::shared_mutex in_mutex;
|
|
|
|
/**
|
|
* @brief Inbound queue thread
|
|
*/
|
|
std::thread* in_thr;
|
|
|
|
/**
|
|
* @brief Inbound queue condition, signalled when there are requests to fulfill
|
|
*/
|
|
std::condition_variable in_ready;
|
|
|
|
/**
|
|
* @brief Ratelimit bucket counters
|
|
*/
|
|
std::map<std::string, bucket_t> buckets;
|
|
|
|
/**
|
|
* @brief Queue of requests to be made
|
|
*/
|
|
std::map<std::string, std::vector<http_request*>> requests_in;
|
|
|
|
/**
|
|
* @brief Inbound queue thread loop
|
|
* @param index Thread index
|
|
*/
|
|
void in_loop(uint32_t index);
|
|
public:
|
|
/**
|
|
* @brief Construct a new in thread object
|
|
*
|
|
* @param owner Owning cluster
|
|
* @param req_q Owning request queue
|
|
* @param index Thread index number
|
|
*/
|
|
in_thread(class cluster* owner, class request_queue* req_q, uint32_t index);
|
|
|
|
/**
|
|
* @brief Destroy the in thread object
|
|
* This will end the thread that is owned by this object by joining it.
|
|
*/
|
|
~in_thread();
|
|
|
|
/**
|
|
* @brief Post a http_request to this thread.
|
|
*
|
|
* @param req http_request to post. The pointer will be freed when it has
|
|
* been executed.
|
|
*/
|
|
void post_request(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 two threads, one to make outbound HTTP requests and push the returned
|
|
* results into a queue, and the second to call the callback methods with these results.
|
|
* They are separated so that 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().
|
|
*/
|
|
class DPP_EXPORT request_queue {
|
|
protected:
|
|
/**
|
|
* @brief Required so in_thread can access these member variables
|
|
*/
|
|
friend class in_thread;
|
|
|
|
/**
|
|
* @brief The cluster that owns this request_queue
|
|
*/
|
|
class cluster* creator;
|
|
|
|
/**
|
|
* @brief Outbound queue mutex thread safety
|
|
*/
|
|
std::shared_mutex out_mutex;
|
|
|
|
/**
|
|
* @brief Outbound queue thread
|
|
* Note that although there are many 'in queues', which handle the HTTP requests,
|
|
* there is only ever one 'out queue' which dispatches the results to the caller.
|
|
* This is to simplify thread management in bots that use the library, as less mutexing
|
|
* and thread safety boilerplate is required.
|
|
*/
|
|
std::thread* out_thread;
|
|
|
|
/**
|
|
* @brief Outbound queue condition, signalled when there are requests completed to call callbacks for
|
|
*/
|
|
std::condition_variable out_ready;
|
|
|
|
/**
|
|
* @brief Completed requests queue
|
|
*/
|
|
std::queue<std::pair<http_request_completion_t*, http_request*>> responses_out;
|
|
|
|
/**
|
|
* @brief A vector of inbound request threads forming a pool.
|
|
* There are a set number of these defined by a constant in queues.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 thread 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 threads in the pool. These are few and far between.
|
|
*/
|
|
std::vector<in_thread*> requests_in;
|
|
|
|
/**
|
|
* @brief Completed requests to delete
|
|
*/
|
|
std::multimap<time_t, std::pair<http_request_completion_t*, http_request*>> responses_to_delete;
|
|
|
|
/**
|
|
* @brief Set to true if the threads should terminate
|
|
*/
|
|
bool terminating;
|
|
|
|
/**
|
|
* @brief True if globally rate limited - makes the entire request thread wait
|
|
*/
|
|
bool globally_ratelimited;
|
|
|
|
/**
|
|
* @brief How many seconds we are globally rate limited for, if globally_ratelimited is true
|
|
*/
|
|
uint64_t globally_limited_for;
|
|
|
|
/**
|
|
* @brief Number of request threads in the thread pool
|
|
*/
|
|
uint32_t in_thread_pool_size;
|
|
|
|
/**
|
|
* @brief Outbound queue thread loop
|
|
*/
|
|
void out_loop();
|
|
public:
|
|
|
|
/**
|
|
* @brief constructor
|
|
* @param owner The creating cluster.
|
|
* @param request_threads The number of http request threads to allocate to the threadpool.
|
|
* By default eight threads are allocated.
|
|
* Side effects: Creates threads for the queue
|
|
*/
|
|
request_queue(class cluster* owner, uint32_t request_threads = 8);
|
|
|
|
/**
|
|
* @brief Add more request threads to the library at runtime.
|
|
* @note You should do this at a quiet time when there are few requests happening.
|
|
* This will reorganise the hashing used to place requests into the thread pool so if you do
|
|
* this while the bot is busy there is a small chance of receiving "429 rate limited" errors.
|
|
* @param request_threads Number of threads to add. It is not possible to scale down at runtime.
|
|
* @return reference to self
|
|
*/
|
|
request_queue& add_request_threads(uint32_t request_threads);
|
|
|
|
/**
|
|
* @brief Get the request thread count
|
|
* @return uint32_t number of request threads that are active
|
|
*/
|
|
uint32_t get_request_thread_count() const;
|
|
|
|
/**
|
|
* @brief Destroy the request queue object.
|
|
* Side effects: Joins and deletes queue threads
|
|
*/
|
|
~request_queue();
|
|
|
|
/**
|
|
* @brief Put a http_request into the request queue. You should ALWAYS "new" an object
|
|
* to pass to here -- don't submit an object that's on the stack!
|
|
* @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(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;
|
|
};
|
|
|
|
};
|