diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/abaddon.cpp | 6 | ||||
-rw-r--r-- | src/abaddon.hpp | 2 | ||||
-rw-r--r-- | src/components/chatinput.cpp | 204 | ||||
-rw-r--r-- | src/components/chatinput.hpp | 89 | ||||
-rw-r--r-- | src/components/chatlist.cpp | 4 | ||||
-rw-r--r-- | src/components/chatlist.hpp | 3 | ||||
-rw-r--r-- | src/components/chatwindow.cpp | 11 | ||||
-rw-r--r-- | src/components/chatwindow.hpp | 4 | ||||
-rw-r--r-- | src/constants.hpp | 1 | ||||
-rw-r--r-- | src/discord/discord.cpp | 55 | ||||
-rw-r--r-- | src/discord/discord.hpp | 7 | ||||
-rw-r--r-- | src/discord/httpclient.cpp | 19 | ||||
-rw-r--r-- | src/discord/httpclient.hpp | 3 | ||||
-rw-r--r-- | src/http.cpp | 45 | ||||
-rw-r--r-- | src/http.hpp | 15 |
15 files changed, 416 insertions, 52 deletions
diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 2e8ecaa..05e1942 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -743,7 +743,7 @@ void Abaddon::ActionChatLoadHistory(Snowflake id) { }); } -void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message) { +void Abaddon::ActionChatInputSubmit(std::string msg, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message) { if (msg.substr(0, 7) == "/shrug " || msg == "/shrug") msg = msg.substr(6) + "\xC2\xAF\x5C\x5F\x28\xE3\x83\x84\x29\x5F\x2F\xC2\xAF"; // this is important @@ -751,9 +751,9 @@ void Abaddon::ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflak if (!m_discord.HasChannelPermission(m_discord.GetUserData().ID, channel, Permission::VIEW_CHANNEL)) return; if (referenced_message.IsValid()) - m_discord.SendChatMessage(msg, channel, referenced_message); + m_discord.SendChatMessage(msg, attachment_paths, channel, referenced_message); else - m_discord.SendChatMessage(msg, channel); + m_discord.SendChatMessage(msg, attachment_paths, channel); } void Abaddon::ActionChatEditMessage(Snowflake channel_id, Snowflake id) { diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 3296c45..9efb891 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -36,7 +36,7 @@ public: void ActionSetToken(); void ActionJoinGuildDialog(); void ActionChannelOpened(Snowflake id, bool expand_to = true); - void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message); + void ActionChatInputSubmit(std::string msg, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message); void ActionChatLoadHistory(Snowflake id); void ActionChatEditMessage(Snowflake channel_id, Snowflake id); void ActionInsertMention(Snowflake id); diff --git a/src/components/chatinput.cpp b/src/components/chatinput.cpp index c3eca32..6afdd82 100644 --- a/src/components/chatinput.cpp +++ b/src/components/chatinput.cpp @@ -1,6 +1,10 @@ #include "chatinput.hpp" +#include "abaddon.hpp" +#include "constants.hpp" +#include <filesystem> +#include <utility> -ChatInput::ChatInput() { +ChatInputText::ChatInputText() { get_style_context()->add_class("message-input"); set_propagate_natural_height(true); set_min_content_height(20); @@ -20,22 +24,26 @@ ChatInput::ChatInput() { add(m_textview); } -void ChatInput::InsertText(const Glib::ustring &text) { +void ChatInputText::InsertText(const Glib::ustring &text) { GetBuffer()->insert_at_cursor(text); m_textview.grab_focus(); } -Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() { +Glib::RefPtr<Gtk::TextBuffer> ChatInputText::GetBuffer() { return m_textview.get_buffer(); } // this isnt connected directly so that the chat window can handle stuff like the completer first -bool ChatInput::ProcessKeyPress(GdkEventKey *event) { +bool ChatInputText::ProcessKeyPress(GdkEventKey *event) { if (event->keyval == GDK_KEY_Escape) { m_signal_escape.emit(); return true; } + if ((event->state & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_v) { + return CheckHandleClipboardPaste(); + } + if (event->keyval == GDK_KEY_Return) { if (event->state & GDK_SHIFT_MASK) return false; @@ -53,10 +61,196 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) { return false; } -void ChatInput::on_grab_focus() { +void ChatInputText::on_grab_focus() { m_textview.grab_focus(); } +bool ChatInputText::CheckHandleClipboardPaste() { + auto clip = Gtk::Clipboard::get(); + if (!clip->wait_is_image_available()) return false; + + const auto pb = clip->wait_for_image(); + std::array<char, L_tmpnam> dest_name {}; + if (std::tmpnam(dest_name.data()) == nullptr) { + fprintf(stderr, "failed to get temporary path\n"); + return true; + } + + // stinky + std::filesystem::path part1(std::filesystem::temp_directory_path() / "abaddon-cache"); + std::filesystem::path part2(dest_name.data()); + const auto dest_path = (part1 / part2.relative_path()).string(); + + try { + pb->save(dest_path, "png"); + } catch (...) { + fprintf(stderr, "pasted image save error\n"); + return true; + } + + m_signal_image_paste.emit(pb, dest_path); + + return true; +} + +ChatInputText::type_signal_submit ChatInputText::signal_submit() { + return m_signal_submit; +} + +ChatInputText::type_signal_escape ChatInputText::signal_escape() { + return m_signal_escape; +} + +ChatInputText::type_signal_image_paste ChatInputText::signal_image_paste() { + return m_signal_image_paste; +} + +ChatInputAttachmentContainer::ChatInputAttachmentContainer() + : m_box(Gtk::ORIENTATION_HORIZONTAL) { + get_style_context()->add_class("attachment-container"); + + add(m_box); + m_box.show(); + + set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER); + set_vexpand(true); + set_size_request(-1, AttachmentItemSize + 10); +} + +void ChatInputAttachmentContainer::Clear() { + for (auto *x : m_attachments) + delete x; + m_attachments.clear(); +} + +bool ChatInputAttachmentContainer::AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &path) { + if (m_attachments.size() == 10) return false; + + auto *item = Gtk::make_managed<ChatInputAttachmentItem>(path, pb); + item->show(); + item->set_valign(Gtk::ALIGN_CENTER); + m_box.add(*item); + + m_attachments.insert(item); + + item->signal_remove().connect([this, item] { + m_attachments.erase(item); + delete item; + if (m_attachments.empty()) + m_signal_emptied.emit(); + }); + + return true; +} + +std::vector<std::string> ChatInputAttachmentContainer::GetFilePaths() const { + std::vector<std::string> ret; + for (auto *x : m_attachments) + ret.push_back(x->GetPath()); + return ret; +} + +ChatInputAttachmentContainer::type_signal_emptied ChatInputAttachmentContainer::signal_emptied() { + return m_signal_emptied; +} + +ChatInputAttachmentItem::ChatInputAttachmentItem(std::string path, const Glib::RefPtr<Gdk::Pixbuf> &pb) + : m_path(std::move(path)) + , m_img(Gtk::make_managed<Gtk::Image>()) { + get_style_context()->add_class("attachment-item"); + + int outw, outh; + GetImageDimensions(pb->get_width(), pb->get_height(), outw, outh, AttachmentItemSize, AttachmentItemSize); + m_img->property_pixbuf() = pb->scale_simple(outw, outh, Gdk::INTERP_BILINEAR); + + set_size_request(AttachmentItemSize, AttachmentItemSize); + m_box.add(*m_img); + add(m_box); + show_all_children(); + + SetupMenu(); +} + +std::string ChatInputAttachmentItem::GetPath() const { + return m_path; +} + +void ChatInputAttachmentItem::SetupMenu() { + m_menu_remove.set_label("Remove"); + m_menu_remove.signal_activate().connect([this] { + m_signal_remove.emit(); + }); + + m_menu.add(m_menu_remove); + m_menu.show_all(); + + signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->button == GDK_BUTTON_SECONDARY) { + m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); + return true; + } + + return false; + }); +} + +ChatInputAttachmentItem::type_signal_remove ChatInputAttachmentItem::signal_remove() { + return m_signal_remove; +} + +ChatInput::ChatInput() + : Gtk::Box(Gtk::ORIENTATION_VERTICAL) { + m_input.signal_escape().connect([this] { + m_attachments.Clear(); + m_attachments_revealer.set_reveal_child(false); + m_signal_escape.emit(); + }); + m_input.signal_submit().connect([this](const Glib::ustring &input) -> bool { + const auto attachments = m_attachments.GetFilePaths(); + bool b = m_signal_submit.emit(input, attachments); + if (b) { + m_attachments.Clear(); + m_attachments_revealer.set_reveal_child(false); + } + return b; + }); + + m_attachments.set_vexpand(false); + + m_attachments_revealer.set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP); + m_attachments_revealer.add(m_attachments); + add(m_attachments_revealer); + add(m_input); + show_all_children(); + + m_input.signal_image_paste().connect([this](const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &path) { + if (m_attachments.AddImage(pb, path)) + m_attachments_revealer.set_reveal_child(true); + }); + + // double hack ! + auto cb = [this](GdkEventKey *e) -> bool { + return event(reinterpret_cast<GdkEvent *>(e)); + }; + m_input.signal_key_press_event().connect(cb, false); + + m_attachments.signal_emptied().connect([this] { + m_attachments_revealer.set_reveal_child(false); + }); +} + +void ChatInput::InsertText(const Glib::ustring &text) { + m_input.InsertText(text); +} + +Glib::RefPtr<Gtk::TextBuffer> ChatInput::GetBuffer() { + return m_input.GetBuffer(); +} + +bool ChatInput::ProcessKeyPress(GdkEventKey *event) { + return m_input.ProcessKeyPress(event); +} + ChatInput::type_signal_submit ChatInput::signal_submit() { return m_signal_submit; } diff --git a/src/components/chatinput.hpp b/src/components/chatinput.hpp index ad7f0b1..a6b2a31 100644 --- a/src/components/chatinput.hpp +++ b/src/components/chatinput.hpp @@ -1,9 +1,57 @@ #pragma once #include <gtkmm.h> -class ChatInput : public Gtk::ScrolledWindow { +class ChatInputAttachmentItem : public Gtk::EventBox { public: - ChatInput(); + ChatInputAttachmentItem(std::string path, const Glib::RefPtr<Gdk::Pixbuf> &pb); + + [[nodiscard]] std::string GetPath() const; + +private: + void SetupMenu(); + + Gtk::Menu m_menu; + Gtk::MenuItem m_menu_remove; + + Gtk::Box m_box; + Gtk::Image *m_img = nullptr; + + std::string m_path; + +private: + using type_signal_remove = sigc::signal<void>; + + type_signal_remove m_signal_remove; + +public: + type_signal_remove signal_remove(); +}; + +class ChatInputAttachmentContainer : public Gtk::ScrolledWindow { +public: + ChatInputAttachmentContainer(); + + void Clear(); + bool AddImage(const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &path); + [[nodiscard]] std::vector<std::string> GetFilePaths() const; + +private: + std::set<ChatInputAttachmentItem *> m_attachments; + + Gtk::Box m_box; + +private: + using type_signal_emptied = sigc::signal<void>; + + type_signal_emptied m_signal_emptied; + +public: + type_signal_emptied signal_emptied(); +}; + +class ChatInputText : public Gtk::ScrolledWindow { +public: + ChatInputText(); void InsertText(const Glib::ustring &text); Glib::RefPtr<Gtk::TextBuffer> GetBuffer(); @@ -15,9 +63,42 @@ protected: private: Gtk::TextView m_textview; + bool CheckHandleClipboardPaste(); + void HandleNewPastedImage(const Glib::RefPtr<Gdk::Pixbuf> &pb, const std::string &filename); + +public: + using type_signal_submit = sigc::signal<bool, Glib::ustring>; + using type_signal_escape = sigc::signal<void>; + using type_signal_image_paste = sigc::signal<void, Glib::RefPtr<Gdk::Pixbuf>, std::string>; + + type_signal_submit signal_submit(); + type_signal_escape signal_escape(); + type_signal_image_paste signal_image_paste(); + +private: + type_signal_submit m_signal_submit; + type_signal_escape m_signal_escape; + type_signal_image_paste m_signal_image_paste; +}; + +class ChatInput : public Gtk::Box { +public: + ChatInput(); + + void InsertText(const Glib::ustring &text); + Glib::RefPtr<Gtk::TextBuffer> GetBuffer(); + bool ProcessKeyPress(GdkEventKey *event); + +private: + Gtk::Revealer m_attachments_revealer; + ChatInputAttachmentContainer m_attachments; + ChatInputText m_input; + public: - typedef sigc::signal<bool, Glib::ustring> type_signal_submit; - typedef sigc::signal<void> type_signal_escape; + // text, attachments -> request sent + // maybe this should be reduced to a single struct, its bound to get more complicated (application commands?) + using type_signal_submit = sigc::signal<bool, Glib::ustring, std::vector<std::string>>; + using type_signal_escape = sigc::signal<void>; type_signal_submit signal_submit(); type_signal_escape signal_escape(); diff --git a/src/components/chatlist.cpp b/src/components/chatlist.cpp index 5b923b5..9bd8daf 100644 --- a/src/components/chatlist.cpp +++ b/src/components/chatlist.cpp @@ -352,10 +352,6 @@ ChatList::type_signal_action_message_edit ChatList::signal_action_message_edit() return m_signal_action_message_edit; } -ChatList::type_signal_action_chat_submit ChatList::signal_action_chat_submit() { - return m_signal_action_chat_submit; -} - ChatList::type_signal_action_chat_load_history ChatList::signal_action_chat_load_history() { return m_signal_action_chat_load_history; } diff --git a/src/components/chatlist.hpp b/src/components/chatlist.hpp index f77bbd6..9cc6992 100644 --- a/src/components/chatlist.hpp +++ b/src/components/chatlist.hpp @@ -63,7 +63,6 @@ private: public: // these are all forwarded by the parent using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>; - using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>; using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>; using type_signal_action_channel_click = sigc::signal<void, Snowflake>; using type_signal_action_insert_mention = sigc::signal<void, Snowflake>; @@ -73,7 +72,6 @@ public: using type_signal_action_reply_to = sigc::signal<void, Snowflake>; type_signal_action_message_edit signal_action_message_edit(); - type_signal_action_chat_submit signal_action_chat_submit(); type_signal_action_chat_load_history signal_action_chat_load_history(); type_signal_action_channel_click signal_action_channel_click(); type_signal_action_insert_mention signal_action_insert_mention(); @@ -84,7 +82,6 @@ public: private: type_signal_action_message_edit m_signal_action_message_edit; - type_signal_action_chat_submit m_signal_action_chat_submit; type_signal_action_chat_load_history m_signal_action_chat_load_history; type_signal_action_channel_click m_signal_action_channel_click; type_signal_action_insert_mention m_signal_action_insert_mention; diff --git a/src/components/chatwindow.cpp b/src/components/chatwindow.cpp index 5aab4e6..46c34d4 100644 --- a/src/components/chatwindow.cpp +++ b/src/components/chatwindow.cpp @@ -45,6 +45,8 @@ ChatWindow::ChatWindow() { m_topic_text.set_halign(Gtk::ALIGN_START); m_topic_text.show(); + m_input->set_valign(Gtk::ALIGN_END); + m_input->signal_submit().connect(sigc::mem_fun(*this, &ChatWindow::OnInputSubmit)); m_input->signal_escape().connect([this]() { if (m_is_replying) @@ -70,9 +72,6 @@ ChatWindow::ChatWindow() { m_chat->signal_action_chat_load_history().connect([this](Snowflake id) { m_signal_action_chat_load_history.emit(id); }); - m_chat->signal_action_chat_submit().connect([this](const std::string &str, Snowflake channel_id, Snowflake referenced_id) { - m_signal_action_chat_submit.emit(str, channel_id, referenced_id); - }); m_chat->signal_action_insert_mention().connect([this](Snowflake id) { // lowkey gross m_signal_action_insert_mention.emit(id); @@ -210,15 +209,15 @@ Snowflake ChatWindow::GetActiveChannel() const { return m_active_channel; } -bool ChatWindow::OnInputSubmit(const Glib::ustring &text) { +bool ChatWindow::OnInputSubmit(const Glib::ustring &text, const std::vector<std::string> &attachment_paths) { if (!m_rate_limit_indicator->CanSpeak()) return false; - if (text.empty()) + if (text.empty() && attachment_paths.empty()) return false; if (m_active_channel.IsValid()) - m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler + m_signal_action_chat_submit.emit(text, attachment_paths, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler if (m_is_replying) StopReplying(); diff --git a/src/components/chatwindow.hpp b/src/components/chatwindow.hpp index 9b27ff1..ab0bee1 100644 --- a/src/components/chatwindow.hpp +++ b/src/components/chatwindow.hpp @@ -55,7 +55,7 @@ protected: Snowflake m_active_channel; - bool OnInputSubmit(const Glib::ustring &text); + bool OnInputSubmit(const Glib::ustring &text, const std::vector<std::string> &attachment_paths); bool OnKeyPressEvent(GdkEventKey *e); void OnScrollEdgeOvershot(Gtk::PositionType pos); @@ -84,7 +84,7 @@ protected: public: using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>; - using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>; + using type_signal_action_chat_submit = sigc::signal<void, std::string, std::vector<std::string>, Snowflake, Snowflake>; using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>; using type_signal_action_channel_click = sigc::signal<void, Snowflake, bool>; using type_signal_action_insert_mention = sigc::signal<void, Snowflake>; diff --git a/src/constants.hpp b/src/constants.hpp index 6c6276f..30de2ae 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -2,3 +2,4 @@ constexpr static uint64_t SnowflakeSplitDifference = 600; constexpr static int MaxMessagesForChatCull = 50; // this has to be 50 (for now) cuz that magic number is used in a couple other places and i dont feel like replacing them +constexpr static int AttachmentItemSize = 128; diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index d94f3df..151d88c 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -424,37 +424,34 @@ void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::re } } -void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel) { - // @([^@#]{1,32})#(\\d{4}) +void DiscordClient::SendChatMessageAttachments(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message) { const auto nonce = std::to_string(Snowflake::FromNow()); CreateMessageObject obj; obj.Content = content; obj.Nonce = nonce; - m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce)); - // dummy data so the content can be shown while waiting for MESSAGE_CREATE - Message tmp; - tmp.Content = content; - tmp.ID = nonce; - tmp.ChannelID = channel; - tmp.Author = GetUserData(); - tmp.IsTTS = false; - tmp.DoesMentionEveryone = false; - tmp.Type = MessageType::DEFAULT; - tmp.IsPinned = false; - tmp.Timestamp = "2000-01-01T00:00:00.000000+00:00"; - tmp.Nonce = obj.Nonce; - tmp.IsPending = true; - m_store.SetMessage(tmp.ID, tmp); - m_signal_message_sent.emit(tmp); + if (referenced_message.IsValid()) + obj.MessageReference.emplace().MessageID = referenced_message; + + auto req = m_http.CreateRequest(http::REQUEST_POST, "/channels/" + std::to_string(channel) + "/messages"); + req.make_form(); + req.add_field("payload_json", nlohmann::json(obj).dump().c_str(), CURL_ZERO_TERMINATED); + for (size_t i = 0; i < attachment_paths.size(); i++) { + const auto field_name = "files[" + std::to_string(i) + "]"; + req.add_file(field_name, attachment_paths.at(i), "unknown.png"); + } + m_http.Execute(std::move(req), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce)); } -void DiscordClient::SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message) { +void DiscordClient::SendChatMessageText(const std::string &content, Snowflake channel, Snowflake referenced_message) { + // @([^@#]{1,32})#(\\d{4}) const auto nonce = std::to_string(Snowflake::FromNow()); CreateMessageObject obj; obj.Content = content; obj.Nonce = nonce; - obj.MessageReference.emplace().MessageID = referenced_message; + if (referenced_message.IsValid()) + obj.MessageReference.emplace().MessageID = referenced_message; m_http.MakePOST("/channels/" + std::to_string(channel) + "/messages", nlohmann::json(obj).dump(), sigc::bind<0>(sigc::mem_fun(*this, &DiscordClient::ChatMessageCallback), nonce)); + // dummy data so the content can be shown while waiting for MESSAGE_CREATE Message tmp; tmp.Content = content; tmp.ID = nonce; @@ -471,6 +468,24 @@ void DiscordClient::SendChatMessage(const std::string &content, Snowflake channe m_signal_message_sent.emit(tmp); } +void DiscordClient::SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel) { + if (attachment_paths.empty()) + SendChatMessageText(content, channel); + else { + puts("attach"); + SendChatMessageAttachments(content, attachment_paths, channel, Snowflake::Invalid); + } +} + +void DiscordClient::SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message) { + if (attachment_paths.empty()) + SendChatMessageText(content, channel, referenced_message); + else { + puts("attach"); + SendChatMessageAttachments(content, attachment_paths, channel, referenced_message); + } +} + void DiscordClient::DeleteMessage(Snowflake channel_id, Snowflake id) { std::string path = "/channels/" + std::to_string(channel_id) + "/messages/" + std::to_string(id); m_http.MakeDELETE(path, [](auto) {}); diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 6310296..fdb5a5f 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -104,8 +104,8 @@ public: void ChatMessageCallback(const std::string &nonce, const http::response_type &response); - void SendChatMessage(const std::string &content, Snowflake channel); - void SendChatMessage(const std::string &content, Snowflake channel, Snowflake referenced_message); + void SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel); + void SendChatMessage(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message); void DeleteMessage(Snowflake channel_id, Snowflake id); void EditMessage(Snowflake channel_id, Snowflake id, std::string content); void SendLazyLoad(Snowflake id); @@ -223,6 +223,9 @@ private: std::vector<uint8_t> m_decompress_buf; z_stream m_zstream; + void SendChatMessageAttachments(const std::string &content, const std::vector<std::string> &attachment_paths, Snowflake channel, Snowflake referenced_message = Snowflake::Invalid); + void SendChatMessageText(const std::string &content, Snowflake channel, Snowflake referenced_message = Snowflake::Invalid); + static std::string GetAPIURL(); static std::string GetGatewayURL(); diff --git a/src/discord/httpclient.cpp b/src/discord/httpclient.cpp index f5b640c..6646bf3 100644 --- a/src/discord/httpclient.cpp +++ b/src/discord/httpclient.cpp @@ -112,6 +112,25 @@ void HTTPClient::MakeGET(const std::string &path, const std::function<void(http: })); } +http::request HTTPClient::CreateRequest(http::EMethod method, std::string path) { + http::request req(method, m_api_base + path); + req.set_header("Authorization", m_authorization); + req.set_user_agent(!m_agent.empty() ? m_agent : "Abaddon"); +#ifdef USE_LOCAL_PROXY + req.set_proxy("http://127.0.0.1:8888"); + req.set_verify_ssl(false); +#endif + return req; +} + +void HTTPClient::Execute(http::request &&req, const std::function<void(http::response_type r)> &cb) { + printf("%s %s\n", req.get_method(), req.get_url().c_str()); + m_futures.push_back(std::async(std::launch::async, [this, cb, req = std::move(req)]() mutable { + 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) diff --git a/src/discord/httpclient.hpp b/src/discord/httpclient.hpp index 47b8a4d..366d4eb 100644 --- a/src/discord/httpclient.hpp +++ b/src/discord/httpclient.hpp @@ -23,6 +23,9 @@ public: void MakePOST(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb); void MakePUT(const std::string &path, const std::string &payload, const std::function<void(http::response_type r)> &cb); + [[nodiscard]] http::request CreateRequest(http::EMethod method, std::string path); + void Execute(http::request &&req, const std::function<void(http::response_type r)> &cb); + private: void OnResponse(const http::response_type &r, const std::function<void(http::response_type r)> &cb); void CleanupFutures(); diff --git a/src/http.cpp b/src/http.cpp index 7338d39..beb1944 100644 --- a/src/http.cpp +++ b/src/http.cpp @@ -29,12 +29,33 @@ request::request(EMethod method, std::string url) prepare(); } +request::request(request &&other) noexcept + : m_curl(std::exchange(other.m_curl, nullptr)) + , m_url(std::exchange(other.m_url, "")) + , m_method(std::exchange(other.m_method, nullptr)) + , m_header_list(std::exchange(other.m_header_list, nullptr)) + , m_error_buf(other.m_error_buf) + , m_form(std::exchange(other.m_form, nullptr)) { + // i think this is correct??? +} + request::~request() { if (m_curl != nullptr) curl_easy_cleanup(m_curl); if (m_header_list != nullptr) curl_slist_free_all(m_header_list); + + if (m_form != nullptr) + curl_mime_free(m_form); +} + +const std::string &request::get_url() const { + return m_url; +} + +const char *request::get_method() const { + return m_method; } void request::set_verify_ssl(bool verify) { @@ -57,6 +78,26 @@ void request::set_user_agent(const std::string &data) { curl_easy_setopt(m_curl, CURLOPT_USERAGENT, data.c_str()); } +void request::make_form() { + m_form = curl_mime_init(m_curl); +} + +// file must exist until request completes +void request::add_file(std::string_view name, std::string_view file_path, std::string_view filename) { + auto *field = curl_mime_addpart(m_form); + curl_mime_name(field, name.data()); + curl_mime_filedata(field, file_path.data()); + curl_mime_filename(field, filename.data()); +} + +// copied +void request::add_field(std::string_view name, const char *data, size_t size) { + puts(name.data()); + auto *field = curl_mime_addpart(m_form); + curl_mime_name(field, name.data()); + curl_mime_data(field, data, size); +} + response request::execute() { if (m_curl == nullptr) { auto response = detail::make_response(m_url, EStatusCode::ClientErrorCURLInit); @@ -76,12 +117,14 @@ response request::execute() { m_error_buf[0] = '\0'; if (m_header_list != nullptr) curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_header_list); + if (m_form != nullptr) + curl_easy_setopt(m_curl, CURLOPT_MIMEPOST, m_form); 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); + response.error_string += " " + std::string(m_error_buf.data()); return response; } diff --git a/src/http.hpp b/src/http.hpp index 4220a6e..0c570db 100644 --- a/src/http.hpp +++ b/src/http.hpp @@ -1,4 +1,5 @@ #pragma once +#include <array> #include <string> #include <curl/curl.h> @@ -98,13 +99,24 @@ struct response { struct request { request(EMethod method, std::string url); + request(request &&other) noexcept; ~request(); + request(const request &) = delete; + request &operator=(const request &) = delete; + request &operator=(request &&) noexcept = delete; + + const std::string &get_url() const; + const char *get_method() const; + 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); + void make_form(); + void add_file(std::string_view name, std::string_view file_path, std::string_view filename); + void add_field(std::string_view name, const char *data, size_t size); response execute(); @@ -115,7 +127,8 @@ private: std::string m_url; const char *m_method; curl_slist *m_header_list = nullptr; - char m_error_buf[CURL_ERROR_SIZE] = { 0 }; + std::array<char, CURL_ERROR_SIZE> m_error_buf = { 0 }; + curl_mime *m_form = nullptr; }; using response_type = response; |