/************************************************************************************ * * 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 #include #include #include #include #include #include #include #include namespace dpp { /** Encodes a url parameter similar to php urlencode() */ std::string url_encode(const std::string &value); /** Error values. Don't change the order or add extra values here, * as they map onto the error values of cpp-httplib */ 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 CoreExport http_request_completion_t { /** HTTP headers of response */ std::map headers; /** HTTP status, e.g. 200 = OK, 404 = Not found, 429 = Rate limited */ uint16_t status = 0; /** Error status (e.g. if the request could not connect at all) */ http_error error = h_success; /** Ratelimit bucket */ std::string ratelimit_bucket; /** Ratelimit limit of requests */ uint64_t ratelimit_limit = 0; /** Ratelimit remaining requests */ uint64_t ratelimit_remaining = 0; /** Ratelimit reset after (seconds) */ uint64_t ratelimit_reset_after = 0; /** Ratelimit retry after (seconds) */ uint64_t ratelimit_retry_after = 0; /** True if this request has caused us to be globally rate limited */ bool ratelimit_global = false; /** Reply body */ std::string body; }; /** * @brief Results of HTTP requests are called back to these std::function types. * @note Returned http_completion_events are called ASYNCRONOUSLY in your * code which means they execute in a separate thread. The completion events * arrive in order. */ typedef std::function http_completion_event; /** 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 CoreExport http_request { /** Completion callback */ http_completion_event complete_handler; /** True if request has been made */ bool completed; public: /** Endpoint name e.g. /api/users */ std::string endpoint; /** Major and minor parameters */ std::string parameters; /** Postdata for POST and PUT */ std::string postdata; /** HTTP method for request */ http_method method; /** Upload file name (server side) */ std::string file_name; /** Upload file contents (binary) */ std::string file_content; /** 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 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 &filename = "", const std::string &filecontent = ""); /** Destructor */ ~http_request(); /** Call the completion callback, if the request is complete. * @param c callback to call */ void complete(const http_request_completion_t &c); /** Execute the HTTP request and mark the request complete. * @param owner creating cluster */ http_request_completion_t Run(const class cluster* owner); /** Returns true if the request is complete */ bool is_completed(); }; /** A rate limit bucket. The library builds one of these for * each endpoint. */ struct CoreExport bucket_t { /** Request limit */ uint64_t limit; /** Requests remaining */ uint64_t remaining; /** Ratelimit of this bucket resets after this many seconds */ uint64_t reset_after; /** Ratelimit of this bucket can be retried after this many seconds */ uint64_t retry_after; /** Timestamp this buckets counters were updated */ time_t timestamp; }; /** * @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 is usually only one request_queue object in each dpp::cluster, which is used * internally for the various REST methods such as sending messages. */ class CoreExport request_queue { private: /** The cluster that owns this request_queue */ const class cluster* creator; /** Mutexes for thread safety */ std::mutex in_mutex; std::mutex out_mutex; /** In and out threads */ std::thread* in_thread; std::thread* out_thread; /** Ratelimit bucket counters */ std::map buckets; /** Queue of requests to be made */ std::map> requests_in; /** Completed requests queue */ std::queue> responses_out; /** Completed requests to delete */ std::multimap> responses_to_delete; /** Set to true if the threads should terminate */ bool terminating; /** True if globally rate limited - makes the entire request thread wait */ bool globally_ratelimited; /** How many seconds we are globally rate limited for, if globally_ratelimited is true */ uint64_t globally_limited_for; /** Ports for notifications of request completion. * Why are we using sockets here instead of std::condition_variable? Because * in the future we will want to notify across clusters of completion and state, * and we can't do this across processes with condition variables. */ int in_queue_port; int out_queue_port; int in_queue_listen_sock; int in_queue_connect_sock; int out_queue_listen_sock; int out_queue_connect_sock; /** Thread loop functions */ void in_loop(); void out_loop(); /** Notify request thread of a new request */ void emit_in_queue_signal(); /** Notify completion thread of new completed request */ void emit_out_queue_signal(); public: /** Constructor * @param owner The creating cluster */ request_queue(const class cluster* owner); /** Destructor */ ~request_queue(); /** 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! * @param req request to add */ void post_request(http_request *req); }; };