remove cpr as a dependency (#21)

abstract away library usage
This commit is contained in:
ouwou 2021-01-23 21:07:03 +00:00 committed by GitHub
parent 547124c94f
commit b28bfd6f20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 451 additions and 251 deletions

View File

@ -20,7 +20,7 @@ jobs:
- name: Fetch dependencies
uses: lukka/run-vcpkg@main
with:
vcpkgArguments: gtkmm nlohmann-json cpr zlib sqlite3 glibmm openssl ixwebsocket
vcpkgArguments: gtkmm nlohmann-json zlib sqlite3 glibmm openssl ixwebsocket curl
vcpkgDirectory: ${{ github.workspace }}/ci/vcpkg/
vcpkgTriplet: x64-windows
@ -71,7 +71,6 @@ jobs:
run: |
brew install gtkmm3
brew install nlohmann-json
brew install cpr
- name: Build
uses: lukka/run-cmake@main
@ -110,15 +109,6 @@ jobs:
run: |
mkdir deps
cd deps
git clone https://github.com/whoshuu/cpr
cd cpr
git checkout 9ff9cef6c794ec3d52d94a62ca791e2f2babca45
mkdir build
cd build
cmake ..
make
sudo make install
cd ../..
git clone https://github.com/nlohmann/json
cd json
git checkout db78ac1d7716f56fc9f1b030b715f872f93964e4

View File

@ -10,7 +10,6 @@ set(USE_OPEN_SSL TRUE)
find_package(nlohmann_json REQUIRED)
find_package(CURL)
find_package(cpr REQUIRED)
find_package(ZLIB REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(gtkmm REQUIRED)
@ -51,9 +50,9 @@ file(GLOB ABADDON_SOURCES
add_executable(abaddon ${ABADDON_SOURCES})
target_include_directories(abaddon PUBLIC ${GTKMM_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${CPR_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${ZLIB_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${SQLite3_INCLUDE_DIRS})
target_include_directories(abaddon PUBLIC ${NLOHMANN_JSON_INCLUDE_DIRS})
if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND
@ -87,6 +86,5 @@ endif()
target_link_libraries(abaddon ${SQLite3_LIBRARIES})
target_link_libraries(abaddon ${GTKMM_LIBRARIES})
target_link_libraries(abaddon ${CURL_LIBRARIES})
target_link_libraries(abaddon ${CPR_LIBRARY})
target_link_libraries(abaddon ${ZLIB_LIBRARY})
target_link_libraries(abaddon ${NLOHMANN_JSON_LIBRARIES})

View File

@ -13,7 +13,7 @@
### Building:
#### Windows:
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `vcpkg install gtkmm:x64-windows nlohmann-json:x64-windows ixwebsocket:x64-windows cpr:x64-windows zlib:x64-windows simpleini:x64-windows sqlite3:x64-windows openssl:x64-windows`
2. `vcpkg install gtkmm:x64-windows nlohmann-json:x64-windows ixwebsocket:x64-windows zlib:x64-windows simpleini:x64-windows sqlite3:x64-windows openssl:x64-windows curl:x64-windows`
3. `mkdir build && cd build`
4. `cmake -G"Visual Studio 16 2019" -A x64 -DCMAKE_TOOLCHAIN_FILE=c:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=x64-windows ..`
5. Build with Visual Studio
@ -22,13 +22,13 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
#### Mac:
1. `git clone https://github.com/uowuo/abaddon && cd abaddon`
2. `brew install gtkmm3 nlohmann-json cpr`
2. `brew install gtkmm3 nlohmann-json`
3. `mkdir build && cd build`
4. `cmake ..`
5. `make`
#### Linux:
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`, [cpr](https://github.com/whoshuu/cpr), and [nlohmann-json](https://github.com/nlohmann/json)
1. Install dependencies: `libgtkmm-3.0-dev`, `libcurl4-gnutls-dev`, and [nlohmann-json](https://github.com/nlohmann/json)
2. `git clone https://github.com/uowuo/abaddon && cd abaddon`
3. `mkdir build && cd build`
4. `cmake ..`
@ -36,8 +36,8 @@ Or, do steps 1 and 2, and open CMakeLists.txt in Visual Studio if `vcpkg integra
### Downloads (from CI):
- Windows: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-windows-RelWithDebInfo.zip)
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned, unpackaged, requires gtkmm3 and [cpr](https://github.com/whoshuu/cpr/) (e.g. from homebrew)
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now), requires gtkmm3 and [cpr](https://github.com/whoshuu/cpr/). built on Ubuntu 18.04 + gcc9
- MacOS: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-macos-RelWithDebInfo.zip) unsigned, unpackaged, requires gtkmm3 (e.g. from homebrew)
- Linux: [here](https://nightly.link/uowuo/abaddon/workflows/ci/master/build-linux-MinSizeRel.zip) unpackaged (for now), requires gtkmm3. built on Ubuntu 18.04 + gcc9
Make sure you start from the directory where `css` and `res` are
@ -45,7 +45,7 @@ Make sure you start from the directory where `css` and `res` are
* [gtkmm](https://www.gtkmm.org/en/)
* [JSON for Modern C++](https://github.com/nlohmann/json)
* [IXWebSocket](https://github.com/machinezone/IXWebSocket)
* [C++ Requests: Curl for People](https://github.com/whoshuu/cpr/)
* [libcurl](https://curl.se/)
* [zlib](https://zlib.net/)
* [simpleini](https://github.com/brofield/simpleini)
* [SQLite3](https://www.sqlite.org/index.html)

View File

@ -1,17 +0,0 @@
find_path(CPR_INCLUDE_DIR
NAMES cpr/cpr.h)
find_library(CPR_LIBRARY
NAMES cpr
HINTS ${CPR_LIBRARY_ROOT})
set(CPR_LIBRARIES ${CPR_LIBRARY})
set(CPR_INCLUDE_DIRS ${CPR_INCLUDE_DIR})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(cpr
FOUND_VAR CPR_FOUND
REQUIRED_VARS
CPR_LIBRARY
CPR_INCLUDE_DIR
VERSION_VAR CPR_VERSION)

View File

@ -101,7 +101,7 @@ std::set<Snowflake> DiscordClient::GetMessagesForChannel(Snowflake id) const {
void DiscordClient::FetchInvite(std::string code, sigc::slot<void(std::optional<InviteData>)> callback) {
sigc::signal<void, std::optional<InviteData>> signal;
signal.connect(callback);
m_http.MakeGET("/invites/" + code + "?with_counts=true", [this, callback](cpr::Response r) {
m_http.MakeGET("/invites/" + code + "?with_counts=true", [this, callback](http::response_type r) {
if (!CheckCode(r)) {
if (r.status_code == 404)
callback(std::nullopt);
@ -114,7 +114,7 @@ void DiscordClient::FetchInvite(std::string code, sigc::slot<void(std::optional<
void DiscordClient::FetchMessagesInChannel(Snowflake id, std::function<void(const std::vector<Snowflake> &)> cb) {
std::string path = "/channels/" + std::to_string(id) + "/messages?limit=50";
m_http.MakeGET(path, [this, id, cb](cpr::Response r) {
m_http.MakeGET(path, [this, id, cb](const http::response_type &r) {
if (!CheckCode(r)) return;
std::vector<Message> msgs;
@ -137,7 +137,7 @@ void DiscordClient::FetchMessagesInChannel(Snowflake id, std::function<void(cons
void DiscordClient::FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, std::function<void(const std::vector<Snowflake> &)> cb) {
std::string path = "/channels/" + std::to_string(channel_id) + "/messages?limit=50&before=" + std::to_string(before_id);
m_http.MakeGET(path, [this, channel_id, cb](cpr::Response r) {
m_http.MakeGET(path, [this, channel_id, cb](http::response_type r) {
if (!CheckCode(r)) return;
std::vector<Message> msgs;
@ -453,7 +453,7 @@ void DiscordClient::SetGuildName(Snowflake id, const Glib::ustring &name, sigc::
obj.Name = name;
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const cpr::Response &r) {
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const http::response_type &r) {
const auto success = r.status_code == 200;
signal.emit(success);
});
@ -468,7 +468,7 @@ void DiscordClient::SetGuildIcon(Snowflake id, const std::string &data, sigc::sl
obj.IconData = data;
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const cpr::Response &r) {
m_http.MakePATCH("/guilds/" + std::to_string(id), nlohmann::json(obj).dump(), [this, signal](const http::response_type &r) {
const auto success = r.status_code == 200;
signal.emit(success);
});
@ -481,7 +481,7 @@ void DiscordClient::UnbanUser(Snowflake guild_id, Snowflake user_id) {
void DiscordClient::UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(bool success)> callback) {
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakeDELETE("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback](const cpr::Response &response) {
m_http.MakeDELETE("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback](const http::response_type &response) {
callback(response.status_code == 204);
});
}
@ -493,7 +493,7 @@ void DiscordClient::DeleteInvite(const std::string &code) {
void DiscordClient::DeleteInvite(const std::string &code, sigc::slot<void(bool success)> callback) {
sigc::signal<void, bool> signal;
signal.connect(callback);
m_http.MakeDELETE("/invites/" + code, [this, callback](const cpr::Response &response) {
m_http.MakeDELETE("/invites/" + code, [this, callback](const http::response_type &response) {
callback(CheckCode(response));
});
}
@ -505,7 +505,7 @@ std::vector<BanData> DiscordClient::GetBansInGuild(Snowflake guild_id) {
void DiscordClient::FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::slot<void(BanData)> callback) {
sigc::signal<void, BanData> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback, guild_id](const cpr::Response &response) {
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback, guild_id](const http::response_type &response) {
if (!CheckCode(response)) return;
auto ban = nlohmann::json::parse(response.text).get<BanData>();
m_store.SetBan(guild_id, ban.User.ID, ban);
@ -517,7 +517,7 @@ void DiscordClient::FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::s
void DiscordClient::FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vector<BanData>)> callback) {
sigc::signal<void, std::vector<BanData>> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans", [this, callback, guild_id](const cpr::Response &response) {
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans", [this, callback, guild_id](const http::response_type &response) {
if (!CheckCode(response)) return;
auto bans = nlohmann::json::parse(response.text).get<std::vector<BanData>>();
m_store.BeginTransaction();
@ -533,7 +533,7 @@ void DiscordClient::FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vect
void DiscordClient::FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::vector<InviteData>)> callback) {
sigc::signal<void, std::vector<InviteData>> signal;
signal.connect(callback);
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/invites", [this, callback, guild_id](const cpr::Response &response) {
m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/invites", [this, callback, guild_id](const http::response_type &response) {
// store?
if (!CheckCode(response)) return;
auto invites = nlohmann::json::parse(response.text).get<std::vector<InviteData>>();
@ -1232,9 +1232,9 @@ void DiscordClient::HandleSocketOpen() {
void DiscordClient::HandleSocketClose(uint16_t code) {
}
bool DiscordClient::CheckCode(const cpr::Response &r) {
bool DiscordClient::CheckCode(const http::response_type &r) {
if (r.status_code >= 300 || r.error) {
fprintf(stderr, "api request to %s failed with status code %d\n", r.url.c_str(), r.status_code);
fprintf(stderr, "api request to %s failed with status code %d: %s\n", r.url.c_str(), r.status_code, r.error_string.c_str());
return false;
}

View File

@ -1,6 +1,6 @@
#pragma once
#include "websocket.hpp"
#include "http.hpp"
#include "httpclient.hpp"
#include "objects.hpp"
#include "store.hpp"
#include <sigc++/sigc++.h>
@ -175,7 +175,7 @@ private:
void HandleSocketOpen();
void HandleSocketClose(uint16_t code);
bool CheckCode(const cpr::Response &r);
bool CheckCode(const http::response_type &r);
void StoreMessageData(Message &msg);

View File

@ -1,156 +0,0 @@
#include "http.hpp"
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient(std::string api_base)
: m_api_base(api_base) {
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
}
void HTTPClient::SetUserAgent(std::string agent) {
m_agent = agent;
}
void HTTPClient::SetAuth(std::string auth) {
m_authorization = auth;
}
void HTTPClient::MakeDELETE(std::string path, std::function<void(cpr::Response r)> cb) {
printf("DELETE %s\n", path.c_str());
auto url = cpr::Url { m_api_base + path };
auto headers = cpr::Header {
{ "Authorization", m_authorization },
};
auto ua = cpr::UserAgent {m_agent != "" ? m_agent : "Abaddon" };
#ifdef USE_LOCAL_PROXY
m_futures.push_back(cpr::DeleteCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, ua,
cpr::Proxies { { "http", "127.0.0.1:8888" }, { "https", "127.0.0.1:8888" } },
cpr::VerifySsl { false }));
#else
m_futures.push_back(cpr::DeleteCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, ua));
#endif
}
void HTTPClient::MakePATCH(std::string path, std::string payload, std::function<void(cpr::Response r)> cb) {
printf("PATCH %s\n", path.c_str());
auto url = cpr::Url { m_api_base + path };
auto headers = cpr::Header {
{ "Authorization", m_authorization },
{ "Content-Type", "application/json" },
};
auto ua = cpr::UserAgent { m_agent != "" ? m_agent : "Abaddon" };
auto body = cpr::Body { payload };
#ifdef USE_LOCAL_PROXY
m_futures.push_back(cpr::PatchCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, body, ua,
cpr::Proxies { { "http", "127.0.0.1:8888" }, { "https", "127.0.0.1:8888" } },
cpr::VerifySsl { false }));
#else
m_futures.push_back(cpr::PatchCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, body, ua));
#endif
}
void HTTPClient::MakePOST(std::string path, std::string payload, std::function<void(cpr::Response r)> cb) {
printf("POST %s\n", path.c_str());
auto url = cpr::Url { m_api_base + path };
auto headers = cpr::Header {
{ "Authorization", m_authorization },
{ "Content-Type", "application/json" },
};
auto ua = cpr::UserAgent { m_agent != "" ? m_agent : "Abaddon" };
auto body = cpr::Body { payload };
#ifdef USE_LOCAL_PROXY
m_futures.push_back(cpr::PostCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, body, ua,
cpr::Proxies { { "http", "127.0.0.1:8888" }, { "https", "127.0.0.1:8888" } },
cpr::VerifySsl { false }));
#else
m_futures.push_back(cpr::PostCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, body, ua));
#endif
}
void HTTPClient::MakePUT(std::string path, std::string payload, std::function<void(cpr::Response r)> cb) {
printf("PUT %s\n", path.c_str());
auto url = cpr::Url { m_api_base + path };
auto headers = cpr::Header {
{ "Authorization", m_authorization },
{ "Content-Type", "application/json" },
};
auto ua = cpr::UserAgent { m_agent != "" ? m_agent : "Abaddon" };
auto body = cpr::Body { payload };
#ifdef USE_LOCAL_PROXY
m_futures.push_back(cpr::PutCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, body, ua,
cpr::Proxies { { "http", "127.0.0.1:8888" }, { "https", "127.0.0.1:8888" } },
cpr::VerifySsl { false }));
#else
m_futures.push_back(cpr::PutCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, body, ua));
#endif
}
void HTTPClient::MakeGET(std::string path, std::function<void(cpr::Response r)> cb) {
printf("GET %s\n", path.c_str());
auto url = cpr::Url { m_api_base + path };
auto headers = cpr::Header {
{ "Authorization", m_authorization },
{ "Content-Type", "application/json" },
};
auto ua = cpr::UserAgent { m_agent != "" ? m_agent : "Abaddon" };
auto x = cpr::UserAgent {};
#ifdef USE_LOCAL_PROXY
m_futures.push_back(cpr::GetCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, ua,
cpr::Proxies { { "http", "127.0.0.1:8888" }, { "https", "127.0.0.1:8888" } },
cpr::VerifySsl { false }));
#else
m_futures.push_back(cpr::GetCallback(
std::bind(&HTTPClient::OnResponse, this, std::placeholders::_1, cb),
url, headers, ua));
#endif
}
void HTTPClient::CleanupFutures() {
for (auto it = m_futures.begin(); it != m_futures.end();) {
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
it = m_futures.erase(it);
else
it++;
}
}
void HTTPClient::RunCallbacks() {
m_mutex.lock();
m_queue.front()();
m_queue.pop();
m_mutex.unlock();
}
void HTTPClient::OnResponse(cpr::Response r, std::function<void(cpr::Response r)> cb) {
CleanupFutures();
try {
m_mutex.lock();
m_queue.push([this, r, cb] { cb(r); });
m_dispatcher.emit();
m_mutex.unlock();
} catch (std::exception &e) {
fprintf(stderr, "error handling response (%s, code %d): %s\n", r.url.c_str(), r.status_code, e.what());
}
}

View File

@ -1,37 +0,0 @@
#pragma once
#include <cpr/cpr.h>
#include <functional>
#include <future>
#include <string>
#include <unordered_map>
#include <memory>
#include <mutex>
#include <queue>
#include <glibmm.h>
class HTTPClient {
public:
HTTPClient(std::string api_base);
void SetUserAgent(std::string agent);
void SetAuth(std::string auth);
void MakeDELETE(std::string path, std::function<void(cpr::Response r)> cb);
void MakeGET(std::string path, std::function<void(cpr::Response r)> cb);
void MakePATCH(std::string path, std::string payload, std::function<void(cpr::Response r)> cb);
void MakePOST(std::string path, std::string payload, std::function<void(cpr::Response r)> cb);
void MakePUT(std::string path, std::string payload, std::function<void(cpr::Response r)> cb);
private:
void OnResponse(cpr::Response r, std::function<void(cpr::Response r)> cb);
void CleanupFutures();
mutable std::mutex m_mutex;
Glib::Dispatcher m_dispatcher;
std::queue<std::function<void()>> m_queue;
void RunCallbacks();
std::vector<std::future<void>> m_futures;
std::string m_api_base;
std::string m_authorization;
std::string m_agent;
};

136
discord/httpclient.cpp Normal file
View File

@ -0,0 +1,136 @@
#include "httpclient.hpp"
#include "httpclient.hpp"
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient(std::string api_base)
: m_api_base(api_base) {
m_dispatcher.connect(sigc::mem_fun(*this, &HTTPClient::RunCallbacks));
}
void HTTPClient::SetUserAgent(std::string agent) {
m_agent = agent;
}
void HTTPClient::SetAuth(std::string auth) {
m_authorization = auth;
}
void HTTPClient::MakeDELETE(const std::string &path, std::function<void(http::response_type r)> cb) {
printf("DELETE %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_DELETE, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute();
OnResponse(res, cb);
}));
}
void HTTPClient::MakePATCH(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
printf("PATCH %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PATCH, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute();
OnResponse(res, cb);
}));
}
void HTTPClient::MakePOST(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
printf("POST %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_POST, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute();
OnResponse(res, cb);
}));
}
void HTTPClient::MakePUT(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb) {
printf("PUT %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb, payload] {
http::request req(http::REQUEST_PUT, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
req.set_body(payload);
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute();
OnResponse(res, cb);
}));
}
void HTTPClient::MakeGET(const std::string &path, std::function<void(http::response_type r)> cb) {
printf("GET %s\n", path.c_str());
m_futures.push_back(std::async(std::launch::async, [this, path, cb] {
http::request req(http::REQUEST_GET, m_api_base + path);
req.set_header("Authorization", m_authorization);
req.set_header("Content-Type", "application/json");
req.set_user_agent(m_agent != "" ? m_agent : "Abaddon");
#ifdef USE_LOCAL_PROXY
req.set_proxy("http://127.0.0.1:8888");
req.set_verify_ssl(false);
#endif
auto res = req.execute();
OnResponse(res, cb);
}));
}
void HTTPClient::CleanupFutures() {
for (auto it = m_futures.begin(); it != m_futures.end();) {
if (it->wait_for(std::chrono::seconds(0)) == std::future_status::ready)
it = m_futures.erase(it);
else
it++;
}
}
void HTTPClient::RunCallbacks() {
m_mutex.lock();
m_queue.front()();
m_queue.pop();
m_mutex.unlock();
}
void HTTPClient::OnResponse(const http::response_type &r, std::function<void(http::response_type r)> cb) {
CleanupFutures();
try {
m_mutex.lock();
m_queue.push([this, r, cb] { cb(r); });
m_dispatcher.emit();
m_mutex.unlock();
} catch (const std::exception &e) {
fprintf(stderr, "error handling response (%s, code %d): %s\n", r.url.c_str(), r.status_code, e.what());
}
}

37
discord/httpclient.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <functional>
#include <future>
#include <string>
#include <unordered_map>
#include <memory>
#include <mutex>
#include <queue>
#include <glibmm.h>
#include "../http.hpp"
class HTTPClient {
public:
HTTPClient(std::string api_base);
void SetUserAgent(std::string agent);
void SetAuth(std::string auth);
void MakeDELETE(const std::string &path, std::function<void(http::response_type r)> cb);
void MakeGET(const std::string &path, std::function<void(http::response_type r)> cb);
void MakePATCH(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
void MakePOST(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
void MakePUT(const std::string &path, const std::string &payload, std::function<void(http::response_type r)> cb);
private:
void OnResponse(const http::response_type &r, std::function<void(http::response_type r)> cb);
void CleanupFutures();
mutable std::mutex m_mutex;
Glib::Dispatcher m_dispatcher;
std::queue<std::function<void()>> m_queue;
void RunCallbacks();
std::vector<std::future<void>> m_futures;
std::string m_api_base;
std::string m_authorization;
std::string m_agent;
};

View File

@ -51,10 +51,10 @@ void Cache::GetFileFromURL(std::string url, callback_type cb) {
if (m_canceled) return;
m_semaphore->wait();
if (m_canceled) return;
const auto &r = cpr::Get(cpr::Url { url });
http::request req(http::REQUEST_GET, url);
m_semaphore->notify();
if (m_canceled) return;
OnResponse(r);
OnResponse(req.execute());
});
m_futures.push_back(std::move(future));
}
@ -79,7 +79,7 @@ void Cache::CleanupFutures() {
}
}
void Cache::OnResponse(const cpr::Response &r) {
void Cache::OnResponse(const http::response_type &r) {
CleanupFutures(); // see above comment
if (r.error || r.status_code > 300) return;

View File

@ -1,13 +1,12 @@
#pragma once
#include <cpr/cpr.h>
#include <functional>
#include <string>
#include <filesystem>
#include <vector>
#include <unordered_map>
#include <future>
#include "util.hpp"
// todo throttle requests and keep track of active requests to stop redundant requests
#include "http.hpp"
class Cache {
public:
@ -23,7 +22,7 @@ private:
std::string GetCachedName(std::string str);
void CleanupFutures();
void RespondFromPath(std::filesystem::path path, callback_type cb);
void OnResponse(const cpr::Response &r);
void OnResponse(const http::response_type &r);
std::unique_ptr<Semaphore> m_semaphore;

121
http.cpp Normal file
View File

@ -0,0 +1,121 @@
#include "http.hpp"
namespace http {
request::request(EMethod method, const std::string &url)
: m_url(url) {
switch (method) {
case REQUEST_GET:
m_method = "GET";
break;
case REQUEST_POST:
m_method = "POST";
break;
case REQUEST_PATCH:
m_method = "PATCH";
break;
case REQUEST_PUT:
m_method = "PUT";
break;
case REQUEST_DELETE:
m_method = "DELETE";
break;
default:
m_method = "GET";
break;
}
prepare();
}
request::~request() {
if (m_curl != nullptr)
curl_easy_cleanup(m_curl);
if (m_header_list != nullptr)
curl_slist_free_all(m_header_list);
}
void request::set_verify_ssl(bool verify) {
curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, verify ? 1L : 0L);
}
void request::set_proxy(const std::string &proxy) {
curl_easy_setopt(m_curl, CURLOPT_PROXY, proxy.c_str());
}
void request::set_header(const std::string &name, const std::string &value) {
m_header_list = curl_slist_append(m_header_list, (name + ": " + value).c_str());
}
void request::set_body(const std::string &data) {
curl_easy_setopt(m_curl, CURLOPT_COPYPOSTFIELDS, data.c_str());
}
void request::set_user_agent(const std::string &data) {
curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str());
}
response request::execute() {
if (m_curl == nullptr) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit);
response.error_string = "curl pointer is null";
}
detail::check_init();
std::string str;
curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, m_method);
curl_easy_setopt(m_curl, CURLOPT_URL, m_url.c_str());
curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1);
curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, detail::curl_write_data_callback);
curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &str);
curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_error_buf);
if (m_header_list != nullptr)
curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list);
CURLcode result = curl_easy_perform(m_curl);
if (result != CURLE_OK) {
auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLPerform);
response.error_string = curl_easy_strerror(result);
response.error_string += " " + std::string(m_error_buf);
return response;
}
int response_code = 0;
curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &response_code);
auto response = detail::make_response(m_url, response_code);
response.text = str;
return response;
}
void request::prepare() {
m_curl = curl_easy_init();
}
namespace detail {
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, std::string *outstr) {
auto new_length = size * nmemb;
outstr->append(reinterpret_cast<char *>(ptr), new_length);
return new_length;
}
response make_response(const std::string &url, int code) {
response r;
r.url = url;
r.status_code = static_cast<EStatusCode>(code);
if (code < http::EStatusCode::ClientErrorMax)
r.error = true;
return r;
}
void check_init() {
static bool initialized = false;
if (!initialized) {
curl_global_init(CURL_GLOBAL_ALL);
initialized = true;
}
}
} // namespace detail
} // namespace http

129
http.hpp Normal file
View File

@ -0,0 +1,129 @@
#pragma once
#include <string>
#include <curl/curl.h>
// i regret not using snake case for everything oh well
namespace http {
enum EStatusCode : int {
Continue = 100,
SwitchingProtocols = 101,
Processing = 102,
EarlyHints = 103,
OK = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
MultiStatus = 207,
AlreadyReported = 208,
IMUsed = 226,
MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
SwitchProxy = 306,
TemporaryRedirect = 307,
PermanentRedirect = 308,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthorizationRequired = 407,
RequestTimeout = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
PayloadTooLarge = 413,
URITooLong = 414,
UnsupportedMediaType = 415,
RangeNotSatisfiable = 416,
ExpectationFailed = 417,
ImATeapot = 418,
MisdirectedRequest = 421,
UnprocessableEntity = 422,
Locked = 423,
FailedDependency = 424,
TooEarly = 425,
UpgradeRequired = 426,
PreconditionRequired = 428,
TooManyRequests = 429,
RequestHeaderFieldsTooLarge = 431,
UnavailableForLegalReasons = 451,
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HTTPVersionNotSupported = 505,
VariantAlsoNegotiates = 506,
InsufficientStorage = 507,
LoopDetected = 508,
NotExtended = 510,
NetworkAuthenticationRequired = 511,
ClientError = 1,
ClientErrorCURLInit,
ClientErrorCURLPerform,
ClientErrorMax = 99,
};
enum EMethod {
REQUEST_GET,
REQUEST_POST,
REQUEST_PATCH,
REQUEST_PUT,
REQUEST_DELETE,
};
struct response {
EStatusCode status_code;
std::string text;
std::string url;
bool error = false;
std::string error_string;
};
struct request {
request(EMethod method, const std::string &url);
~request();
void set_verify_ssl(bool verify);
void set_proxy(const std::string &proxy);
void set_header(const std::string &name, const std::string &value);
void set_body(const std::string &data);
void set_user_agent(const std::string &data);
response execute();
private:
void prepare();
CURL *m_curl;
std::string m_url;
const char *m_method;
curl_slist *m_header_list = nullptr;
char m_error_buf[CURL_ERROR_SIZE] = { 0 };
};
using response_type = response;
namespace detail {
size_t curl_write_data_callback(void *ptr, size_t size, size_t nmemb, std::string *outstr);
response make_response(const std::string &url, int code);
void check_init();
} // namespace detail
} // namespace http