From b08d2bf366c6ccc8a61e5eb77fc2fdb9bcfa5dba Mon Sep 17 00:00:00 2001 From: Green Sky Date: Sat, 22 Jul 2023 23:31:56 +0200 Subject: [PATCH] inital commit (message v3) --- CMakeLists.txt | 31 +++++ LICENSE | 24 ++++ README.md | 6 + solanaceae/message3/components.hpp | 120 ++++++++++++++++ solanaceae/message3/components_id.inl | 36 +++++ solanaceae/message3/file.hpp | 20 +++ solanaceae/message3/file_r_file.hpp | 53 +++++++ solanaceae/message3/file_r_mem.hpp | 38 +++++ solanaceae/message3/file_rw_file.hpp | 60 ++++++++ solanaceae/message3/file_w_file.hpp | 46 ++++++ solanaceae/message3/message_model3.hpp | 27 ++++ solanaceae/message3/message_time_sort.cpp | 29 ++++ solanaceae/message3/message_time_sort.hpp | 23 +++ .../message3/registry_message_model.cpp | 131 ++++++++++++++++++ .../message3/registry_message_model.hpp | 100 +++++++++++++ 15 files changed, 744 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 solanaceae/message3/components.hpp create mode 100644 solanaceae/message3/components_id.inl create mode 100644 solanaceae/message3/file.hpp create mode 100644 solanaceae/message3/file_r_file.hpp create mode 100644 solanaceae/message3/file_r_mem.hpp create mode 100644 solanaceae/message3/file_rw_file.hpp create mode 100644 solanaceae/message3/file_w_file.hpp create mode 100644 solanaceae/message3/message_model3.hpp create mode 100644 solanaceae/message3/message_time_sort.cpp create mode 100644 solanaceae/message3/message_time_sort.hpp create mode 100644 solanaceae/message3/registry_message_model.cpp create mode 100644 solanaceae/message3/registry_message_model.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..da8ca36 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +project(solanaceae) + +add_library(solanaceae_message3 + ./solanaceae/message3/file.hpp + ./solanaceae/message3/file_r_mem.hpp + ./solanaceae/message3/file_r_file.hpp + ./solanaceae/message3/file_w_file.hpp + ./solanaceae/message3/file_rw_file.hpp + + ./solanaceae/message3/components.hpp + ./solanaceae/message3/components_id.inl + + ./solanaceae/message3/message_model3.hpp + + ./solanaceae/message3/registry_message_model.hpp + ./solanaceae/message3/registry_message_model.cpp + + ./solanaceae/message3/message_time_sort.hpp + ./solanaceae/message3/message_time_sort.cpp +) + +target_include_directories(solanaceae_message3 PUBLIC .) +target_compile_features(solanaceae_message3 PUBLIC cxx_std_17) +target_link_libraries(solanaceae_message3 PUBLIC + solanaceae_util + solanaceae_contact + EnTT::EnTT +) + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2780797 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +The Code is under the following License, if not stated otherwise: + +MIT License + +Copyright (c) 2023 Erik Scholz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..177d650 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +`plant !` + +provides `message` functionallity for solanaceae code. + +relies on [EnTT](https://github.com/skypjack/entt) + diff --git a/solanaceae/message3/components.hpp b/solanaceae/message3/components.hpp new file mode 100644 index 0000000..f7fc413 --- /dev/null +++ b/solanaceae/message3/components.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include + +//fwd +struct FileI; + +#include +#include + +namespace Message::Components { + + struct ContactFrom { + Contact3 c; + }; + + struct ContactTo { + Contact3 c; + }; + + struct Timestamp { + uint64_t ts {0}; + }; + + struct TimestampProcessed { + uint64_t ts {0}; + }; + + struct TimestampWritten { + uint64_t ts {0}; + }; + + struct MessageText { + std::string text; + MessageText(const std::string_view& view) : text(view) {} + }; + + struct TagMessageIsAction {}; + + namespace Transfer { + + //struct TransferState { + //enum State { + //running, + //paused, + //finished, + //failed, + //} state = paused; + //}; + struct TagHaveAll {}; + + struct BytesSent { + uint64_t total {0u}; + }; + + struct BytesReceived { + uint64_t total {0u}; + }; + + // TODO: rename to stream? + using File = std::unique_ptr; + + struct TagReceiving {}; + struct TagSending {}; + // TODO: add both? + + // convert to enum? + // TODO: local/remote + // TODO: invert? + struct TagPaused {}; + + struct StateCanceled { + enum Reason { + disconnected, + remote_canceld, + local_canceld, + } reason = disconnected; + }; + +#if 0 + struct FileID { + // persistent ID + // sometimes called file_id or hash + ToxKey id; + // TODO: variable length + }; + + struct FileKind { + // TODO: use tox file kind + uint64_t kind {0}; + }; +#endif + + struct FileInfo { + struct FileDirEntry { + std::string file_name; // full path relative to base + uint64_t file_size {0}; + }; + std::vector file_list; + uint64_t total_size {0}; + }; + + // describes the files locally + // filename might be different to non local FileInfo + // order is the same + struct FileInfoLocal { + std::vector file_list; + }; + + // TODO: rename to start? or set or ... + struct ActionAccept { + std::string save_to_path; + }; + + } // Transfer + +} // Message::Components + +#include "./components_id.inl" + diff --git a/solanaceae/message3/components_id.inl b/solanaceae/message3/components_id.inl new file mode 100644 index 0000000..6aed4bd --- /dev/null +++ b/solanaceae/message3/components_id.inl @@ -0,0 +1,36 @@ +#include "./components.hpp" + +#include + +// TODO: move more central +#define DEFINE_COMP_ID(x) \ +template<> \ +constexpr entt::id_type entt::type_hash::value() noexcept { \ + using namespace entt::literals; \ + return #x##_hs; \ +} + +// cross compile(r) stable ids + +DEFINE_COMP_ID(Message::Components::ContactFrom) +DEFINE_COMP_ID(Message::Components::ContactTo) +DEFINE_COMP_ID(Message::Components::Timestamp) +DEFINE_COMP_ID(Message::Components::TimestampProcessed) +DEFINE_COMP_ID(Message::Components::TimestampWritten) +DEFINE_COMP_ID(Message::Components::MessageText) +DEFINE_COMP_ID(Message::Components::TagMessageIsAction) + +DEFINE_COMP_ID(Message::Components::Transfer::TagHaveAll) +DEFINE_COMP_ID(Message::Components::Transfer::BytesSent) +DEFINE_COMP_ID(Message::Components::Transfer::BytesReceived) +DEFINE_COMP_ID(Message::Components::Transfer::File) +DEFINE_COMP_ID(Message::Components::Transfer::TagReceiving) +DEFINE_COMP_ID(Message::Components::Transfer::TagSending) +DEFINE_COMP_ID(Message::Components::Transfer::TagPaused) +DEFINE_COMP_ID(Message::Components::Transfer::StateCanceled) +DEFINE_COMP_ID(Message::Components::Transfer::FileInfo) +DEFINE_COMP_ID(Message::Components::Transfer::FileInfoLocal) +DEFINE_COMP_ID(Message::Components::Transfer::ActionAccept) + +#undef DEFINE_COMP_ID + diff --git a/solanaceae/message3/file.hpp b/solanaceae/message3/file.hpp new file mode 100644 index 0000000..496db8a --- /dev/null +++ b/solanaceae/message3/file.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +struct FileI { + uint64_t _file_size {0}; + // TODO: remove? + uint64_t _bytes_read {0}; + uint64_t _bytes_written {0}; + + virtual ~FileI(void) {} + + virtual bool isGood(void) = 0; + + // TODO: move to owning/nonowning pointers + virtual std::vector read(uint64_t pos, uint32_t size) = 0; + virtual bool write(uint64_t pos, const std::vector& data) = 0; +}; + diff --git a/solanaceae/message3/file_r_file.hpp b/solanaceae/message3/file_r_file.hpp new file mode 100644 index 0000000..27e7ffb --- /dev/null +++ b/solanaceae/message3/file_r_file.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "./file.hpp" + +#include + +struct FileRFile : public FileI { + std::ifstream _file; + + FileRFile(std::string_view file_path) : _file(static_cast(file_path), std::ios::binary) { + if (!_file.is_open()) { + return; // TODO: error + } + + // figure out size + _file.seekg(0, _file.end); + _file_size = _file.tellg(); + _file.seekg(0, _file.beg); + } + + //FileRFile(std::ifstream&& other_file) : _file(std::move(other_file)) {} + //FileRFile(std::ifstream&& other_file, size_t file_size) : _file(std::move(other_file)), _file_size(file_size) {} + + virtual ~FileRFile(void) {} + + bool isGood(void) override { + return _file.is_open() && _file.good(); + } + + std::vector read(uint64_t pos, uint32_t size) override { + if (_file_size > 0 && pos >= _file_size) { + return {}; + } + + // TODO: error check + _file.seekg(pos); + + // TODO: optimize + std::vector chunk; + int read_char; + for (size_t i = 0; i < size && (_file_size == 0 || i+pos < _file_size) && (read_char = _file.get()) != std::ifstream::traits_type::eof(); i++) { + chunk.push_back(read_char); + } + + _bytes_read += chunk.size(); + + return chunk; + } + + // read only + bool write(uint64_t, const std::vector&) override { return false; } +}; + diff --git a/solanaceae/message3/file_r_mem.hpp b/solanaceae/message3/file_r_mem.hpp new file mode 100644 index 0000000..7a68e31 --- /dev/null +++ b/solanaceae/message3/file_r_mem.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "./file.hpp" + +struct FileRMem : public FileI { + std::vector _data; + + FileRMem(void) = delete; + FileRMem(const std::vector& data) : _data(data) { + _file_size = _data.size(); + } + virtual ~FileRMem(void) {} + + bool isGood(void) override { + return true; + } + + std::vector read(uint64_t pos, uint32_t size) override { + if (_file_size > 0 && pos >= _data.size()) { + return {}; + } + + // TODO: optimize + std::vector chunk; + for (size_t i = 0; i < size && i+pos < _data.size(); i++) { + chunk.push_back(_data[pos+i]); + } + //chunk.insert(chunk.begin(), _data.cbegin()+pos, _data.cbegin()+pos+size); + + _bytes_read += chunk.size(); + + return chunk; + } + + // read only + bool write(uint64_t, const std::vector&) override { return false; } +}; + diff --git a/solanaceae/message3/file_rw_file.hpp b/solanaceae/message3/file_rw_file.hpp new file mode 100644 index 0000000..8bb0d58 --- /dev/null +++ b/solanaceae/message3/file_rw_file.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "./file.hpp" + +#include + +struct FileRWFile : public FileI { + std::fstream _file; + + //FileWFile(std::string_view file_path, uint64_t file_size) : _file(static_cast(file_path), std::ios::binary) { + //_file_size = file_size; + + //if (!_file.is_open()) { + //return; // TODO: error + //} + //} + + virtual ~FileRWFile(void) {} + + bool isGood(void) override { + return _file.is_open() && _file.good(); + } + + std::vector read(uint64_t pos, uint32_t size) override { + if (pos >= _file_size) { + return {}; + } + + // TODO: error check + _file.seekg(pos); + + // TODO: optimize + std::vector chunk; + int read_char; + for (size_t i = 0; i < size && (_file_size == 0 || i+pos < _file_size) && (read_char = _file.get()) != std::ifstream::traits_type::eof(); i++) { + chunk.push_back(read_char); + } + + _bytes_read += chunk.size(); + + return chunk; + } + + bool write(uint64_t pos, const std::vector& data) override { + if (pos >= _file_size) { + return false; + } + + // if out-of-order, seek + if (_file.tellp() != int64_t(pos)) { + // TODO: error check + _file.seekp(pos); + } + + _file.write(reinterpret_cast(data.data()), data.size()); + + return _file.good(); + } +}; + diff --git a/solanaceae/message3/file_w_file.hpp b/solanaceae/message3/file_w_file.hpp new file mode 100644 index 0000000..80e9129 --- /dev/null +++ b/solanaceae/message3/file_w_file.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "./file.hpp" + +#include +#include + +struct FileWFile : public FileI { + std::ofstream _file; + + FileWFile(std::string_view file_path, uint64_t file_size) : _file(static_cast(file_path), std::ios::binary) { + _file_size = file_size; + + if (!_file.is_open()) { + return; // TODO: error + } + } + + virtual ~FileWFile(void) {} + + bool isGood(void) override { + return _file.is_open() && _file.good(); + } + + // write only + std::vector read(uint64_t, uint32_t) override { return {}; } + + bool write(uint64_t pos, const std::vector& data) override { + if (_file_size > 0 && pos >= _file_size) { + return false; + } + + // if out-of-order, seek + if (_file.tellp() != int64_t(pos)) { + // TODO: error check + _file.seekp(pos); + } + + _file.write(reinterpret_cast(data.data()), data.size()); + + _bytes_written += data.size(); + + return _file.good(); + } +}; + diff --git a/solanaceae/message3/message_model3.hpp b/solanaceae/message3/message_model3.hpp new file mode 100644 index 0000000..f3dce05 --- /dev/null +++ b/solanaceae/message3/message_model3.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +//#include "./file.hpp" + +// TODO: move, rename, do something?, change in favor of tox? +//enum class FileKind : uint32_t { + //file = 0u, + //avatar = 1u, +//}; + +// interface to send messages +struct MessageModel3I { + virtual ~MessageModel3I(void) {} + + // return true if a handler was found for the contact + + virtual bool sendText(const Contact3 c, std::string_view message, bool action = false) { (void)c,(void)message,(void)action; return false; } + + //virtual bool sendFile(const Contact& c, std::string_view file_name, std::unique_ptr file) { (void)c,(void)message,(void)action; return false; } + virtual bool sendFilePath(const Contact3 c, std::string_view file_name, std::string_view file_path) { (void)c,(void)file_name,(void)file_path; return false; } + + + //virtual bool sendFileMem(const Contact& c, std::string_view file_name, const std::vector& file) = 0; +}; + diff --git a/solanaceae/message3/message_time_sort.cpp b/solanaceae/message3/message_time_sort.cpp new file mode 100644 index 0000000..56a8e7a --- /dev/null +++ b/solanaceae/message3/message_time_sort.cpp @@ -0,0 +1,29 @@ +#include "./message_time_sort.hpp" + +#include + +MessageTimeSort::MessageTimeSort(RegistryMessageModel& rmm) : _rmm(rmm) { + _rmm.subscribe(this, RegistryMessageModel_Event::message_construct); + _rmm.subscribe(this, RegistryMessageModel_Event::message_updated); +} + +void MessageTimeSort::iterate(void) { + // TODO: maybe only every x for updated + for (auto* reg : _to_sort) { + reg->sort([](const auto& lhs, const auto& rhs) -> bool { + return lhs.ts < rhs.ts; + }, entt::insertion_sort{}); + } + _to_sort.clear(); +} + +bool MessageTimeSort::onEvent(const Message::Events::MessageConstruct& e) { + _to_sort.emplace(e.e.registry()); + return false; +} + +bool MessageTimeSort::onEvent(const Message::Events::MessageUpdated& e) { + _to_sort.emplace(e.e.registry()); + return false; +} + diff --git a/solanaceae/message3/message_time_sort.hpp b/solanaceae/message3/message_time_sort.hpp new file mode 100644 index 0000000..bed43b5 --- /dev/null +++ b/solanaceae/message3/message_time_sort.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "./registry_message_model.hpp" + +#include + +class MessageTimeSort : public RegistryMessageModelEventI { + RegistryMessageModel& _rmm; + + // TODO: use contact instead + entt::dense_set _to_sort; + + public: + MessageTimeSort(RegistryMessageModel& rmm); + + // do the sorting + void iterate(void); + + protected: // mm + bool onEvent(const Message::Events::MessageConstruct& e) override; + bool onEvent(const Message::Events::MessageUpdated& e) override; +}; + diff --git a/solanaceae/message3/registry_message_model.cpp b/solanaceae/message3/registry_message_model.cpp new file mode 100644 index 0000000..294eb94 --- /dev/null +++ b/solanaceae/message3/registry_message_model.cpp @@ -0,0 +1,131 @@ +#include "./registry_message_model.hpp" + +#include + +#include + +Message3Registry* RegistryMessageModel::get(Contact3 c) { + if (_cr.valid(c) && !_cr.all_of(c)) { + // TODO: loop upwards + if (!_cr.all_of(c)) { + return nullptr; + } + c = _cr.get(c).parent; + } + + if (!_cr.valid(c)) { + // TODO: throw error + return nullptr; + } + + const auto it = _contact_messages.find(c); + if (it != _contact_messages.end()) { + return it->second.get(); + } + + auto& reg_sh = _contact_messages[c] = std::make_unique(); + return reg_sh.get(); +} + +Message3Registry* RegistryMessageModel::get(Contact3 c) const { + if (_cr.valid(c) && !_cr.all_of(c)) { + // TODO: loop upwards + if (!_cr.all_of(c)) { + return nullptr; + } + c = _cr.get(c).parent; + } + + if (!_cr.valid(c)) { + // TODO: throw error + return nullptr; + } + + const auto it = _contact_messages.find(c); + if (it != _contact_messages.cend()) { + return it->second.get(); + } + + return nullptr; +} + +void RegistryMessageModel::throwEventConstruct(Message3Registry& reg, Message3 e) { + std::cout << "RMM debug: event construct " << entt::to_integral(e) << "\n"; + dispatch( + RegistryMessageModel_Event::message_construct, + Message::Events::MessageConstruct{ + Message3Handle{reg, e} + } + ); +} + +void RegistryMessageModel::throwEventUpdate(Message3Registry& reg, Message3 e) { + // the update while update lock is hacky + _update_queue.push_back({reg, e}); + if (!_update_in_progess) { + _update_in_progess = true; + for (size_t i = 0; i < _update_queue.size(); i++) { + std::cout << "RMM debug: event update " << entt::to_integral(e) << "\n"; + dispatch( + RegistryMessageModel_Event::message_updated, + Message::Events::MessageUpdated{ + _update_queue.at(i) + } + ); + } + _update_queue.clear(); + _update_in_progess = false; + } +} + +void RegistryMessageModel::throwEventDestroy(Message3Registry& reg, Message3 e) { + std::cout << "RMM debug: event destroy " << entt::to_integral(e) << "\n"; + dispatch( + RegistryMessageModel_Event::message_destroy, + Message::Events::MessageDestory{ + Message3Handle{reg, e} + } + ); +} + +void RegistryMessageModel::throwEventConstruct(const Contact3 c, Message3 e) { + if (auto* reg_ptr = get(c); reg_ptr) { + throwEventConstruct(*reg_ptr, e); + } +} + +void RegistryMessageModel::throwEventUpdate(const Contact3 c, Message3 e) { + if (auto* reg_ptr = get(c); reg_ptr) { + throwEventUpdate(*reg_ptr, e); + } +} + +void RegistryMessageModel::throwEventDestroy(const Contact3 c, Message3 e) { + if (auto* reg_ptr = get(c); reg_ptr) { + throwEventDestroy(*reg_ptr, e); + } +} + +bool RegistryMessageModel::sendText(const Contact3 c, std::string_view message, bool action) { + std::cout << "RMM debug: event send text\n"; + + // manual, bc its not an "event" + for (auto* zei : _subscribers.at(size_t(RegistryMessageModel_Event::send_text))) { + if (zei->sendText(c, message, action)) { + return true; + } + } + return false; +} + +bool RegistryMessageModel::sendFilePath(const Contact3 c, std::string_view file_name, std::string_view file_path) { + std::cout << "RMM debug: event send file path\n"; + + // manual, bc its not an "event" + for (auto* zei : _subscribers.at(size_t(RegistryMessageModel_Event::send_file_path))) { + if (zei->sendFilePath(c, file_name, file_path)) { + return true; + } + } + return false; +} diff --git a/solanaceae/message3/registry_message_model.hpp b/solanaceae/message3/registry_message_model.hpp new file mode 100644 index 0000000..78e0f22 --- /dev/null +++ b/solanaceae/message3/registry_message_model.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include + +#include "./message_model3.hpp" + +#include +#include +#include + +#include + +// strong typing +enum class Message3 : uint32_t {}; +using Message3Registry = entt::basic_registry; +using Message3Handle = entt::basic_handle; + +namespace Message::Events { + + struct MessageConstruct { + const Message3Handle e; + }; + + struct MessageUpdated { + const Message3Handle e; + // hint? + // like component list? + }; + + struct MessageDestory { + const Message3Handle e; + }; + +} // Events + +enum class RegistryMessageModel_Event : uint32_t { + message_construct, + message_updated, + message_destroy, + + send_text, + send_file_path, + + MAX +}; + +// 2 in 1 +struct RegistryMessageModelEventI : public MessageModel3I { + using enumType = RegistryMessageModel_Event; + + virtual ~RegistryMessageModelEventI(void) {} + + virtual bool onEvent(const Message::Events::MessageConstruct&) { return false; } + virtual bool onEvent(const Message::Events::MessageUpdated&) { return false; } + virtual bool onEvent(const Message::Events::MessageDestory&) { return false; } + + // mm3 + // send text + // send file path +}; +using RegistryMessageModelEventProviderI = EventProviderI; + +class RegistryMessageModel : public RegistryMessageModelEventProviderI, public MessageModel3I { + protected: + Contact3Registry& _cr; + + entt::dense_map> _contact_messages; + + bool _update_in_progess {false}; + std::vector _update_queue {}; + + public: + RegistryMessageModel(Contact3Registry& cr) : _cr(cr) {} + virtual ~RegistryMessageModel(void) {} + + // TODO: iterate? + + public: + Message3Registry* get(Contact3 c); + Message3Registry* get(Contact3 c) const; + + public: // dispatcher + // !!! remember to manually throw these externally + void throwEventConstruct(Message3Registry& reg, Message3 e); + void throwEventUpdate(Message3Registry& reg, Message3 e); + void throwEventDestroy(Message3Registry& reg, Message3 e); + + void throwEventConstruct(Message3Handle h) { throwEventConstruct(*h.registry(), h.entity()); } + void throwEventUpdate(Message3Handle h) { throwEventUpdate(*h.registry(), h.entity()); } + void throwEventDestroy(Message3Handle h) { throwEventDestroy(*h.registry(), h.entity()); } + + void throwEventConstruct(const Contact3 c, Message3 e); + void throwEventUpdate(const Contact3 c, Message3 e); + void throwEventDestroy(const Contact3 c, Message3 e); + + public: // mm3 + bool sendText(const Contact3 c, std::string_view message, bool action = false) override; + bool sendFilePath(const Contact3 c, std::string_view file_name, std::string_view file_path) override; +}; +