Skip to content

Commit

Permalink
fix, feat: Add MIME type to HTTP file transfer API (#643)
Browse files Browse the repository at this point in the history
  • Loading branch information
braindigitalis authored Apr 2, 2023
2 parents 1aee2b5 + 4ddadd5 commit 58cde9a
Show file tree
Hide file tree
Showing 15 changed files with 66 additions and 33 deletions.
6 changes: 4 additions & 2 deletions include/dpp/cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -1169,8 +1169,9 @@ class DPP_EXPORT cluster {
* @param callback Function to call when the HTTP call completes. The callback parameter will contain amongst other things, the decoded json.
* @param filename Filename to post for POST requests (for uploading files)
* @param filecontent File content to post for POST requests (for uploading files)
* @param filemimetype File content to post for POST requests (for uploading files)
*/
void post_rest(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::string &filename = "", const std::string &filecontent = "");
void post_rest(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::string &filename = "", const std::string &filecontent = "", const std::string &filemimetype = "");

/**
* @brief Post a multipart REST request. Where possible use a helper method instead like message_create
Expand All @@ -1183,8 +1184,9 @@ class DPP_EXPORT cluster {
* @param callback Function to call when the HTTP call completes. The callback parameter will contain amongst other things, the decoded json.
* @param filename List of filenames to post for POST requests (for uploading files)
* @param filecontent List of file content to post for POST requests (for uploading files)
* @param filemimetypes List of mime types for each file to post for POST requests (for uploading files)
*/
void post_rest_multipart(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::vector<std::string> &filename = {}, const std::vector<std::string> &filecontent = {});
void post_rest_multipart(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::vector<std::string> &filename = {}, const std::vector<std::string>& filecontent = {}, const std::vector<std::string>& filemimetypes = {});

/**
* @brief Make a HTTP(S) request. For use when wanting asynchronous access to HTTP APIs outside of Discord.
Expand Down
3 changes: 2 additions & 1 deletion include/dpp/httpsclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,10 @@ class DPP_EXPORT https_client : public ssl_client
* @param json The json content
* @param filenames File names of files to send
* @param contents Contents of each of the files to send
* @param contents MIME types of each of the files to send
* @return multipart mime content and headers
*/
static multipart_content build_multipart(const std::string &json, const std::vector<std::string>& filenames = {}, const std::vector<std::string>& contents = {});
static multipart_content build_multipart(const std::string &json, const std::vector<std::string>& filenames = {}, const std::vector<std::string>& contents = {}, const std::vector<std::string>& mimetypes = {});

/**
* @brief Processes incoming data from the SSL socket input buffer.
Expand Down
6 changes: 5 additions & 1 deletion include/dpp/message.h
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,9 @@ struct DPP_EXPORT message : public managed {
/** File content to upload (raw binary) */
std::vector<std::string> filecontent;

/** Mime type of files to upload */
std::vector<std::string> filemimetype;

/**
* @brief Reference to another message, e.g. a reply
*/
Expand Down Expand Up @@ -1466,9 +1469,10 @@ struct DPP_EXPORT message : public managed {
*
* @param filename filename
* @param filecontent raw file content contained in std::string
* @param filemimetype optional mime type of the file
* @return message& reference to self
*/
message& add_file(const std::string &filename, const std::string &filecontent);
message& add_file(const std::string &filename, const std::string &filecontent, const std::string &filemimetype = "");

/**
* @brief Set the message content
Expand Down
8 changes: 6 additions & 2 deletions include/dpp/queues.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ class DPP_EXPORT http_request {
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) */
Expand All @@ -166,8 +168,9 @@ class DPP_EXPORT http_request {
* @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
*/
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 = "");
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 = "");

/**
* @brief Constructor. When constructing one of these objects it should be passed to request_queue::post_request().
Expand All @@ -179,8 +182,9 @@ class DPP_EXPORT http_request {
* @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
*/
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 = {});
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 = {});

/**
* @brief Constructor. When constructing one of these objects it should be passed to request_queue::post_request().
Expand Down
8 changes: 4 additions & 4 deletions src/dpp/cluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ json error_response(const std::string& message, http_request_completion_t& rv)
return j;
}

void cluster::post_rest(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::string &filename, const std::string &filecontent) {
void cluster::post_rest(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::string &filename, const std::string &filecontent, const std::string &filemimetype) {
/* NOTE: This is not a memory leak! The request_queue will free the http_request once it reaches the end of its lifecycle */
rest->post_request(new http_request(endpoint + "/" + major_parameters, parameters, [endpoint, callback](http_request_completion_t rv) {
json j;
Expand All @@ -310,10 +310,10 @@ void cluster::post_rest(const std::string &endpoint, const std::string &major_pa
if (callback) {
callback(j, rv);
}
}, postdata, method, get_audit_reason(), filename, filecontent));
}, postdata, method, get_audit_reason(), filename, filecontent, filemimetype));
}

void cluster::post_rest_multipart(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::vector<std::string> &filename, const std::vector<std::string> &filecontent) {
void cluster::post_rest_multipart(const std::string &endpoint, const std::string &major_parameters, const std::string &parameters, http_method method, const std::string &postdata, json_encode_t callback, const std::vector<std::string> &filename, const std::vector<std::string> &filecontent, const std::vector<std::string> &filemimetypes) {
/* NOTE: This is not a memory leak! The request_queue will free the http_request once it reaches the end of its lifecycle */
rest->post_request(new http_request(endpoint + "/" + major_parameters, parameters, [endpoint, callback](http_request_completion_t rv) {
json j;
Expand All @@ -328,7 +328,7 @@ void cluster::post_rest_multipart(const std::string &endpoint, const std::string
if (callback) {
callback(j, rv);
}
}, postdata, method, get_audit_reason(), filename, filecontent));
}, postdata, method, get_audit_reason(), filename, filecontent, filemimetypes));
}


Expand Down
10 changes: 5 additions & 5 deletions src/dpp/cluster/appcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,31 +132,31 @@ void cluster::interaction_response_create(snowflake interaction_id, const std::s
if (callback) {
callback(confirmation_callback_t(this, confirmation(), http));
}
}, r.msg->filename, r.msg->filecontent);
}, r.msg->filename, r.msg->filecontent, r.msg->filemimetype);
}

void cluster::interaction_response_edit(const std::string &token, const message &m, command_completion_event_t callback) {
this->post_rest_multipart(API_PATH "/webhooks", std::to_string(me.id), utility::url_encode(token) + "/messages/@original", m_patch, m.build_json(), [this, callback](json &j, const http_request_completion_t& http) {
if (callback) {
callback(confirmation_callback_t(this, confirmation(), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}

void cluster::interaction_followup_create(const std::string &token, const message &m, command_completion_event_t callback) {
this->post_rest_multipart(API_PATH "/webhooks", std::to_string(me.id), utility::url_encode(token), m_post, m.build_json(), [this, callback](json &j, const http_request_completion_t& http) {
if (callback) {
callback(confirmation_callback_t(this, confirmation(), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}

void cluster::interaction_followup_edit_original(const std::string &token, const message &m, command_completion_event_t callback) {
this->post_rest_multipart(API_PATH "/webhooks", std::to_string(me.id), utility::url_encode(token) + "/messages/@original", m_patch, m.build_json(), [this, callback](json &j, const http_request_completion_t& http) {
if (callback) {
callback(confirmation_callback_t(this, confirmation(), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}

void cluster::interaction_followup_delete(const std::string &token, command_completion_event_t callback) {
Expand All @@ -168,7 +168,7 @@ void cluster::interaction_followup_edit(const std::string &token, const message
if (callback) {
callback(confirmation_callback_t(this, confirmation(), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}

void cluster::interaction_followup_get(const std::string &token, snowflake message_id, command_completion_event_t callback) {
Expand Down
4 changes: 2 additions & 2 deletions src/dpp/cluster/message.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ void cluster::message_create(const message &m, command_completion_event_t callba
if (callback) {
callback(confirmation_callback_t(this, message(this).fill_from_json(&j), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}


Expand Down Expand Up @@ -116,7 +116,7 @@ void cluster::message_edit(const message &m, command_completion_event_t callback
if (callback) {
callback(confirmation_callback_t(this, message(this).fill_from_json(&j), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}


Expand Down
15 changes: 14 additions & 1 deletion src/dpp/cluster/sticker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,25 @@

namespace dpp {

namespace {
std::string get_sticker_mimetype(const sticker &s) {
static const std::map<sticker_format, std::string> mime_types = {
{ sticker_format::sf_png, "image/png" },
{ sticker_format::sf_apng, "image/png" },
{ sticker_format::sf_lottie, "application/json" },
{ sticker_format::sf_gif, "image/gif" },
};

return mime_types.find(s.format_type)->second;
}
}

void cluster::guild_sticker_create(sticker &s, command_completion_event_t callback) {
this->post_rest(API_PATH "/guilds", std::to_string(s.guild_id), "stickers", m_post, s.build_json(false), [this, callback](json &j, const http_request_completion_t& http) {
if (callback) {
callback(confirmation_callback_t(this, sticker().fill_from_json(&j), http));
}
}, s.filename, s.filecontent);
}, s.filename, s.filecontent, get_sticker_mimetype(s));
}

void cluster::guild_sticker_delete(snowflake sticker_id, snowflake guild_id, command_completion_event_t callback) {
Expand Down
2 changes: 1 addition & 1 deletion src/dpp/cluster/thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ void cluster::thread_create_in_forum(const std::string& thread_name, snowflake c
}
callback(confirmation_callback_t(this, t, http));
}
}, msg.filename, msg.filecontent);
}, msg.filename, msg.filecontent, msg.filemimetype);
}

void cluster::thread_create(const std::string& thread_name, snowflake channel_id, uint16_t auto_archive_duration, channel_type thread_type, bool invitable, uint16_t rate_limit_per_user, command_completion_event_t callback)
Expand Down
4 changes: 2 additions & 2 deletions src/dpp/cluster/webhook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void cluster::edit_webhook_message(const class webhook &wh, const struct message
if (callback) {
callback(confirmation_callback_t(this, message(this).fill_from_json(&j), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}


Expand Down Expand Up @@ -95,7 +95,7 @@ void cluster::execute_webhook(const class webhook &wh, const struct message& m,
if (callback) {
callback(confirmation_callback_t(this, message(this).fill_from_json(&j), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}


Expand Down
2 changes: 1 addition & 1 deletion src/dpp/dispatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ void interaction_create_t::edit_original_response(const message & m, command_com
if (callback) {
callback(confirmation_callback_t(creator, message().fill_from_json(&j), http));
}
}, m.filename, m.filecontent);
}, m.filename, m.filecontent, m.filemimetype);
}

void interaction_create_t::delete_original_response(command_completion_event_t callback) const
Expand Down
12 changes: 8 additions & 4 deletions src/dpp/httpsclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ void https_client::connect()
}
}

multipart_content https_client::build_multipart(const std::string &json, const std::vector<std::string>& filenames, const std::vector<std::string>& contents) {
multipart_content https_client::build_multipart(const std::string &json, const std::vector<std::string>& filenames, const std::vector<std::string>& contents, const std::vector<std::string>& mimetypes) {
if (filenames.empty() && contents.empty()) {
if (!json.empty()) {
return { json, "application/json" };
Expand All @@ -84,20 +84,24 @@ multipart_content https_client::build_multipart(const std::string &json, const s
time_t dummy2 = time(nullptr) * time(nullptr);
const std::string two_cr("\r\n\r\n");
const std::string boundary("-------------" + to_hex(dummy1) + to_hex(dummy2));
const std::string mime_part_start("--" + boundary + "\r\nContent-Type: application/octet-stream\r\nContent-Disposition: form-data; ");
const std::string part_start("--" + boundary + "\r\nContent-Disposition: form-data; ");
const std::string mime_type_start("\r\nContent-Type: ");
const std::string default_mime_type("application/octet-stream");

std::string content("--" + boundary);

/* Special case, single file */
content += "\r\nContent-Type: application/json\r\nContent-Disposition: form-data; name=\"payload_json\"" + two_cr;
content += json + "\r\n";
if (filenames.size() == 1 && contents.size() == 1) {
content += mime_part_start + "name=\"file\"; filename=\"" + filenames[0] + "\"" + two_cr;
content += part_start + "name=\"file\"; filename=\"" + filenames[0] + "\"";
content += mime_type_start + (mimetypes.empty() || mimetypes[0].empty() ? default_mime_type : mimetypes[0]) + two_cr;
content += contents[0];
} else {
/* Multiple files */
for (size_t i = 0; i < filenames.size(); ++i) {
content += mime_part_start + "name=\"files[" + std::to_string(i) + "]\"; filename=\"" + filenames[i] + "\"" + two_cr;
content += part_start + "name=\"files[" + std::to_string(i) + "]\"; filename=\"" + filenames[i] + "\"";
content += "\r\nContent-Type: " + (mimetypes.size() < i || mimetypes[i].empty() ? default_mime_type : mimetypes[i]) + two_cr;
content += contents[i];
content += "\r\n";
}
Expand Down
5 changes: 3 additions & 2 deletions src/dpp/message.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,10 @@ message& message::set_file_content(const std::string &fc)
return *this;
}

message& message::add_file(const std::string &fn, const std::string &fc) {
filecontent.push_back(fc);
message& message::add_file(const std::string &fn, const std::string &fc, const std::string &fm) {
filename.push_back(fn);
filecontent.push_back(fc);
filemimetype.push_back(fm);
return *this;
}

Expand Down
12 changes: 8 additions & 4 deletions src/dpp/queues.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,22 @@ namespace dpp {
static std::string http_version = "DiscordBot (https://github.com/brainboxdotcc/DPP, " + std::to_string(DPP_VERSION_MAJOR) + "." + std::to_string(DPP_VERSION_MINOR) + "." + std::to_string(DPP_VERSION_PATCH) + ")";
static const char* DISCORD_HOST = "https://discord.com";

http_request::http_request(const std::string &_endpoint, const std::string &_parameters, http_completion_event completion, const std::string &_postdata, http_method _method, const std::string &audit_reason, const std::string &filename, const std::string &filecontent)
http_request::http_request(const std::string &_endpoint, const std::string &_parameters, http_completion_event completion, const std::string &_postdata, http_method _method, const std::string &audit_reason, const std::string &filename, const std::string &filecontent, const std::string &filemimetype)
: complete_handler(completion), completed(false), non_discord(false), endpoint(_endpoint), parameters(_parameters), postdata(_postdata), method(_method), reason(audit_reason), mimetype("application/json"), waiting(false)
{
if (!filename.empty())
file_name.push_back(filename);
if (!filecontent.empty())
file_content.push_back(filecontent);
if (!filemimetype.empty())
file_mimetypes.push_back(filemimetype);
}

http_request::http_request(const std::string &_endpoint, const std::string &_parameters, http_completion_event completion, const std::string &_postdata, http_method method, const std::string &audit_reason, const std::vector<std::string> &filename, const std::vector<std::string> &filecontent)
: complete_handler(completion), completed(false), non_discord(false), endpoint(_endpoint), parameters(_parameters), postdata(_postdata), method(method), reason(audit_reason), file_name(filename), file_content(filecontent), mimetype("application/json"), waiting(false)
http_request::http_request(const std::string &_endpoint, const std::string &_parameters, http_completion_event completion, const std::string &_postdata, http_method method, const std::string &audit_reason, const std::vector<std::string> &filename, const std::vector<std::string> &filecontent, const std::vector<std::string> &filemimetypes)
: complete_handler(completion), completed(false), non_discord(false), endpoint(_endpoint), parameters(_parameters), postdata(_postdata), method(method), reason(audit_reason), file_name(filename), file_content(filecontent), file_mimetypes(filemimetypes), mimetype("application/json"), waiting(false)
{
if (filecontent.size() && !filemimetypes.size())
std::cout << "hi" << std::endl;
}


Expand Down Expand Up @@ -167,7 +171,7 @@ http_request_completion_t http_request::run(cluster* owner) {
multipart = { postdata, "" };
} else {

multipart = https_client::build_multipart(postdata, file_name, file_content);
multipart = https_client::build_multipart(postdata, file_name, file_content, file_mimetypes);
if (!multipart.mimetype.empty()) {
headers.emplace("Content-Type", multipart.mimetype);
}
Expand Down
Loading

0 comments on commit 58cde9a

Please # to comment.