diff --git a/src/chat/chat_tz.txt b/src/chat/chat_tz.txt deleted file mode 100644 index 82acf57e..00000000 --- a/src/chat/chat_tz.txt +++ /dev/null @@ -1,11 +0,0 @@ -Архитектура чата (группового) - -1. сообщения чата (текст + ссылки на аттачи) хранятся в таблице: -id (primary key), timestamp, user_id, msg (utf8 text 4k max), sign - -2. реакции: key(msg_id, user_id), type, sign -3. аттачи: key(msg_id, user_id), filename, sign - -Структура каталогов: -chats.db - база - diff --git a/src/msg_transport.c b/src/msg_transport.c index 8d97af23..771a5c29 100644 --- a/src/msg_transport.c +++ b/src/msg_transport.c @@ -9,6 +9,8 @@ #include "utun_instance.h" #include "etcp_router.h" #include "etcp.h" +#include "secure_channel.h" +#include "route_bgp.h" #include "../lib/u_async.h" #include "../lib/debug_config.h" #include "../lib/mem.h" @@ -44,6 +46,7 @@ struct msg_client { uint8_t recv_buffer[MSG_MAX_MSG_SIZE]; uint16_t recv_len; uint64_t bound_peer_id; /* 0 = не забинден */ + uint8_t is_localhost; /* 1 = подключение с loopback */ }; struct msg_transport { @@ -275,6 +278,18 @@ static void accept_callback(socket_t fd, void* arg) { client->fd = client_fd; client->transport = t; + /* Определяем loopback */ + if (client_addr.ss_family == AF_INET) { + struct sockaddr_in* sin = (struct sockaddr_in*)&client_addr; + client->is_localhost = (ntohl(sin->sin_addr.s_addr) == 0x7F000001u); /* 127.0.0.1 */ + } else if (client_addr.ss_family == AF_INET6) { + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&client_addr; + static const uint8_t loopback_v6[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; + client->is_localhost = (memcmp(&sin6->sin6_addr, loopback_v6, 16) == 0); + } else { + client->is_localhost = 0; + } + client->socket_id = uasync_add_socket_t(t->ua, client_fd, client_read_callback, NULL, client_except_callback, client); @@ -425,6 +440,44 @@ static void handle_client_data(struct msg_transport* t, struct msg_client* clien DEBUG_INFO(DEBUG_CATEGORY_MSGTRANSPORT, "UNBIND"); break; + case MSG_CTL_GET_PUBKEY: + send_response(client, MSG_RSP_PUBKEY, 0, + t->instance->my_keys.public_key, SC_PUBKEY_SIZE); + break; + + case MSG_CTL_GET_PRIVKEY: + if (!client->is_localhost) { + send_error(client, MSG_ERR_ACCESS_DENIED, "private key only available from localhost"); + DEBUG_WARN(DEBUG_CATEGORY_MSGTRANSPORT, "GET_PRIVKEY denied: not localhost"); + break; + } + send_response(client, MSG_RSP_PRIVKEY, 0, + t->instance->my_keys.private_key, SC_PRIVKEY_SIZE); + break; + + case MSG_CTL_GET_PEER_INFO: { + uint64_t peer_id = hdr->id; + struct ROUTE_BGP* bgp = t->instance->bgp; + if (!bgp) { + send_error(client, MSG_ERR_PEER_NOT_FOUND, "BGP not initialized"); + break; + } + struct NODEINFO_Q* nq = route_bgp_get_node(bgp, peer_id); + if (!nq) { + send_error(client, MSG_ERR_PEER_NOT_FOUND, "peer not found"); + DEBUG_WARN(DEBUG_CATEGORY_MSGTRANSPORT, "GET_PEER_INFO: peer=%016llx not found", + (unsigned long long)peer_id); + break; + } + uint8_t buf[SC_PUBKEY_SIZE * 2]; + memcpy(buf, nq->node.public_key, SC_PUBKEY_SIZE); + memcpy(buf + SC_PUBKEY_SIZE, nq->node.ed25519_public_key, SC_PUBKEY_SIZE); + send_response(client, MSG_RSP_PEER_INFO, peer_id, buf, SC_PUBKEY_SIZE * 2); + DEBUG_INFO(DEBUG_CATEGORY_MSGTRANSPORT, "GET_PEER_INFO: peer=%016llx sent pubkey+edpub", + (unsigned long long)peer_id); + break; + } + case MSG_DATA_SEND: { uint64_t dst = hdr->id ? hdr->id : client->bound_peer_id; if (dst == 0) { diff --git a/src/msg_transport.h b/src/msg_transport.h index e9833d63..54169402 100644 --- a/src/msg_transport.h +++ b/src/msg_transport.h @@ -34,9 +34,15 @@ extern "C" { /* Типы сообщений */ #define MSG_CTL_BIND 0x01 /* GUI→utun: привязать TCP-соединение к пиру id */ #define MSG_CTL_UNBIND 0x02 /* GUI→utun: отвязать */ +#define MSG_CTL_GET_PUBKEY 0x03 /* GUI→utun: запросить публичный ключ */ +#define MSG_CTL_GET_PRIVKEY 0x04 /* GUI→utun: запросить приватный ключ (только localhost) */ +#define MSG_CTL_GET_PEER_INFO 0x05 /* GUI→utun: запросить ключи пира id (pubkey + ed25519_pubkey) */ #define MSG_DATA_SEND 0x10 /* GUI→utun: отправить сообщение пиру id */ #define MSG_DATA_RECV 0x11 /* utun→GUI: получено сообщение от пира id */ #define MSG_RSP_BIND_OK 0xE1 /* utun→GUI: BIND успешен */ +#define MSG_RSP_PUBKEY 0xE2 /* utun→GUI: публичный ключ (32 байта) */ +#define MSG_RSP_PRIVKEY 0xE3 /* utun→GUI: приватный ключ (32 байта) */ +#define MSG_RSP_PEER_INFO 0xE4 /* utun→GUI: ключи пира (pubkey+edpub=64 байта) */ #define MSG_RSP_ERROR 0xFF /* utun→GUI: ошибка */ /* Коды ошибок для MSG_RSP_ERROR */ @@ -44,6 +50,8 @@ extern "C" { #define MSG_ERR_NOT_BOUND 0x02 #define MSG_ERR_NO_ROUTE 0x03 #define MSG_ERR_SEND_FAILED 0x04 +#define MSG_ERR_ACCESS_DENIED 0x07 +#define MSG_ERR_PEER_NOT_FOUND 0x08 #define MSG_ERR_ID_TAKEN 0x06 #pragma pack(push, 1) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index afa7df25..ccbf15f6 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -1,21 +1,41 @@ cmake_minimum_required(VERSION 3.16) project(chatgui VERSION 0.1.0 LANGUAGES C CXX) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5 COMPONENTS Widgets Network QUIET) +find_package(Qt5 COMPONENTS Widgets Network Test QUIET) if(NOT Qt5_FOUND) - find_package(Qt6 COMPONENTS Widgets Network REQUIRED) + find_package(Qt6 COMPONENTS Widgets Network Test REQUIRED) set(QT_LIBS Qt6::Widgets Qt6::Network) + set(QT_TEST_LIB Qt6::Test) else() set(QT_LIBS Qt5::Widgets Qt5::Network) + set(QT_TEST_LIB Qt5::Test) endif() find_package(rlottie REQUIRED) find_package(ZLIB REQUIRED) +find_package(OpenSSL REQUIRED) + +set(ZXING_READERS ON CACHE BOOL "") +set(ZXING_WRITERS "OLD" CACHE STRING "") +set(ZXING_ENABLE_QRCODE ON CACHE BOOL "") +set(ZXING_ENABLE_AZTEC OFF CACHE BOOL "") +set(ZXING_ENABLE_DATAMATRIX OFF CACHE BOOL "") +set(ZXING_ENABLE_MAXICODE OFF CACHE BOOL "") +set(ZXING_ENABLE_PDF417 OFF CACHE BOOL "") +set(ZXING_ENABLE_1D OFF CACHE BOOL "") +set(ZXING_C_API OFF CACHE BOOL "") +set(ZXING_EXAMPLES OFF CACHE BOOL "") +set(ZXING_EXAMPLES_QT OFF CACHE BOOL "") +set(ZXING_BLACKBOX_TESTS OFF CACHE BOOL "") +set(ZXING_UNIT_TESTS OFF CACHE BOOL "") +set(BUILD_SHARED_LIBS OFF CACHE BOOL "") +add_subdirectory(zxing-cpp) +set_target_properties(ZXing PROPERTIES AUTOMOC OFF) add_executable(chatgui src/main.cpp @@ -32,7 +52,12 @@ add_executable(chatgui src/inputbar.cpp src/lottieicon.cpp src/animtimer.cpp + src/qrcode_utils.cpp + src/invite_link.cpp + src/invitedialog.cpp transport/msg_client.cpp + transport/crypto.cpp + transport/chat_propagator.cpp db/db_manager.cpp db/sqlite3.c resources/chatgui.qrc @@ -41,4 +66,18 @@ add_executable(chatgui target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/db) target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1) set_source_files_properties(db/sqlite3.c PROPERTIES LANGUAGE C) -target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB pthread dl) +target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing pthread dl) + +# ============================================================================ +# Tests +# ============================================================================ + +enable_testing() + +add_executable(test_crypto + tests/test_crypto.cpp + transport/crypto.cpp +) +target_include_directories(test_crypto PRIVATE ${CMAKE_SOURCE_DIR}) +target_link_libraries(test_crypto PRIVATE Qt5::Core ${QT_TEST_LIB} OpenSSL::Crypto pthread) +add_test(NAME test_crypto COMMAND test_crypto) diff --git a/tools/chatgui/Makefile b/tools/chatgui/Makefile index d9e3d328..7a483cce 100644 --- a/tools/chatgui/Makefile +++ b/tools/chatgui/Makefile @@ -4,7 +4,12 @@ all: @[ -f $(BUILDDIR)/Makefile ] || cmake -S . -B $(BUILDDIR) cmake --build $(BUILDDIR) +check: + @[ -f $(BUILDDIR)/Makefile ] || cmake -S . -B $(BUILDDIR) + cmake --build $(BUILDDIR) --target test_crypto + cd $(BUILDDIR) && ctest --output-on-failure + clean: rm -rf $(BUILDDIR) -.PHONY: all clean +.PHONY: all check clean diff --git a/tools/chatgui/chat_tz.txt b/tools/chatgui/chat_tz.txt index d2c5c42f..0f13037f 100644 --- a/tools/chatgui/chat_tz.txt +++ b/tools/chatgui/chat_tz.txt @@ -1,13 +1,201 @@ -Архитектура чата (группового) -!! пока мысли - не использовать это как готовое тз - - -1. сообщения чата (текст + ссылки на аттачи) хранятся в таблице: -id (primary key), timestamp, user_id, msg (utf8 text 4k max), sign - -2. реакции: key(msg_id, user_id), type, sign -3. аттачи: key(msg_id, user_id), filename, sign - -Структура каталогов: -chats.db - база - +Архитектура чата (группового) + +База данных: SQLite3 (WAL mode), chats.db + +================================================================================ +Схема БД (10 таблиц) +================================================================================ + +-- 1. local_identity — текущий локальный узел (одна строка, приватный ключ здесь) +-- приватный ключ получаем от utun через MSG_RSP_PRIVKEY (только localhost) +CREATE TABLE local_identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + node_id INTEGER NOT NULL UNIQUE, + name TEXT NOT NULL, + x25519_pubkey BLOB NOT NULL, -- 32 bytes + x25519_privkey BLOB, -- 32 bytes (только на этом устройстве) + ed25519_pubkey BLOB, -- 32 bytes (derive из x25519 privkey) + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()) +); + + +-- 2. my_nodes — другие наши ноды (другие устройства, нет приватного ключа) +CREATE TABLE my_nodes ( + node_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + x25519_pubkey BLOB NOT NULL, -- 32 bytes + ed25519_pubkey BLOB, -- 32 bytes + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()) +); + + +-- 3. nodes — известные удалённые узлы (получаем через msg_transport от utun) +CREATE TABLE nodes ( + node_id INTEGER PRIMARY KEY, + name TEXT, + x25519_pubkey BLOB NOT NULL, -- 32 bytes + ed25519_pubkey BLOB, -- 32 bytes (для проверки подписи пира) + is_online INTEGER DEFAULT 0, + last_seen_at INTEGER DEFAULT (unixepoch()), + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()) +); + + +-- 3a. accounts — UI-информация об узлах для отображения сообщений +CREATE TABLE accounts ( + node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id), + display_name TEXT NOT NULL, -- "Alice" + avatar_color TEXT DEFAULT '#4A90E2', -- hex RGB + avatar_letter TEXT NOT NULL, -- "A" + is_contact INTEGER DEFAULT 1, -- показывать в боковой панели + created_at INTEGER DEFAULT (unixepoch()) +); + + +-- 4. channels — каналы (DM и групповые) +CREATE TABLE channels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id TEXT NOT NULL UNIQUE, -- "dm:nodeA_nodeB" (A +#include +#include DbManager::DbManager(const QString& path, QObject* parent) : QObject(parent) @@ -11,106 +14,632 @@ DbManager::DbManager(const QString& path, QObject* parent) m_db = nullptr; return; } - sqlite3_exec(m_db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr); - + sqlite3_exec(m_db, "PRAGMA foreign_keys=ON", nullptr, nullptr, nullptr); + m_myNodeId = 100; initTables(); - - GUI_INFO("Database opened: %s", path.toUtf8().constData()); + if (getChannels().isEmpty()) seedStubData(); + GUI_INFO("Database opened: %s (my_node_id=%llu)", path.toUtf8().constData(), + static_cast(m_myNodeId)); } DbManager::~DbManager() { - if (m_db) { - sqlite3_close(m_db); - m_db = nullptr; - GUI_INFO("Database closed"); + if (m_db) { sqlite3_close(m_db); m_db = nullptr; } +} + +/* ======================================================================== */ + +sqlite3_stmt* DbManager::prepareOrNull(const char* sql) const { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) { + GUI_ERROR("prepare failed: %s — %s", sql, sqlite3_errmsg(m_db)); + return nullptr; } + return stmt; } -void DbManager::initTables() { - sqlite3_exec(m_db, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr); +QString DbManager::colText(sqlite3_stmt* stmt, int col) { + return QString::fromUtf8( + reinterpret_cast(sqlite3_column_text(stmt, col))); +} + +QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) { + int len = sqlite3_column_bytes(stmt, col); + if (len <= 0) return {}; + return QByteArray(static_cast(sqlite3_column_blob(stmt, col)), len); +} + +/* ======================================================================== */ +void DbManager::initTables() { const char* sql = "CREATE TABLE IF NOT EXISTS local_identity (" " id INTEGER PRIMARY KEY CHECK (id = 1)," " node_id INTEGER NOT NULL UNIQUE," " name TEXT NOT NULL," " x25519_pubkey BLOB NOT NULL," + " x25519_privkey BLOB," + " ed25519_pubkey BLOB," + " created_at INTEGER DEFAULT (unixepoch())," + " updated_at INTEGER DEFAULT (unixepoch())" + ");" + + "CREATE TABLE IF NOT EXISTS my_nodes (" + " node_id INTEGER PRIMARY KEY," + " name TEXT NOT NULL," + " x25519_pubkey BLOB NOT NULL," " ed25519_pubkey BLOB," - " created_at TEXT DEFAULT (datetime('now'))," - " updated_at TEXT DEFAULT (datetime('now'))" + " created_at INTEGER DEFAULT (unixepoch())," + " updated_at INTEGER DEFAULT (unixepoch())" ");" "CREATE TABLE IF NOT EXISTS nodes (" " node_id INTEGER PRIMARY KEY," " name TEXT," - " x25519_pubkey BLOB," + " x25519_pubkey BLOB NOT NULL," " ed25519_pubkey BLOB," - " hop_count INTEGER DEFAULT 0," " is_online INTEGER DEFAULT 0," - " last_seen_ver INTEGER DEFAULT 0," - " connectivity_json TEXT," - " created_at TEXT DEFAULT (datetime('now'))," - " updated_at TEXT DEFAULT (datetime('now'))" - ");" - - "CREATE TABLE IF NOT EXISTS node_addresses (" - " id INTEGER PRIMARY KEY AUTOINCREMENT," - " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," - " family INTEGER NOT NULL DEFAULT 4," - " ip BLOB NOT NULL," - " port INTEGER NOT NULL," - " addr_type INTEGER NOT NULL DEFAULT 0," - " socket_type INTEGER DEFAULT 0," - " priority INTEGER DEFAULT 0" + " last_seen_at INTEGER DEFAULT (unixepoch())," + " created_at INTEGER DEFAULT (unixepoch())," + " updated_at INTEGER DEFAULT (unixepoch())" ");" - "CREATE INDEX IF NOT EXISTS idx_node_addr_node ON node_addresses(node_id);" - "CREATE TABLE IF NOT EXISTS node_subnets (" - " id INTEGER PRIMARY KEY AUTOINCREMENT," - " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," - " family INTEGER NOT NULL DEFAULT 4," - " network BLOB NOT NULL," - " prefix_length INTEGER NOT NULL" + "CREATE TABLE IF NOT EXISTS accounts (" + " node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id)," + " display_name TEXT NOT NULL," + " avatar_color TEXT DEFAULT '#4A90E2'," + " avatar_letter TEXT NOT NULL," + " is_contact INTEGER DEFAULT 1," + " created_at INTEGER DEFAULT (unixepoch())" ");" - "CREATE INDEX IF NOT EXISTS idx_subnet_node ON node_subnets(node_id);" "CREATE TABLE IF NOT EXISTS channels (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " channel_id TEXT NOT NULL UNIQUE," " name TEXT NOT NULL," - " member_ids_json TEXT NOT NULL," + " owner_node_id INTEGER," " is_dm INTEGER DEFAULT 0," " last_message TEXT," - " last_msg_at TEXT," - " created_at TEXT DEFAULT (datetime('now'))" + " last_msg_at INTEGER," + " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_cid ON channels(channel_id);" + "CREATE TABLE IF NOT EXISTS channel_members (" + " channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE," + " node_id INTEGER NOT NULL," + " joined_at INTEGER DEFAULT (unixepoch())," + " PRIMARY KEY (channel_id, node_id)" + ");" + "CREATE INDEX IF NOT EXISTS idx_cm_node ON channel_members(node_id);" + "CREATE TABLE IF NOT EXISTS messages (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE," " author_node_id INTEGER NOT NULL," - " content TEXT NOT NULL," + " content TEXT NOT NULL CHECK(length(content) <= 4096)," " content_type TEXT DEFAULT 'text/plain'," " timestamp INTEGER NOT NULL," - " local_seq INTEGER DEFAULT 0," - " signature BLOB," + " signature BLOB NOT NULL CHECK(length(signature) == 64)," " is_outgoing INTEGER DEFAULT 0," " is_read INTEGER DEFAULT 0," - " created_at TEXT DEFAULT (datetime('now'))" + " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE INDEX IF NOT EXISTS idx_msg_channel_time ON messages(channel_id, timestamp);" - "CREATE INDEX IF NOT EXISTS idx_msg_author ON messages(author_node_id);" - "CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_dedup ON messages(channel_id, author_node_id, timestamp);"; + "CREATE INDEX IF NOT EXISTS idx_msg_author ON messages(author_node_id);" + "CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_dedup " + " ON messages(channel_id, author_node_id, timestamp);" - char* errMsg = nullptr; - int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &errMsg); - if (rc != SQLITE_OK) { - GUI_ERROR("Failed to create tables: %s", errMsg); - sqlite3_free(errMsg); - return; + "CREATE TABLE IF NOT EXISTS reactions (" + " message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE," + " node_id INTEGER NOT NULL," + " emoji TEXT NOT NULL," + " signature BLOB NOT NULL CHECK(length(signature) == 64)," + " created_at INTEGER DEFAULT (unixepoch())," + " PRIMARY KEY (message_id, node_id, emoji)" + ");" + + "CREATE TABLE IF NOT EXISTS attachments (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE," + " filename TEXT NOT NULL," + " content_type TEXT NOT NULL," + " size INTEGER NOT NULL," + " sha256 BLOB NOT NULL," + " file_path TEXT," + " signature BLOB NOT NULL CHECK(length(signature) == 64)," + " is_upload INTEGER DEFAULT 0," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + "CREATE INDEX IF NOT EXISTS idx_att_msg ON attachments(message_id);" + + "CREATE TABLE IF NOT EXISTS ui_state (" + " key TEXT PRIMARY KEY," + " value TEXT" + ");" + + "CREATE TABLE IF NOT EXISTS bucket_meta (" + " channel_id TEXT NOT NULL," + " day_unix INTEGER NOT NULL," + " hour INTEGER NOT NULL," + " msg_count INTEGER DEFAULT 0," + " sig_hash BLOB DEFAULT ''," + " PRIMARY KEY (channel_id, day_unix, hour)" + ");" + + "CREATE TABLE IF NOT EXISTS node_addresses (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," + " family INTEGER NOT NULL CHECK(family IN (4, 6))," + " address BLOB NOT NULL," + " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," + " is_nat INTEGER DEFAULT 0," + " created_at INTEGER DEFAULT (unixepoch())," + " updated_at INTEGER DEFAULT (unixepoch())" + ");" + "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);"; + + char* err = nullptr; + if (sqlite3_exec(m_db, sql, nullptr, nullptr, &err) != SQLITE_OK) { + GUI_ERROR("initTables: %s", err); + sqlite3_free(err); + } else { + GUI_INFO("Database tables initialized (11 tables)"); } +} + +/* ======================================================================== */ + +QList DbManager::getChannels() const { + QList list; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at" + " FROM channels c JOIN channel_members cm ON c.channel_id = cm.channel_id" + " WHERE cm.node_id = ? ORDER BY c.last_msg_at DESC"); + if (!stmt) return list; + sqlite3_bind_int64(stmt, 1, static_cast(m_myNodeId)); + while (sqlite3_step(stmt) == SQLITE_ROW) { + ChannelRow r; + r.channelId = colText(stmt, 0); + r.name = colText(stmt, 1); + r.isDm = sqlite3_column_int(stmt, 2) != 0; + r.lastMessage = colText(stmt, 3); + r.lastMsgAt = sqlite3_column_int64(stmt, 4); + list.append(r); + } + sqlite3_finalize(stmt); + return list; +} + +QList DbManager::getMessages(const QString& channelId, int limit) const { + QList list; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT id, author_node_id, content, content_type, timestamp," + " signature, is_outgoing, is_read" + " FROM messages WHERE channel_id = ? ORDER BY timestamp ASC LIMIT ?"); + if (!stmt) return list; + sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, limit); + while (sqlite3_step(stmt) == SQLITE_ROW) { + MessageRow m; + m.id = sqlite3_column_int64(stmt, 0); + m.channelId = channelId; + m.authorNodeId = static_cast(sqlite3_column_int64(stmt, 1)); + m.content = colText(stmt, 2); + m.contentType = colText(stmt, 3); + m.timestamp = sqlite3_column_int64(stmt, 4); + m.signature = colBlob(stmt, 5); + m.isOutgoing = sqlite3_column_int(stmt, 6) != 0; + m.isRead = sqlite3_column_int(stmt, 7) != 0; + list.append(m); + } + sqlite3_finalize(stmt); + return list; +} + +QList DbManager::getAccounts(bool contactsOnly) const { + QList list; + const char* sql = contactsOnly + ? "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact" + " FROM accounts WHERE is_contact = 1 ORDER BY display_name" + : "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact" + " FROM accounts ORDER BY display_name"; + sqlite3_stmt* stmt = prepareOrNull(sql); + if (!stmt) return list; + while (sqlite3_step(stmt) == SQLITE_ROW) { + AccountRow a; + a.nodeId = static_cast(sqlite3_column_int64(stmt, 0)); + a.displayName = colText(stmt, 1); + a.avatarColor = colText(stmt, 2); + a.avatarLetter = colText(stmt, 3); + a.isContact = sqlite3_column_int(stmt, 4) != 0; + list.append(a); + } + sqlite3_finalize(stmt); + return list; +} + +AccountRow DbManager::getAccount(quint64 nodeId) const { + AccountRow a; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact" + " FROM accounts WHERE node_id = ?"); + if (!stmt) return a; + sqlite3_bind_int64(stmt, 1, static_cast(nodeId)); + if (sqlite3_step(stmt) == SQLITE_ROW) { + a.nodeId = static_cast(sqlite3_column_int64(stmt, 0)); + a.displayName = colText(stmt, 1); + a.avatarColor = colText(stmt, 2); + a.avatarLetter = colText(stmt, 3); + a.isContact = sqlite3_column_int(stmt, 4) != 0; + } + sqlite3_finalize(stmt); + return a; +} + +QByteArray DbManager::getNodeEdPub(quint64 nodeId) const { + sqlite3_stmt* stmt = prepareOrNull( + "SELECT ed25519_pubkey FROM nodes WHERE node_id=?"); + if (!stmt) return {}; + sqlite3_bind_int64(stmt, 1, static_cast(nodeId)); + QByteArray r; + if (sqlite3_step(stmt) == SQLITE_ROW) + r = colBlob(stmt, 0); + sqlite3_finalize(stmt); + return r; +} + +QList DbManager::getBucketMeta(const QString& channelId) const { + QList list; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT day_unix, hour, msg_count, sig_hash" + " FROM bucket_meta WHERE channel_id=? ORDER BY day_unix, hour"); + if (!stmt) return list; + sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC); + while (sqlite3_step(stmt) == SQLITE_ROW) { + BucketMeta b; + b.day = sqlite3_column_int64(stmt, 0); + b.hour = sqlite3_column_int(stmt, 1); + b.count = sqlite3_column_int(stmt, 2); + b.hash = colBlob(stmt, 3); + list.append(b); + } + sqlite3_finalize(stmt); + return list; +} + +void DbManager::setBucketMeta(const QString& channelId, qint64 day, int hour, + int count, const QByteArray& hash) { + sqlite3_stmt* stmt = prepareOrNull( + "INSERT OR REPLACE INTO bucket_meta(channel_id,day_unix,hour,msg_count,sig_hash)" + " VALUES(?,?,?,?,?)"); + if (!stmt) return; + sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, day); + sqlite3_bind_int(stmt, 3, hour); + sqlite3_bind_int(stmt, 4, count); + sqlite3_bind_blob(stmt, 5, hash.constData(), hash.size(), SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + +QList DbManager::getMessageSigs(const QString& channelId, + qint64 day, int hour) const { + QList sigs; + qint64 dayStart = day + hour * 3600; + qint64 dayEnd = dayStart + 3600; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT signature FROM messages WHERE channel_id=?" + " AND timestamp/1000 >= ? AND timestamp/1000 < ? ORDER BY timestamp"); + if (!stmt) return sigs; + sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, dayStart); + sqlite3_bind_int64(stmt, 3, dayEnd); + while (sqlite3_step(stmt) == SQLITE_ROW) + sigs.append(colBlob(stmt, 0)); + sqlite3_finalize(stmt); + return sigs; +} + +QList DbManager::getMessagesBySigs(const QList& sigs) const { + QList msgs; + if (sigs.isEmpty()) return msgs; + QString sql = "SELECT id, channel_id, author_node_id, content, content_type," + " timestamp, signature, is_outgoing, is_read FROM messages" + " WHERE signature IN ("; + for (int i = 0; i < sigs.size(); i++) + sql += (i > 0 ? ",?" : "?"); + sql += ") ORDER BY timestamp"; + sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); + if (!stmt) return msgs; + for (int i = 0; i < sigs.size(); i++) + sqlite3_bind_blob(stmt, i + 1, sigs[i].constData(), sigs[i].size(), SQLITE_STATIC); + while (sqlite3_step(stmt) == SQLITE_ROW) { + MessageRow m; + m.id = sqlite3_column_int64(stmt, 0); + m.channelId = colText(stmt, 1); + m.authorNodeId = static_cast(sqlite3_column_int64(stmt, 2)); + m.content = colText(stmt, 3); + m.contentType = colText(stmt, 4); + m.timestamp = sqlite3_column_int64(stmt, 5); + m.signature = colBlob(stmt, 6); + m.isOutgoing = sqlite3_column_int(stmt, 7) != 0; + m.isRead = sqlite3_column_int(stmt, 8) != 0; + msgs.append(m); + } + sqlite3_finalize(stmt); + return msgs; +} + +QList DbManager::getOnlinePeers() const { + QList peers; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT node_id FROM nodes WHERE is_online=1 AND node_id != ?"); + if (!stmt) return peers; + sqlite3_bind_int64(stmt, 1, static_cast(m_myNodeId)); + while (sqlite3_step(stmt) == SQLITE_ROW) + peers.append(static_cast(sqlite3_column_int64(stmt, 0))); + sqlite3_finalize(stmt); + return peers; +} + +bool DbManager::insertMessage(const QString& channelId, quint64 authorNodeId, + const QString& content, qint64 timestamp, int isOutgoing) { + static const unsigned char sig64[64] = {}; + sqlite3_stmt* stmt = prepareOrNull( + "INSERT INTO messages" + " (channel_id,author_node_id,content,timestamp,signature,is_outgoing,is_read)" + " VALUES(?,?,?,?,?,?,1)"); + if (!stmt) return false; + sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, static_cast(authorNodeId)); + sqlite3_bind_text(stmt, 3, content.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 4, timestamp); + sqlite3_bind_blob(stmt, 5, sig64, 64, SQLITE_STATIC); + sqlite3_bind_int(stmt, 6, isOutgoing); + bool ok = (sqlite3_step(stmt) == SQLITE_DONE); + sqlite3_finalize(stmt); + + /* update bucket_meta */ + qint64 day = timestamp / 86400000; /* ms per day */ + int hour = static_cast((timestamp % 86400000) / 3600000); + auto sigs = getMessageSigs(channelId, day, hour); + int cnt = sigs.size(); + QByteArray hash(32, '\0'); + if (cnt > 0) { + EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + EVP_DigestInit_ex(md_ctx, EVP_sha256(), NULL); + for (auto& s : sigs) + EVP_DigestUpdate(md_ctx, s.constData(), s.size()); + unsigned int len = SHA256_DIGEST_LENGTH; + uint8_t buf[SHA256_DIGEST_LENGTH]; + EVP_DigestFinal_ex(md_ctx, buf, &len); + EVP_MD_CTX_free(md_ctx); + hash = QByteArray(reinterpret_cast(buf), static_cast(len)); + } + setBucketMeta(channelId, day, hour, cnt, hash); + return ok; +} + +QByteArray DbManager::getLocalPubkey() const { + sqlite3_stmt* stmt = prepareOrNull( + "SELECT x25519_pubkey FROM local_identity WHERE id=1"); + if (!stmt) return {}; + QByteArray r; + if (sqlite3_step(stmt) == SQLITE_ROW) + r = colBlob(stmt, 0); + sqlite3_finalize(stmt); + return r; +} + +QList DbManager::getOnlinePeerAddresses(quint64 excludeNodeId) const { + QList list; + sqlite3_stmt* stmt = prepareOrNull( + "SELECT na.node_id, na.family, na.address, na.port" + " FROM node_addresses na JOIN nodes n ON na.node_id = n.node_id" + " WHERE n.is_online=1 AND na.is_nat=0 AND na.node_id != ?" + " ORDER BY RANDOM()"); + if (!stmt) return list; + sqlite3_bind_int64(stmt, 1, static_cast(excludeNodeId)); + while (sqlite3_step(stmt) == SQLITE_ROW) { + NodeAddr a; + a.nodeId = static_cast(sqlite3_column_int64(stmt, 0)); + a.family = sqlite3_column_int(stmt, 1); + a.address = colBlob(stmt, 2); + a.port = static_cast(sqlite3_column_int(stmt, 3)); + list.append(a); + } + sqlite3_finalize(stmt); + return list; +} + +/* ======================================================================== */ + +static constexpr unsigned char kDummySig[64] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, + 0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, + 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27, + 0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F, + 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37, + 0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F +}; + +void DbManager::seedStubData() { + GUI_INFO("Seeding stub data..."); + sqlite3_exec(m_db, "BEGIN", nullptr, nullptr, nullptr); + + /* my_node dummy pubkey */ + unsigned char pub_dummy[32] = {}; + sqlite3_stmt* ins = prepareOrNull( + "INSERT OR IGNORE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey)" + " VALUES(1,?,'MyDevice',?,?)"); + sqlite3_bind_int64(ins, 1, m_myNodeId); + sqlite3_bind_blob(ins, 2, pub_dummy, 32, SQLITE_STATIC); + sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC); + sqlite3_step(ins); sqlite3_finalize(ins); + + /* nodes + accounts */ + struct { quint64 id; const char* name; const char* letter; const char* color; } accs[] = { + {1, "Alice", "A", "#4A90E2"}, {2, "Bob", "B", "#7ED321"}, + {3, "Carol", "C", "#F5A623"}, {4, "Dave", "D", "#9013FE"}, + {5, "Eve", "E", "#00B894"}, + }; + for (auto& a : accs) { + ins = prepareOrNull("INSERT OR IGNORE INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey)" + " VALUES(?,?,?,?)"); + sqlite3_bind_int64(ins, 1, static_cast(a.id)); + sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_STATIC); + sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC); + sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_STATIC); + sqlite3_step(ins); sqlite3_finalize(ins); + + ins = prepareOrNull("INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter)" + " VALUES(?,?,?,?)"); + sqlite3_bind_int64(ins, 1, static_cast(a.id)); + sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_STATIC); + sqlite3_bind_text(ins, 3, a.color, -1, SQLITE_STATIC); + sqlite3_bind_text(ins, 4, a.letter, -1, SQLITE_STATIC); + sqlite3_step(ins); sqlite3_finalize(ins); + } + + /* set some nodes online for demo */ + sqlite3_exec(m_db, + "UPDATE nodes SET is_online=1 WHERE node_id IN (1,2,3)", nullptr, nullptr, nullptr); + + /* node addresses for online peers (stub, for invite link demo) */ + struct { quint64 id; int fam; unsigned char addr[16]; int port; int nat; } addrs[] = { + {1, 4, {192,168,1,10}, 8001, 0}, + {1, 6, {0xfd,0x12,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, 8001, 0}, + {2, 4, {10,0,0,5}, 9001, 0}, + {2, 4, {172,16,0,100}, 9001, 0}, + {3, 4, {192,168,5,20}, 10001, 1}, /* Carol behind NAT */ + }; + for (auto& ad : addrs) { + ins = prepareOrNull("INSERT OR IGNORE INTO node_addresses(node_id,family,address,port,is_nat)" + " VALUES(?,?,?,?,?)"); + sqlite3_bind_int64(ins, 1, static_cast(ad.id)); + sqlite3_bind_int(ins, 2, ad.fam); + sqlite3_bind_blob(ins, 3, ad.addr, ad.fam == 4 ? 4 : 16, SQLITE_STATIC); + sqlite3_bind_int(ins, 4, ad.port); + sqlite3_bind_int(ins, 5, ad.nat); + sqlite3_step(ins); sqlite3_finalize(ins); + } + + /* channels (5) */ + struct { const char* cid; const char* name; } chs[] = { + {"ch:general", "#general"}, + {"ch:random", "#random"}, + {"ch:dev-team", "#dev-team"}, + {"ch:announce", "#announcements"}, + {"ch:offtopic", "#offtopic"}, + }; + for (auto& ch : chs) { + ins = prepareOrNull("INSERT OR IGNORE INTO channels(channel_id,name) VALUES(?,?)"); + sqlite3_bind_text(ins, 1, ch.cid, -1, SQLITE_STATIC); + sqlite3_bind_text(ins, 2, ch.name, -1, SQLITE_STATIC); + sqlite3_step(ins); sqlite3_finalize(ins); + + /* my node member */ + ins = prepareOrNull("INSERT OR IGNORE INTO channel_members(channel_id,node_id) VALUES(?,?)"); + sqlite3_bind_text(ins, 1, ch.cid, -1, SQLITE_STATIC); + sqlite3_bind_int64(ins, 2, static_cast(m_myNodeId)); + sqlite3_step(ins); sqlite3_finalize(ins); + + /* all 5 accounts as members (everyone in every channel for demo) */ + for (auto& a : accs) { + ins = prepareOrNull("INSERT OR IGNORE INTO channel_members(channel_id,node_id) VALUES(?,?)"); + sqlite3_bind_text(ins, 1, ch.cid, -1, SQLITE_STATIC); + sqlite3_bind_int64(ins, 2, static_cast(a.id)); + sqlite3_step(ins); sqlite3_finalize(ins); + } + } + + /* messages (8) */ + struct { const char* cid; quint64 author; qint64 ts; const char* text; quint8 o; } msgs[] = { + {"ch:general", 1, 1719200100000, "Hey team! How's the deployment going?", 0}, + {"ch:general", 2, 1719200220000, "All green on staging. Running the final integration tests now.", 0}, + {"ch:general", 3, 1719200520000, "I've pushed the ETCP-42 fix to staging. BGP handshake should be stable now. Please review the patch when you get a chance.", 0}, + {"ch:general", 4, 1719200700000, "Wait — the BGP session is still flapping after the handshake. I'm seeing the keepalive timer fire but no route updates come through. Logs attached.", 0}, + {"ch:general", 1, 1719200880000, "Reviewed the patch. LGTM. Let's merge and deploy to production tomorrow morning.", 0}, + {"ch:general", 2, 1719201000000, "Can you share the full logs for the BGP session? I'll dig into the keepalive timer logic.", 0}, + {"ch:general", 3, 1719201180000, "Sounds good. Let's coordinate in #dev-team. I'll prepare the release notes.", 0}, + {"ch:general", 1, 1719201300000, "Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main.", 0}, + }; + for (auto& m : msgs) { + ins = prepareOrNull( + "INSERT OR IGNORE INTO messages" + " (channel_id,author_node_id,content,timestamp,signature,is_outgoing,is_read)" + " VALUES(?,?,?,?,?,?,1)"); + sqlite3_bind_text(ins, 1, m.cid, -1, SQLITE_STATIC); + sqlite3_bind_int64(ins, 2, static_cast(m.author)); + sqlite3_bind_text(ins, 3, m.text, -1, SQLITE_STATIC); + sqlite3_bind_int64(ins, 4, m.ts); + sqlite3_bind_blob(ins, 5, kDummySig, 64, SQLITE_STATIC); + sqlite3_bind_int(ins, 6, m.o); + sqlite3_step(ins); sqlite3_finalize(ins); + } + + /* reactions */ + struct { qint64 msgRowId; quint64 node; const char* emoji; } reacts[] = { + {2, 1, "\xF0\x9F\x91\x8D"}, {2, 3, "\xF0\x9F\x91\x8D"}, + {2, 1, "\xF0\x9F\x91\x8B"}, {2, 3, "\xF0\x9F\x91\x8B"}, {2, 4, "\xF0\x9F\x91\x8B"}, + {3, 1, "\xF0\x9F\x91\x8D"}, {3, 2, "\xF0\x9F\x91\x8D"}, {3, 4, "\xF0\x9F\x91\x8D"}, + {3, 1, "\xE2\x9D\xA4"}, + {4, 2, "\xF0\x9F\x98\xAE"}, + {5, 1, "\xF0\x9F\x91\x8D"}, {5, 2, "\xF0\x9F\x91\x8D"}, {5, 3, "\xF0\x9F\x91\x8D"}, + {5, 4, "\xF0\x9F\x91\x8D"}, {5, 5, "\xF0\x9F\x91\x8D"}, + {5, 2, "\xF0\x9F\x8E\x89"}, {5, 5, "\xF0\x9F\x8E\x89"}, + {7, 1, "\xF0\x9F\xA4\x96"}, {7, 2, "\xF0\x9F\xA4\x96"}, + {8, 2, "\xF0\x9F\x9A\x80"}, {8, 3, "\xF0\x9F\x9A\x80"}, {8, 4, "\xF0\x9F\x9A\x80"}, + {8, 1, "\xE2\x9D\xA4"}, {8, 2, "\xE2\x9D\xA4"}, {8, 3, "\xE2\x9D\xA4"}, + {8, 4, "\xE2\x9D\xA4"}, {8, 5, "\xE2\x9D\xA4"}, {8, 1, "\xE2\x9D\xA4"}, {8, 1, "\xE2\x9D\xA4"}, + }; + std::string lastMsg; + qint64 lastTs = 0; + for (auto& r : reacts) { + ins = prepareOrNull( + "INSERT OR IGNORE INTO reactions(message_id,node_id,emoji,signature)" + " VALUES(?,?,?,?)"); + sqlite3_bind_int64(ins, 1, r.msgRowId); + sqlite3_bind_int64(ins, 2, static_cast(r.node)); + sqlite3_bind_text(ins, 3, r.emoji, -1, SQLITE_STATIC); + sqlite3_bind_blob(ins, 4, kDummySig, 64, SQLITE_STATIC); + sqlite3_step(ins); sqlite3_finalize(ins); + } + lastMsg = "Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main."; + lastTs = 1719201300000; + + /* update channel last_msg */ + ins = prepareOrNull("UPDATE channels SET last_message=?, last_msg_at=? WHERE channel_id='ch:general'"); + sqlite3_bind_text(ins, 1, lastMsg.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_int64(ins, 2, lastTs); + sqlite3_step(ins); sqlite3_finalize(ins); + + sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr); + GUI_INFO("Seed data inserted (5 accounts, 5 channels, 8 messages, %d reactions, %d addresses)", + static_cast(sizeof(reacts)/sizeof(reacts[0])), + static_cast(sizeof(addrs)/sizeof(addrs[0]))); +} + +QString DbManager::getUiState(const QString& key) const { + sqlite3_stmt* stmt = prepareOrNull("SELECT value FROM ui_state WHERE key=?"); + if (!stmt) return {}; + sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_STATIC); + QString val; + if (sqlite3_step(stmt) == SQLITE_ROW) + val = colText(stmt, 0); + sqlite3_finalize(stmt); + return val; +} - GUI_INFO("Database tables initialized (6 tables, 6 indexes)"); +void DbManager::setUiState(const QString& key, const QString& value) { + sqlite3_stmt* stmt = prepareOrNull( + "INSERT OR REPLACE INTO ui_state(key,value) VALUES(?,?)"); + if (!stmt) return; + sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, value.toUtf8().constData(), -1, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); } diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index a1c69e2a..89bbec0f 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -3,7 +3,44 @@ #include #include +#include #include +#include + +struct ChannelRow { + QString channelId; + QString name; + bool isDm = false; + QString lastMessage; + qint64 lastMsgAt = 0; +}; + +struct MessageRow { + qint64 id = 0; + QString channelId; + quint64 authorNodeId = 0; + QString content; + QString contentType; + qint64 timestamp = 0; + QByteArray signature; + bool isOutgoing = false; + bool isRead = false; +}; + +struct AccountRow { + quint64 nodeId = 0; + QString displayName; + QString avatarColor; + QString avatarLetter; + bool isContact = true; +}; + +struct NodeAddr { + quint64 nodeId = 0; + int family = 0; // 4=IPv4, 6=IPv6 + QByteArray address; + quint16 port = 0; +}; class DbManager : public QObject { Q_OBJECT @@ -12,12 +49,40 @@ public: ~DbManager() override; bool isOpen() const { return m_db != nullptr; } - sqlite3* handle() { return m_db; } + quint64 myNodeId() const { return m_myNodeId; } + + QList getChannels() const; + QList getMessages(const QString& channelId, int limit = 50) const; + QList getAccounts(bool contactsOnly = false) const; + AccountRow getAccount(quint64 nodeId) const; + QByteArray getNodeEdPub(quint64 nodeId) const; + + struct BucketMeta { qint64 day; int hour; int count; QByteArray hash; }; + QList getBucketMeta(const QString& channelId) const; + void setBucketMeta(const QString& channelId, qint64 day, int hour, + int count, const QByteArray& hash); + QList getMessageSigs(const QString& channelId, qint64 day, int hour) const; + QList getMessagesBySigs(const QList& sigs) const; + QList getOnlinePeers() const; + QByteArray getLocalPubkey() const; + QList getOnlinePeerAddresses(quint64 excludeNodeId) const; + + void seedStubData(); + + bool insertMessage(const QString& channelId, quint64 authorNodeId, + const QString& content, qint64 timestamp, int isOutgoing); + + QString getUiState(const QString& key) const; + void setUiState(const QString& key, const QString& value); private: void initTables(); + sqlite3_stmt* prepareOrNull(const char* sql) const; + static QString colText(sqlite3_stmt* stmt, int col); + static QByteArray colBlob(sqlite3_stmt* stmt, int col); sqlite3* m_db = nullptr; + quint64 m_myNodeId = 0; }; #endif diff --git a/tools/chatgui/doc/db_schema.md b/tools/chatgui/doc/db_schema.md index ca678a03..adfc8bac 100644 --- a/tools/chatgui/doc/db_schema.md +++ b/tools/chatgui/doc/db_schema.md @@ -1,5 +1,8 @@ # База данных децентрализованного чата (chatgui) +Сторона сервиса (gui): +- + ## Архитектура хранения Чат **децентрализованный** — нет центрального сервера. Каждый узел (нода uTun) хранит свою @@ -14,24 +17,32 @@ ## Структура таблиц SQLite -### `local_identity` — наш собственный узел (точнее все мои устройства) +### `local_identity` — наш собственный узел + + +### `my_nodes` — все мои устройства) между моими устройствами синхронизируется контент - список чатов и сообщения. +... todo + ```sql CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY, node_id INTEGER NOT NULL UNIQUE, // id из utun name TEXT NOT NULL, // ник + pubkey (ed+x) last_seen INTEGER DEFAULT 0, - online_rating INTEGER DEFAULT 0, // рейттинг связности с узлом. чем больше число тем чаще узел онлайн и с ним хорошая связь + online_rating INTEGER DEFAULT 0, // рейтинг доступности узла. чем больше число тем чаще узел онлайн speed_rating INTEGER DEFAULT 0, // рейтинг скорости обмена с узлом rtt_rating INTEGER DEFAULT 0, // rtt до узла (через промежуточные узлы если есть) - connectivity_json TEXT, // как связаться с узлом + connectivity_json TEXT, // как связаться с узлом - адреса, (пока пусто) parent_node_id INTEGER DEFAULT NULL, // каждая нода должна иметь родителя (кроме корневой) - main_node_id INTEGER DEFAULT NULL, // если несколько устройств у клиента, id главной ноды + main_node_id INTEGER DEFAULT NULL, // если несколько устройств у клиента, id следующей ноды (циклический список) created_at TEXT DEFAULT (datetime('now')), ); ``` + + ### `channels` — чаты (группы узлов) ```sql diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 686ff7d3..0569aaa4 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -1,4 +1,5 @@ #include "accountlist.h" +#include "../db/db_manager.h" #include #include #include @@ -22,10 +23,11 @@ static QPixmap makeAvatar(const QColor &color, const QString &letter) { return pix; } -AccountList::AccountList(QWidget *parent) +AccountList::AccountList(DbManager* db, QWidget *parent) : QWidget(parent) , m_listView(new QListView(this)) , m_model(new QStandardItemModel(this)) + , m_db(db) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -43,27 +45,17 @@ AccountList::AccountList(QWidget *parent) m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); layout->addWidget(m_listView); - - populateStubData(); } -void AccountList::populateStubData() { - struct { - const char *name; - QColor color; - const char *letter; - } accounts[] = { - {"Alice", QColor(74, 144, 226), "A"}, - {"Bob", QColor(126, 211, 33), "B"}, - {"Carol", QColor(245, 166, 35), "C"}, - {"Dave", QColor(144, 19, 254), "D"}, - {"Eve", QColor(0, 184, 148), "E"}, - }; +void AccountList::loadAccounts() { + m_model->clear(); + if (!m_db || !m_db->isOpen()) return; - for (const auto &a : accounts) { + auto accounts = m_db->getAccounts(true); + for (const auto& a : accounts) { auto *item = new QStandardItem(); - item->setText(a.name); - item->setIcon(QIcon(makeAvatar(a.color, a.letter))); + item->setText(a.displayName); + item->setIcon(QIcon(makeAvatar(QColor(a.avatarColor), a.avatarLetter))); m_model->appendRow(item); } } diff --git a/tools/chatgui/src/accountlist.h b/tools/chatgui/src/accountlist.h index 7fc1cfc0..9b5600e3 100644 --- a/tools/chatgui/src/accountlist.h +++ b/tools/chatgui/src/accountlist.h @@ -4,13 +4,17 @@ #include #include +class DbManager; + class AccountList : public QWidget { Q_OBJECT public: - explicit AccountList(QWidget *parent = nullptr); + explicit AccountList(DbManager* db = nullptr, QWidget *parent = nullptr); + + void loadAccounts(); private: - QListView *m_listView; + QListView *m_listView; QStandardItemModel *m_model; - void populateStubData(); + DbManager *m_db; }; diff --git a/tools/chatgui/src/channeldelegate.h b/tools/chatgui/src/channeldelegate.h index 8bfa7324..436545a3 100644 --- a/tools/chatgui/src/channeldelegate.h +++ b/tools/chatgui/src/channeldelegate.h @@ -3,9 +3,10 @@ #include enum ChannelDataRole { - ChannelNameRole = Qt::DisplayRole, - ChannelIconRole = Qt::DecorationRole, - ChannelLastMessageRole = Qt::UserRole + 1 + ChannelNameRole = Qt::DisplayRole, + ChannelIconRole = Qt::DecorationRole, + ChannelLastMessageRole = Qt::UserRole + 1, + ChannelChannelIdRole = Qt::UserRole + 2 }; class ChannelDelegate : public QStyledItemDelegate { diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 7885b1da..9425c4d0 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -1,9 +1,11 @@ #include "channellist.h" #include "channeldelegate.h" +#include "../db/db_manager.h" #include #include #include #include +#include static QIcon makeChannelIcon(const QColor &color, const QString &letter) { QPixmap pix(32, 32); @@ -23,10 +25,11 @@ static QIcon makeChannelIcon(const QColor &color, const QString &letter) { return QIcon(pix); } -ChannelList::ChannelList(QWidget *parent) +ChannelList::ChannelList(DbManager* db, QWidget *parent) : QWidget(parent) , m_listView(new QListView(this)) , m_model(new QStandardItemModel(this)) + , m_db(db) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -43,40 +46,62 @@ ChannelList::ChannelList(QWidget *parent) m_listView->setSelectionMode(QAbstractItemView::SingleSelection); m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_listView->setContextMenuPolicy(Qt::CustomContextMenu); layout->addWidget(m_listView); connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex ¤t, const QModelIndex &) { if (current.isValid()) { - emit channelSelected(current.row()); + QString cid = current.data(ChannelChannelIdRole).toString(); + emit channelSelected(cid); } }); - - populateStubData(); + connect(m_listView, &QListView::customContextMenuRequested, + this, &ChannelList::onContextMenu); } -void ChannelList::populateStubData() { - struct { - const char *name; - const char *lastMsg; - QColor color; - const char *letter; - } channels[] = { - {"#general", "Alice: Sounds good, let's deploy tomorrow morning.", QColor(74, 144, 226), "G"}, - {"#random", "Bob: Anyone seen the latest episode? It was amazing!", QColor(126, 211, 33), "R"}, - {"#dev-team", "Carol: I've pushed the fix for ETCP-42 to staging.", QColor(245, 166, 35), "D"}, - {"#announcements","Admin: Maintenance scheduled for Sunday 02:00 UTC.", QColor(208, 2, 27), "A"}, - {"#support", "Dave: Can someone help me configure the routing table? The BGP session keeps dropping after the handshake completes. I've checked the logs.", QColor(144, 19, 254), "S"}, - {"#offtopic", "Eve: Check out this new keyboard — Cherry MX Blue, 60%.", QColor(0, 184, 148), "O"}, +void ChannelList::loadChannels() { + m_model->clear(); + if (!m_db || !m_db->isOpen()) return; + + auto channels = m_db->getChannels(); + static const QColor colors[] = { + QColor(74,144,226), QColor(126,211,33), QColor(245,166,35), + QColor(208,2,27), QColor(144,19,254), QColor(0,184,148) }; + int ci = 0; - for (const auto &ch : channels) { + for (const auto& ch : channels) { + auto letter = ch.name.mid(0, 1).toUpper(); + if (letter.isEmpty()) letter = "#"; auto *item = new QStandardItem(); - item->setData(ch.name, ChannelNameRole); - item->setData(QString::fromUtf8(ch.lastMsg), ChannelLastMessageRole); - item->setData(makeChannelIcon(ch.color, ch.letter), ChannelIconRole); + item->setData(ch.name, ChannelNameRole); + item->setData(ch.channelId, ChannelChannelIdRole); + item->setData(ch.lastMessage, ChannelLastMessageRole); + item->setData(makeChannelIcon(colors[ci % 6], letter), ChannelIconRole); m_model->appendRow(item); + ci++; } - m_listView->setCurrentIndex(m_model->index(0, 0)); + if (m_model->rowCount() > 0) + m_listView->setCurrentIndex(m_model->index(0, 0)); +} + +void ChannelList::selectChannel(const QString& channelId) { + for (int row = 0; row < m_model->rowCount(); row++) { + if (m_model->item(row)->data(ChannelChannelIdRole).toString() == channelId) { + m_listView->setCurrentIndex(m_model->index(row, 0)); + return; + } + } +} + +void ChannelList::onContextMenu(const QPoint& pos) { + QModelIndex index = m_listView->indexAt(pos); + if (!index.isValid()) return; + QString cid = index.data(ChannelChannelIdRole).toString(); + QMenu menu(this); + QAction *inviteAction = menu.addAction("Пригласить"); + if (menu.exec(m_listView->viewport()->mapToGlobal(pos)) == inviteAction) + emit inviteRequested(cid); } diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index 6f81f9eb..6e4cfa2c 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -3,17 +3,27 @@ #include #include #include +#include + +class DbManager; class ChannelList : public QWidget { Q_OBJECT public: - explicit ChannelList(QWidget *parent = nullptr); + explicit ChannelList(DbManager* db = nullptr, QWidget *parent = nullptr); + + void loadChannels(); + void selectChannel(const QString& channelId); signals: - void channelSelected(int index); + void channelSelected(const QString& channelId); + void inviteRequested(const QString& channelId); + +private slots: + void onContextMenu(const QPoint& pos); private: - QListView *m_listView; + QListView *m_listView; QStandardItemModel *m_model; - void populateStubData(); + DbManager *m_db; }; diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index 218e29f0..1e5040f4 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,16 @@ static QPushButton *makeIconBtn(const QString &text, const QString &tooltip, QWi return btn; } +static QWidget *wrapBottomBtn(QPushButton *btn, int pad) { + auto *w = new QWidget(btn->parentWidget()); + auto *l = new QVBoxLayout(w); + l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + l->addWidget(btn); + l->addSpacing(pad); + return w; +} + InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent) : QWidget(parent) , m_input(new QTextEdit(this)) @@ -41,7 +52,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent) qDebug() << "Attached:" << path; } }); - layout->addWidget(attachBtn, 0, Qt::AlignBottom); + layout->addWidget(wrapBottomBtn(attachBtn, 3), 0, Qt::AlignBottom); m_input->setAcceptRichText(false); m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -58,7 +69,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent) m_emojiBtn = makeIconBtn(QString::fromUtf8("\xF0\x9F\x99\x82"), QString::fromUtf8("Смайлики"), this); - layout->addWidget(m_emojiBtn, 0, Qt::AlignBottom); + layout->addWidget(wrapBottomBtn(m_emojiBtn, 3), 0, Qt::AlignBottom); auto *sendBtn = makeIconBtn(QString::fromUtf8("\xE2\x9E\xA4"), QString::fromUtf8("Отправить"), this); diff --git a/tools/chatgui/src/invite_link.cpp b/tools/chatgui/src/invite_link.cpp new file mode 100644 index 00000000..ba106313 --- /dev/null +++ b/tools/chatgui/src/invite_link.cpp @@ -0,0 +1,100 @@ +#include "invite_link.h" +#include "debug_ui.h" +#include + +static QByteArray encodeAddresses(const QList& addrs) { + QByteArray buf; + for (const auto& a : addrs) { + buf.append(static_cast(a.family)); + buf.append(a.address); + buf.append(static_cast((a.port >> 8) & 0xFF)); + buf.append(static_cast(a.port & 0xFF)); + } + return buf; +} + +static QList decodeAddresses(const uint8_t* data, int len, + int count) { + QList list; + int off = 0; + for (int i = 0; i < count && off < len; i++) { + if (off + 1 > len) break; + InviteAddr a; + a.family = data[off++]; + int alen = (a.family == 4) ? 4 : 16; + if (off + alen + 2 > len) break; + a.address = QByteArray(reinterpret_cast(data + off), alen); + off += alen; + a.port = (static_cast(data[off]) << 8) | data[off + 1]; + off += 2; + list.append(a); + } + return list; +} + +QString encodeInviteLink(const QString& channelId, const QByteArray& pubkey, + const QList& addrs) { + QByteArray cid = channelId.toUtf8(); + if (cid.size() > 65535) { + GUI_ERROR("encodeInviteLink: channelId too long (%d bytes)", cid.size()); + return {}; + } + if (pubkey.size() != 32) { + GUI_ERROR("encodeInviteLink: pubkey must be 32 bytes (got %d)", pubkey.size()); + return {}; + } + + QByteArray addrsEnc = encodeAddresses(addrs); + int addrCount = addrs.size(); + if (addrCount > 65535) addrCount = 65535; + + QByteArray blob; + blob.reserve(1 + 2 + cid.size() + 32 + 2 + addrsEnc.size()); + blob.append(static_cast(INVITE_LINK_VERSION)); + blob.append(static_cast((cid.size() >> 8) & 0xFF)); + blob.append(static_cast(cid.size() & 0xFF)); + blob.append(cid); + blob.append(pubkey); + blob.append(static_cast((addrCount >> 8) & 0xFF)); + blob.append(static_cast(addrCount & 0xFF)); + blob.append(addrsEnc); + + return QString::fromLatin1("utun://") + QString::fromLatin1(blob.toBase64()); +} + +InviteData decodeInviteLink(const QString& link) { + InviteData d; + if (!link.startsWith(QLatin1String("utun://"))) { + GUI_ERROR("decodeInviteLink: invalid prefix"); + return d; + } + QByteArray blob = QByteArray::fromBase64( + link.mid(7).toLatin1()); + const auto* data = reinterpret_cast(blob.constData()); + int len = blob.size(); + + if (len < 5) { + GUI_ERROR("decodeInviteLink: blob too short (%d bytes)", len); + return d; + } + int off = 0; + uint8_t ver = data[off++]; + if (ver != INVITE_LINK_VERSION) { + GUI_ERROR("decodeInviteLink: unsupported version %d", ver); + return d; + } + int cidLen = (static_cast(data[off]) << 8) | data[off + 1]; + off += 2; + if (off + cidLen + 32 + 2 > len) { + GUI_ERROR("decodeInviteLink: truncated blob"); + return d; + } + d.channelId = QString::fromUtf8(reinterpret_cast(data + off), cidLen); + off += cidLen; + d.pubkey = QByteArray(reinterpret_cast(data + off), 32); + off += 32; + int addrCount = (static_cast(data[off]) << 8) | data[off + 1]; + off += 2; + d.addrs = decodeAddresses(data + off, len - off, addrCount); + return d; +} diff --git a/tools/chatgui/src/invite_link.h b/tools/chatgui/src/invite_link.h new file mode 100644 index 00000000..e2893d03 --- /dev/null +++ b/tools/chatgui/src/invite_link.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include "../db/db_manager.h" + +#define INVITE_LINK_VERSION 0x01 + +struct InviteAddr { + int family; // 4=IPv4, 6=IPv6 + QByteArray address; + quint16 port; +}; + +struct InviteData { + QString channelId; + QByteArray pubkey; + QList addrs; +}; + +QString encodeInviteLink(const QString& channelId, const QByteArray& pubkey, + const QList& addrs); + +InviteData decodeInviteLink(const QString& link); diff --git a/tools/chatgui/src/invitedialog.cpp b/tools/chatgui/src/invitedialog.cpp new file mode 100644 index 00000000..9c2fbcfc --- /dev/null +++ b/tools/chatgui/src/invitedialog.cpp @@ -0,0 +1,47 @@ +#include "invitedialog.h" +#include "qrcode_utils.h" +#include +#include +#include +#include +#include +#include +#include + +InviteDialog::InviteDialog(const QString& link, QWidget* parent) + : QDialog(parent), m_link(link) +{ + setWindowTitle("Пригласительная ссылка"); + setMinimumWidth(420); + setModal(true); + + auto *layout = new QVBoxLayout(this); + layout->setSpacing(12); + + auto *linkEdit = new QLineEdit(m_link, this); + linkEdit->setReadOnly(true); + linkEdit->setStyleSheet("padding: 6px; font-family: monospace;"); + layout->addWidget(linkEdit); + + auto *btnLayout = new QHBoxLayout(); + auto *copyBtn = new QPushButton("Скопировать", this); + connect(copyBtn, &QPushButton::clicked, this, [this, linkEdit]() { + QApplication::clipboard()->setText(m_link); + linkEdit->setFocus(); + }); + btnLayout->addWidget(copyBtn); + btnLayout->addStretch(); + layout->addLayout(btnLayout); + + auto *qrLabel = new QLabel(this); + QImage qrImage = generateQRCode(m_link); + if (!qrImage.isNull()) + qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled( + 180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + qrLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(qrLabel, 0, Qt::AlignCenter); + + auto *closeBtn = new QPushButton("OK", this); + connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); + layout->addWidget(closeBtn, 0, Qt::AlignRight); +} diff --git a/tools/chatgui/src/invitedialog.h b/tools/chatgui/src/invitedialog.h new file mode 100644 index 00000000..24ee33e4 --- /dev/null +++ b/tools/chatgui/src/invitedialog.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class InviteDialog : public QDialog { + Q_OBJECT +public: + explicit InviteDialog(const QString& link, QWidget* parent = nullptr); + +private: + QString m_link; +}; diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 5df4f1f9..475e083d 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -1,12 +1,32 @@ #include +#include +#include +#include #include "mainwindow.h" +#include "../db/db_manager.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setApplicationName("Chat"); app.setQuitOnLastWindowClosed(false); - MainWindow w; + QString cfgPath = QCoreApplication::applicationDirPath() + "/chatgui.cfg"; + QString dbPath = "chats.db"; + + if (QFile::exists(cfgPath)) { + QSettings cfg(cfgPath, QSettings::IniFormat); + dbPath = cfg.value("db_path", "chats.db").toString(); + } else { + QSettings cfg(cfgPath, QSettings::IniFormat); + cfg.setValue("db_path", "chats.db"); + cfg.sync(); + } + + if (!QDir::isAbsolutePath(dbPath)) + dbPath = QCoreApplication::applicationDirPath() + "/" + dbPath; + + DbManager db(dbPath); + MainWindow w(nullptr, &db); w.show(); return app.exec(); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 88dcdd20..6fa002e7 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -2,32 +2,66 @@ #include "channellist.h" #include "messagelist.h" #include "accountlist.h" +#include "invitedialog.h" +#include "invite_link.h" +#include "../db/db_manager.h" +#include "../transport/msg_client.h" +#include "../transport/chat_propagator.h" +#include "../transport/crypto.h" #include #include #include #include +#include -MainWindow::MainWindow(QWidget *parent) +MainWindow::MainWindow(QWidget *parent, DbManager* db) : QMainWindow(parent) , m_channelList(nullptr) , m_messageList(nullptr) , m_accountList(nullptr) , m_trayIcon(nullptr) , m_trayMenu(nullptr) + , m_db(db) + , m_client(nullptr) + , m_propagator(nullptr) { setWindowTitle("Chat"); resize(900, 600); - setupUi(); setupTray(); + + if (m_db && m_db->isOpen()) { + m_client = new MsgClient(this); + m_propagator = new ChatPropagator(m_client, m_db, this); + m_messageList->setPropagator(m_propagator); + + connect(m_propagator, &ChatPropagator::messageReceived, + m_messageList, &MessageList::addMessage); + + connect(m_channelList, &ChannelList::channelSelected, + this, [this](const QString& channelId) { + m_db->setUiState("last_channel_id", channelId); + m_messageList->loadChannel(channelId); + }); + + connect(m_channelList, &ChannelList::inviteRequested, + this, &MainWindow::showInviteDialog); + + m_channelList->loadChannels(); + m_accountList->loadAccounts(); + + QString lastCid = m_db->getUiState("last_channel_id"); + if (!lastCid.isEmpty()) + m_channelList->selectChannel(lastCid); + } } void MainWindow::setupUi() { auto *splitter = new QSplitter(Qt::Horizontal, this); - m_channelList = new ChannelList(splitter); - m_messageList = new MessageList(splitter); - m_accountList = new AccountList(splitter); + m_channelList = new ChannelList(m_db, splitter); + m_messageList = new MessageList(m_db, splitter); + m_accountList = new AccountList(m_db, splitter); splitter->addWidget(m_channelList); splitter->addWidget(m_messageList); @@ -69,7 +103,7 @@ void MainWindow::setupTray() { void MainWindow::closeEvent(QCloseEvent *event) { hide(); m_trayIcon->showMessage("Chat", "Application minimized to tray.", - QSystemTrayIcon::Information, 2000); + QSystemTrayIcon::Information, 2000); event->ignore(); } @@ -81,3 +115,24 @@ void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { activateWindow(); } } + +void MainWindow::showInviteDialog(const QString& channelId) { + if (!m_db || !m_db->isOpen()) return; + + QByteArray pubkey = m_db->getLocalPubkey(); + if (pubkey.size() != 32) { + QMessageBox::warning(this, "Ошибка", "Не удалось получить публичный ключ узла."); + return; + } + + QList addrs = m_db->getOnlinePeerAddresses(m_db->myNodeId()); + + QString link = encodeInviteLink(channelId, pubkey, addrs); + if (link.isEmpty()) { + QMessageBox::warning(this, "Ошибка", "Не удалось сформировать пригласительную ссылку."); + return; + } + + InviteDialog dlg(link, this); + dlg.exec(); +} diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index bc2401d3..9483a886 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -7,11 +7,14 @@ class ChannelList; class MessageList; class AccountList; +class DbManager; +class MsgClient; +class ChatPropagator; class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(QWidget *parent = nullptr); + explicit MainWindow(QWidget *parent = nullptr, DbManager* db = nullptr); ~MainWindow() override = default; protected: @@ -19,14 +22,18 @@ protected: private slots: void onTrayActivated(QSystemTrayIcon::ActivationReason reason); + void showInviteDialog(const QString& channelId); private: void setupUi(); void setupTray(); - ChannelList *m_channelList; - MessageList *m_messageList; - AccountList *m_accountList; + ChannelList *m_channelList; + MessageList *m_messageList; + AccountList *m_accountList; QSystemTrayIcon *m_trayIcon; QMenu *m_trayMenu; + DbManager *m_db; + MsgClient *m_client; + ChatPropagator *m_propagator; }; diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 17db68af..f3c7e81d 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -6,6 +6,8 @@ #include "emoji.h" #include "animtimer.h" #include "lottieicon.h" +#include "../db/db_manager.h" +#include "../transport/chat_propagator.h" #include #include #include @@ -61,12 +63,13 @@ static void setMsg(QStandardItem *item, item->setData(reactions, MsgReactionsRole); } -MessageList::MessageList(QWidget *parent) +MessageList::MessageList(DbManager* db, QWidget *parent) : QWidget(parent) , m_view(new ChatView(this)) , m_model(new QStandardItemModel(this)) , m_inputBar(nullptr) , m_emojiPanel(nullptr) + , m_db(db) { auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -107,19 +110,26 @@ MessageList::MessageList(QWidget *parent) layout->addWidget(m_inputBar); connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) { - QColor youColor(74, 144, 226); - auto av = makeAvatar(youColor, "Y"); + auto av = makeAvatar(QColor(74, 144, 226), "Y"); auto *item = new QStandardItem(); + auto ts = QDateTime::currentMSecsSinceEpoch(); setMsg(item, QString::fromUtf8("You"), text, QDateTime::currentDateTime().toString("HH:mm"), av, false, false, QString(), QString(), - QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8B"), 1)}); + QVariantList{}); m_model->appendRow(item); m_view->scrollToBottom(); - }); - populateStubData(); + if (m_db && !m_currentChannelId.isEmpty()) + m_db->insertMessage(m_currentChannelId, m_db->myNodeId(), text, ts, 1); + + if (m_propagator && !m_currentChannelId.isEmpty()) { + static const unsigned char zeroSig[64] = {}; + m_propagator->propagateNewMessage(m_currentChannelId, text.toUtf8(), ts, + QByteArray(reinterpret_cast(zeroSig), 64)); + } + }); connect(AnimTimer::instance(), &AnimTimer::ticked, this, [this]() { m_view->viewport()->update(); @@ -146,107 +156,70 @@ MessageList::MessageList(QWidget *parent) else if (wasActive && !m_animHoverActive) AnimTimer::instance()->deactivate(); }); } -void MessageList::populateStubData() { - QColor cAlice(74, 144, 226), cBob(126, 211, 33), cCarol(245, 166, 35); - QColor cDave(144, 19, 254), cEve(0, 184, 148); - auto avAlice = makeAvatar(cAlice, "A"); - auto avBob = makeAvatar(cBob, "B"); - auto avCarol = makeAvatar(cCarol, "C"); - auto avDave = makeAvatar(cDave, "D"); - - auto *m0 = new QStandardItem(); - setMsg(m0, - QString::fromUtf8("Alice"), - QString::fromUtf8("Hey team! How's the deployment going?"), - "10:15", avAlice, false, - false, QString(), QString(), - {}); - m_model->appendRow(m0); - - auto *m1 = new QStandardItem(); - setMsg(m1, - QString::fromUtf8("Bob"), - QString::fromUtf8("All green on staging. Running the final integration tests now."), - "10:17", avBob, false, - false, QString(), QString(), - QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 2, - {cAlice, cCarol}), - makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8B"), 3)}); - m_model->appendRow(m1); - - auto *m2 = new QStandardItem(); - setMsg(m2, - QString::fromUtf8("Carol"), - QString::fromUtf8("I've pushed the ETCP-42 fix to staging. BGP handshake should be stable now. Please review the patch when you get a chance."), - "10:22", avCarol, true, - false, QString(), QString(), - QVariantList{ - makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 3, - {cAlice, cBob, cDave}), - makeReaction(QString::fromUtf8("\xE2\x9D\xA4\xEF\xB8\x8F"), 1, - {cAlice}) - }); - m_model->appendRow(m2); - - auto *m3 = new QStandardItem(); - setMsg(m3, - QString::fromUtf8("Dave"), - QString::fromUtf8("Wait — the BGP session is still flapping after the handshake. I'm seeing the keepalive timer fire but no route updates come through. Logs attached."), - "10:25", avDave, false, - true, - QString::fromUtf8("Carol"), - QString::fromUtf8("BGP handshake should be stable now"), - QVariantList{ - makeReaction(QString::fromUtf8("\xF0\x9F\x98\xAE"), 1, - {cBob}) - }); - m_model->appendRow(m3); - - auto *m4 = new QStandardItem(); - setMsg(m4, - QString::fromUtf8("Alice"), - QString::fromUtf8("Reviewed the patch. LGTM. Let's merge and deploy to production tomorrow morning."), - "10:28", avAlice, false, - false, QString(), QString(), - QVariantList{ - makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 5), - makeReaction(QString::fromUtf8("\xF0\x9F\x8E\x89"), 2, - {cBob, cEve}) - }); - m_model->appendRow(m4); - - auto *m5 = new QStandardItem(); - setMsg(m5, - QString::fromUtf8("Bob"), - QString::fromUtf8("Can you share the full logs for the BGP session? I'll dig into the keepalive timer logic."), - "10:30", avBob, false, - true, - QString::fromUtf8("Dave"), - QString::fromUtf8("keepalive timer fire but no route updates"), - {}); - m_model->appendRow(m5); - - auto *m6 = new QStandardItem(); - setMsg(m6, - QString::fromUtf8("Carol"), - QString::fromUtf8("Sounds good \xF0\x9F\x91\x8B. Let's coordinate in #dev-team. I'll prepare the release notes."), - "10:33", avCarol, false, - false, QString(), QString(), - QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\xA4\x96"), 2)}); - m_model->appendRow(m6); - - auto *m7 = new QStandardItem(); - setMsg(m7, - QString::fromUtf8("Alice"), - QString::fromUtf8("Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main."), - "10:35", avAlice, false, - false, QString(), QString(), - QVariantList{ - makeReaction(QString::fromUtf8("\xF0\x9F\x9A\x80"), 3, - {cBob, cCarol, cDave}), - makeReaction(QString::fromUtf8("\xE2\x9D\xA4\xEF\xB8\x8F"), 7) - }); - m_model->appendRow(m7); + +void MessageList::loadChannel(const QString& channelId) { + m_model->clear(); + m_avatarCache.clear(); + m_currentChannelId = channelId; + if (!m_db || !m_db->isOpen()) return; + + auto msgs = m_db->getMessages(channelId, 100); + + struct ReactionKey { qint64 msgId; quint64 node; QString emoji; }; + QHash>> reactMap; + + for (const auto& m : msgs) { + AccountRow acc = m_db->getAccount(m.authorNodeId); + QString author = acc.displayName.isEmpty() + ? QString("node_%1").arg(m.authorNodeId) : acc.displayName; + QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; + QColor color(acc.avatarColor); + + if (!m_avatarCache.contains(m.authorNodeId)) { + m_avatarCache[m.authorNodeId] = color; + } + + auto avatar = makeAvatar(color, letter); + auto time = QDateTime::fromMSecsSinceEpoch(m.timestamp).toString("HH:mm"); + + QVariantList reacts; + auto *item = new QStandardItem(); + setMsg(item, author, m.content, time, avatar, false, + false, QString(), QString(), reacts); + m_model->appendRow(item); + } + + if (m_model->rowCount() > 0) + m_view->scrollToBottom(); + m_inputBar->setFocus(); +} + +void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, + const QByteArray& content, qint64 timestamp) { + if (channelId != m_currentChannelId) return; + + AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow(); + QString author = acc.displayName.isEmpty() + ? QString("node_%1").arg(authorNodeId) : acc.displayName; + QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; + QColor color(acc.avatarColor); + auto avatar = makeAvatar(color, letter); + auto time = QDateTime::fromMSecsSinceEpoch(timestamp).toString("HH:mm"); + + /* dedup */ + for (int r = 0; r < m_model->rowCount(); r++) { + auto* it = m_model->item(r); + if (it->data(MsgTimeRole).toString() == time + && it->data(MsgAuthorRole).toString() == author) return; + } + + auto *item = new QStandardItem(); + setMsg(item, author, QString::fromUtf8(content), time, avatar, false, + false, QString(), QString(), QVariantList{}); + m_model->appendRow(item); + + if (m_model->rowCount() > 50) + m_model->removeRow(0); m_view->scrollToBottom(); } diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index 33659689..265f2de5 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -2,21 +2,32 @@ #include #include +#include class ChatView; class InputBar; class EmojiPanel; +class DbManager; +class ChatPropagator; class MessageList : public QWidget { Q_OBJECT public: - explicit MessageList(QWidget *parent = nullptr); + explicit MessageList(DbManager* db = nullptr, QWidget *parent = nullptr); + + void loadChannel(const QString& channelId); + void addMessage(const QString& channelId, quint64 authorNodeId, + const QByteArray& content, qint64 timestamp); + void setPropagator(ChatPropagator* p) { m_propagator = p; } private: - ChatView *m_view; + ChatView *m_view; QStandardItemModel *m_model; - InputBar *m_inputBar; - EmojiPanel *m_emojiPanel; - bool m_animHoverActive = false; - void populateStubData(); + InputBar *m_inputBar; + EmojiPanel *m_emojiPanel; + DbManager *m_db; + ChatPropagator *m_propagator = nullptr; + QString m_currentChannelId; + bool m_animHoverActive = false; + QHash m_avatarCache; }; diff --git a/tools/chatgui/src/qrcode_utils.cpp b/tools/chatgui/src/qrcode_utils.cpp new file mode 100644 index 00000000..018620ca --- /dev/null +++ b/tools/chatgui/src/qrcode_utils.cpp @@ -0,0 +1,26 @@ +#include "qrcode_utils.h" +#include "ZXingQt.h" + +QImage generateQRCode(const QString& text, int moduleSize) +{ + using namespace ZXingQt; + auto barcode = Barcode::fromText(text, BarcodeFormat::QRCode); + if (!barcode.isValid()) + return {}; + WriterOptions opts; + if (moduleSize > 0) + opts.scale(moduleSize); + return barcode.toImage(opts); +} + +QStringList readQRCodes(const QImage& image) +{ + using namespace ZXingQt; + auto results = ReadBarcodes(image); + QStringList texts; + for (const auto& b : results) { + if (b.isValid()) + texts.append(b.text()); + } + return texts; +} diff --git a/tools/chatgui/src/qrcode_utils.h b/tools/chatgui/src/qrcode_utils.h new file mode 100644 index 00000000..bff756e1 --- /dev/null +++ b/tools/chatgui/src/qrcode_utils.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +QImage generateQRCode(const QString& text, int moduleSize = 8); + +QStringList readQRCodes(const QImage& image); diff --git a/tools/chatgui/tests/test_crypto.cpp b/tools/chatgui/tests/test_crypto.cpp new file mode 100644 index 00000000..ae4e2392 --- /dev/null +++ b/tools/chatgui/tests/test_crypto.cpp @@ -0,0 +1,247 @@ +/* + * test_crypto.cpp — тесты криптографических функций (Crypto::) + */ + +#include +#include +#include +#include +#include +#include +#include +#include "../transport/crypto.h" + +class TestCrypto : public QObject { + Q_OBJECT + +private slots: + void initTestCase(); + void test_derive_key_sizes(); + void test_derive_priv_not_empty(); + void test_derive_pub_not_empty(); + void test_priv_ne_pub(); + void test_sign_verify_roundtrip(); + void test_sign_verify_empty(); + void test_verify_wrong_sig(); + void test_verify_wrong_key(); + void test_sign_empty_key(); + void test_sign_invalid_key_size(); + void test_signFile_verifyFile(); + void test_signFile_nonexistent(); + void test_sign_verify_100mb_parallel(); + +private: + void writeTestFile(const QString& path, unsigned char pattern, size_t fileSize); + +private: + QByteArray m_x25519Priv; // 32 random bytes + QByteArray m_edPriv; // derived Ed25519 private key + QByteArray m_edPub; // derived Ed25519 public key + QByteArray m_message; +}; + +void TestCrypto::initTestCase() { + m_x25519Priv = QByteArray(Crypto::kKeySize, 0); + for (int i = 0; i < static_cast(Crypto::kKeySize); i++) + m_x25519Priv[i] = static_cast(0x42 + i); /* детерминированное значение */ + + m_edPriv = Crypto::deriveEd25519Privkey(m_x25519Priv); + m_edPub = Crypto::deriveEd25519Pubkey(m_x25519Priv); + m_message = QByteArray("hello world"); +} + +void TestCrypto::test_derive_key_sizes() { + QCOMPARE(m_edPriv.size(), static_cast(Crypto::kKeySize)); + QCOMPARE(m_edPub.size(), static_cast(Crypto::kKeySize)); +} + +void TestCrypto::test_derive_priv_not_empty() { + QVERIFY(!m_edPriv.isEmpty()); +} + +void TestCrypto::test_derive_pub_not_empty() { + QVERIFY(!m_edPub.isEmpty()); +} + +void TestCrypto::test_priv_ne_pub() { + QVERIFY(m_edPriv != m_edPub); +} + +void TestCrypto::test_sign_verify_roundtrip() { + QByteArray sig = Crypto::sign(m_edPriv, m_message); + QCOMPARE(sig.size(), static_cast(Crypto::kSignSize)); + QVERIFY(Crypto::verify(m_edPub, m_message, sig)); +} + +void TestCrypto::test_sign_verify_empty() { + QByteArray empty; + QByteArray sig = Crypto::sign(m_edPriv, empty); + QCOMPARE(sig.size(), static_cast(Crypto::kSignSize)); + QVERIFY(Crypto::verify(m_edPub, empty, sig)); +} + +void TestCrypto::test_verify_wrong_sig() { + QByteArray sig = Crypto::sign(m_edPriv, m_message); + QVERIFY(!sig.isEmpty()); + sig[8] = static_cast(sig[8] ^ 1); /* портим подпись */ + QVERIFY(!Crypto::verify(m_edPub, m_message, sig)); +} + +void TestCrypto::test_verify_wrong_key() { + QByteArray sig = Crypto::sign(m_edPriv, m_message); + /* другой ключ */ + QByteArray otherPriv(Crypto::kKeySize, 0); + for (int i = 0; i < static_cast(Crypto::kKeySize); i++) + otherPriv[i] = static_cast(0x77 + i); + QByteArray otherPub = Crypto::deriveEd25519Pubkey(otherPriv); + QVERIFY(!otherPub.isEmpty()); + QVERIFY(!Crypto::verify(otherPub, m_message, sig)); +} + +void TestCrypto::test_sign_empty_key() { + QByteArray sig = Crypto::sign(QByteArray(), m_message); + QVERIFY(sig.isEmpty()); + QVERIFY(!Crypto::verify(QByteArray(), m_message, sig)); +} + +void TestCrypto::test_sign_invalid_key_size() { + QByteArray badKey(16, 0x42); + QByteArray sig = Crypto::sign(badKey, m_message); + QVERIFY(sig.isEmpty()); +} + +void TestCrypto::test_signFile_verifyFile() { + QTemporaryFile tmp; + QVERIFY(tmp.open()); + QByteArray content("file content for signing test"); + tmp.write(content); + tmp.close(); + + QByteArray sig; + QEventLoop loop; + Crypto::signFile(tmp.fileName(), m_edPriv, + [&](const Crypto::SignResult& r) { + sig = r.signature; + QVERIFY(r.ok); + QVERIFY(!sig.isEmpty()); + loop.quit(); + }); + loop.exec(); + + QEventLoop loop2; + Crypto::verifyFile(tmp.fileName(), m_edPub, sig, + [&](const Crypto::SignResult& r) { + QVERIFY(r.ok); + loop2.quit(); + }); + loop2.exec(); +} + +void TestCrypto::test_signFile_nonexistent() { + QEventLoop loop; + Crypto::signFile("/nonexistent/path/test", m_edPriv, + [&](const Crypto::SignResult& r) { + QVERIFY(!r.ok); + QVERIFY(!r.error.isEmpty()); + loop.quit(); + }); + loop.exec(); +} + +void TestCrypto::writeTestFile(const QString& path, unsigned char pattern, size_t fileSize) { + QFile f(path); + QVERIFY(f.open(QIODevice::WriteOnly)); + constexpr size_t kChunkSize = 65536; + QByteArray chunk(static_cast(kChunkSize), Qt::Uninitialized); + memset(chunk.data(), pattern, kChunkSize); + size_t remaining = fileSize; + while (remaining >= kChunkSize) { + QCOMPARE(f.write(chunk), static_cast(kChunkSize)); + remaining -= kChunkSize; + } + if (remaining > 0) f.write(chunk.constData(), static_cast(remaining)); + f.close(); +} + +void TestCrypto::test_sign_verify_100mb_parallel() { + constexpr size_t kFileSize = 100ULL * 1024 * 1024; + constexpr int kNumFiles = 3; + + QString paths[kNumFiles]; + QTemporaryFile tmp[kNumFiles]; + for (int i = 0; i < kNumFiles; i++) { + QVERIFY(tmp[i].open()); + paths[i] = tmp[i].fileName(); + tmp[i].close(); + writeTestFile(paths[i], static_cast(0xAA + i), kFileSize); + } + + /* ------- Sign (параллельно) ------- */ + QByteArray sigs[kNumFiles]; + qint64 signTimesMs[kNumFiles] = {}; + QElapsedTimer totalSignTimer; + QAtomicInt signDone{0}; + QEventLoop signLoop; + + totalSignTimer.start(); + for (int i = 0; i < kNumFiles; i++) { + QElapsedTimer t; + t.start(); + Crypto::signFile(paths[i], m_edPriv, + [&, i, t](const Crypto::SignResult& r) mutable { + QVERIFY(r.ok); + QVERIFY(!r.signature.isEmpty()); + sigs[i] = r.signature; + signTimesMs[i] = t.elapsed(); + if (signDone.fetchAndAddRelaxed(1) == kNumFiles - 1) + signLoop.quit(); + }); + } + signLoop.exec(); + qint64 totalSignMs = totalSignTimer.elapsed(); + + /* ------- Verify (параллельно) ------- */ + qint64 verifyTimesMs[kNumFiles] = {}; + QElapsedTimer totalVerifyTimer; + QAtomicInt verifyDone{0}; + QEventLoop verifyLoop; + + totalVerifyTimer.start(); + for (int i = 0; i < kNumFiles; i++) { + QElapsedTimer t; + t.start(); + Crypto::verifyFile(paths[i], m_edPub, sigs[i], + [&, i, t](const Crypto::SignResult& r) mutable { + QVERIFY(r.ok); + verifyTimesMs[i] = t.elapsed(); + if (verifyDone.fetchAndAddRelaxed(1) == kNumFiles - 1) + verifyLoop.quit(); + }); + } + verifyLoop.exec(); + qint64 totalVerifyMs = totalVerifyTimer.elapsed(); + + /* ------- Профилирование ------- */ + qint64 signSum = 0, verifySum = 0; + for (int i = 0; i < kNumFiles; i++) { + signSum += signTimesMs[i]; + verifySum += verifyTimesMs[i]; + } + + qDebug() << "\n======= test_sign_verify_100mb_parallel ======="; + for (int i = 0; i < kNumFiles; i++) + qDebug().nospace() << " File " << i << ": sign=" << signTimesMs[i] + << "ms | verify=" << verifyTimesMs[i] << "ms"; + qDebug().nospace() << "Phase sign: " << totalSignMs << "ms total" + << " (sum=" << signSum << "ms, speedup=" + << (double)signSum / totalSignMs << "x)"; + qDebug().nospace() << "Phase verify: " << totalVerifyMs << "ms total" + << " (sum=" << verifySum << "ms, speedup=" + << (double)verifySum / totalVerifyMs << "x)"; + + /* Вычищаем временные файлы */ + for (int i = 0; i < kNumFiles; i++) QFile::remove(paths[i]); +} + +QTEST_MAIN(TestCrypto) +#include "test_crypto.moc" diff --git a/tools/chatgui/transport/chat_propagator.cpp b/tools/chatgui/transport/chat_propagator.cpp new file mode 100644 index 00000000..54974a40 --- /dev/null +++ b/tools/chatgui/transport/chat_propagator.cpp @@ -0,0 +1,376 @@ +#include "chat_propagator.h" +#include "chat_proto.h" +#include "msg_client.h" +#include "crypto.h" +#include "db_manager.h" + +#include +#include +#include + +static uint64_t read64(const uint8_t* p) { + uint64_t v; memcpy(&v, p, 8); return v; +} +static void write64(uint8_t* p, uint64_t v) { memcpy(p, &v, 8); } +static uint32_t read32(const uint8_t* p) { + uint32_t v; memcpy(&v, p, 4); return v; +} +static void write32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); } +static uint16_t read16(const uint8_t* p) { + uint16_t v; memcpy(&v, p, 2); return v; +} +static void write16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); } + +ChatPropagator::ChatPropagator(MsgClient* client, DbManager* db, QObject* parent) + : QObject(parent) + , m_client(client) + , m_db(db) + , m_ackTimer(new QTimer(this)) +{ + connect(m_client, &MsgClient::received, + this, &ChatPropagator::onReceived); + connect(m_ackTimer, &QTimer::timeout, + this, &ChatPropagator::onAckTimeout); +} + +void ChatPropagator::setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId) { + m_edPriv = edPriv; + m_edPub = edPub; + m_myNodeId = nodeId; +} + +void ChatPropagator::start() { + if (!m_edPriv.isEmpty() && !m_ackTimer->isActive()) + m_ackTimer->start(250); + startCatchUp(); +} + +/* ======================================================================== */ + +void ChatPropagator::propagateNewMessage(const QString& channelId, + const QByteArray& content, + qint64 timestamp, + const QByteArray& signature) { + if (m_edPriv.isEmpty() || !m_myNodeId) return; + + QByteArray packet = buildNewMsgPacket(channelId, content, timestamp, signature); + QList exclude; + exclude.append(m_myNodeId); + fanoutGossip(channelId, packet, signature, ChatProp::kDefaultFanout, exclude); +} + +QByteArray ChatPropagator::buildNewMsgPacket(const QString& channelId, + const QByteArray& content, + qint64 timestamp, + const QByteArray& signature) { + QByteArray cid = channelId.toUtf8(); + uint8_t cidLen = static_cast(cid.size()); + size_t total = 1 + 1 + cidLen + 8 + 2 + content.size() + ChatProp::kSignSize; + + QByteArray pkt(static_cast(total), Qt::Uninitialized); + uint8_t* p = reinterpret_cast(pkt.data()); + *p++ = ChatProp::kNewMsg; + *p++ = cidLen; + memcpy(p, cid.constData(), cidLen); p += cidLen; + write64(p, static_cast(timestamp)); p += 8; + write16(p, static_cast(content.size())); p += 2; + memcpy(p, content.constData(), content.size()); p += content.size(); + memcpy(p, signature.constData(), ChatProp::kSignSize); + return pkt; +} + +QByteArray ChatPropagator::buildAckPacket(const QByteArray& sig) { + QByteArray pkt(static_cast(1 + ChatProp::kSignSize), Qt::Uninitialized); + pkt[0] = static_cast(ChatProp::kNewAck); + memcpy(pkt.data() + 1, sig.constData(), ChatProp::kSignSize); + return pkt; +} + +/* ======================================================================== */ + +void ChatPropagator::fanoutGossip(const QString& channelId, const QByteArray& packet, + const QByteArray& sig, int fanout, + const QList& exclude) { + auto peers = pickRandomPeers(fanout, exclude); + if (peers.isEmpty()) return; + + GossipEntry e; + e.channelId = channelId; + e.packet = packet; + e.sig = sig; + e.peers = peers; + e.sentAt = QDateTime::currentMSecsSinceEpoch(); + m_pending[sig] = e; + + for (auto peer : peers) + sendGossip(peer, packet); +} + +void ChatPropagator::sendGossip(quint64 peer, const QByteArray& packet) { + m_client->sendTo(peer, packet); +} + +/* ======================================================================== */ + +void ChatPropagator::onReceived(quint64 peerId, const QByteArray& data) { + if (data.isEmpty()) return; + uint8_t type = static_cast(data[0]); + switch (type) { + case ChatProp::kNewMsg: handleNewMsg(peerId, data); break; + case ChatProp::kNewAck: handleNewAck(data); break; + case ChatProp::kBucketsRsp: handleBucketsRsp(data); break; + case ChatProp::kHashesRsp: handleHashesRsp(data); break; + case ChatProp::kMsgsRsp: handleMsgsRsp(data); break; + } +} + +void ChatPropagator::handleNewMsg(quint64 fromPeer, const QByteArray& payload) { + QString channelId; + QByteArray content; + qint64 timestamp; + QByteArray sig; + if (!parseNewMsg(payload, channelId, content, timestamp, sig)) + return; + + /* verify sender's signature */ + QByteArray peerEdPub = m_db->getNodeEdPub(fromPeer); + if (!peerEdPub.isEmpty() && !Crypto::verify(peerEdPub, payload.left(payload.size() - 64), sig)) + return; + + /* insert into DB */ + m_db->insertMessage(channelId, fromPeer, content, timestamp, 0); + + emit messageReceived(channelId, fromPeer, content, timestamp); + + /* re-gossip */ + QList exclude; + exclude.append(m_myNodeId); + exclude.append(fromPeer); + fanoutGossip(channelId, payload, sig, ChatProp::kDefaultFanout, exclude); + + /* ack to sender */ + m_client->sendTo(fromPeer, buildAckPacket(sig)); +} + +void ChatPropagator::handleNewAck(const QByteArray& payload) { + if (payload.size() < static_cast(1 + ChatProp::kSignSize)) return; + QByteArray sig = payload.mid(1, static_cast(ChatProp::kSignSize)); + + auto it = m_pending.find(sig); + if (it == m_pending.end()) return; + + /* mark all peers as acked (simple version: ACK means message delivered) */ + m_pending.erase(it); +} + +void ChatPropagator::onAckTimeout() { + qint64 now = QDateTime::currentMSecsSinceEpoch(); + QList toRemove; + + for (auto it = m_pending.begin(); it != m_pending.end(); ++it) { + auto& e = it.value(); + if (now - e.sentAt < ChatProp::kDefaultAckTimeoutMs) continue; + + if (e.retries >= 3) { + toRemove.append(it.key()); + continue; + } + + /* retry: pick new peers for each missing ACK */ + QList unacked; + for (auto p : e.peers) + if (!e.acked.contains(p)) unacked.append(p); + + if (!unacked.isEmpty()) { + auto newPeers = pickRandomPeers(1, e.peers + e.acked + + QList{m_myNodeId}); + if (!newPeers.isEmpty()) { + sendGossip(newPeers.first(), e.packet); + e.peers.append(newPeers.first()); + } + } + + e.sentAt = now; + e.retries++; + } + + for (auto& k : toRemove) m_pending.remove(k); +} + +/* ======================================================================== + * Catch-up + * ======================================================================== */ + +void ChatPropagator::startCatchUp() { + if (!m_db) return; + auto channels = m_db->getChannels(); + if (channels.isEmpty()) return; + + auto peers = pickRandomPeers(ChatProp::kDefaultFanout, {m_myNodeId}); + if (peers.isEmpty()) return; + + for (auto& ch : channels) { + QByteArray cid = ch.channelId.toUtf8(); + QByteArray pkt(static_cast(2 + cid.size()), Qt::Uninitialized); + pkt[0] = static_cast(ChatProp::kBucketsReq); + pkt[1] = static_cast(cid.size()); + memcpy(pkt.data() + 2, cid.constData(), cid.size()); + + for (auto peer : peers) + sendGossip(peer, pkt); + } +} + +void ChatPropagator::handleBucketsRsp(const QByteArray& payload) { + if (payload.size() < 3) return; + const uint8_t* p = reinterpret_cast(payload.constData()); + if (*p != ChatProp::kBucketsRsp) return; p++; + uint8_t cidLen = *p++; + if (p + cidLen + 2 > reinterpret_cast(payload.constData()) + payload.size()) return; + QString channelId = QString::fromUtf8(reinterpret_cast(p), cidLen); p += cidLen; + uint16_t bCount = read16(p); p += 2; + + for (int i = 0; i < bCount; i++) { + if (p + 4 + 1 + 4 + 32 > reinterpret_cast(payload.constData()) + payload.size()) break; + qint64 day = static_cast(read32(p)); p += 4; + int hour = *p++; + int rCount = static_cast(read32(p)); p += 4; + QByteArray rHash(reinterpret_cast(p), 32); p += 32; + + auto local = m_db->getBucketMeta(channelId); + bool found = false; + for (auto& b : local) { + if (b.day == day && b.hour == hour && b.count == rCount && b.hash == rHash) { + found = true; break; + } + } + if (found) continue; + + /* mismatch — request signatures */ + QByteArray cid = channelId.toUtf8(); + QByteArray req(static_cast(7 + cid.size()), Qt::Uninitialized); + uint8_t* rp = reinterpret_cast(req.data()); + *rp++ = ChatProp::kHashesReq; + *rp++ = static_cast(cid.size()); + memcpy(rp, cid.constData(), cid.size()); rp += cid.size(); + write32(rp, static_cast(day)); rp += 4; + *rp = static_cast(hour); + + auto peers = pickRandomPeers(1, {m_myNodeId}); + if (!peers.isEmpty()) + sendGossip(peers.first(), req); + } +} + +void ChatPropagator::handleHashesRsp(const QByteArray& payload) { + if (payload.size() < 3) return; + const uint8_t* p = reinterpret_cast(payload.constData()); + if (*p != ChatProp::kHashesRsp) return; p++; + uint16_t sigCount = read16(p); p += 2; + + QList remoteSigs; + for (int i = 0; i < sigCount; i++) { + if (p + 64 > reinterpret_cast(payload.constData()) + payload.size()) break; + remoteSigs.append(QByteArray(reinterpret_cast(p), 64)); + p += 64; + } + if (remoteSigs.isEmpty()) return; + + /* find missing */ + QList missing; + QList remote = remoteSigs; + for (auto& s : remote) { + /* TODO: efficient lookup — query DB for this sig */ + auto msgs = m_db->getMessagesBySigs({s}); + if (msgs.isEmpty()) missing.append(s); + } + if (missing.isEmpty()) return; + + /* request missing messages */ + int mCount = missing.size(); + QByteArray req(static_cast(3 + mCount * 64), Qt::Uninitialized); + uint8_t* rp = reinterpret_cast(req.data()); + *rp++ = ChatProp::kMsgsReq; + write16(rp, static_cast(mCount)); rp += 2; + for (auto& s : missing) { memcpy(rp, s.constData(), 64); rp += 64; } + + auto peers = pickRandomPeers(1, {m_myNodeId}); + if (!peers.isEmpty()) + sendGossip(peers.first(), req); +} + +void ChatPropagator::handleMsgsRsp(const QByteArray& payload) { + if (payload.size() < 4) return; + const uint8_t* p = reinterpret_cast(payload.constData()); + const uint8_t* end = p + payload.size(); + if (*p != ChatProp::kMsgsRsp) return; p++; + uint8_t cidLen = *p++; + if (p + cidLen + 2 > end) return; + QString channelId = QString::fromUtf8(reinterpret_cast(p), cidLen); p += cidLen; + uint16_t msgCount = read16(p); p += 2; + + for (int i = 0; i < msgCount; i++) { + if (p + 64 + 8 + 8 + 2 > end) break; + QByteArray sig(reinterpret_cast(p), 64); p += 64; + quint64 author = read64(p); p += 8; + qint64 ts = static_cast(read64(p)); p += 8; + uint16_t cLen = read16(p); p += 2; + if (p + cLen > end) break; + QByteArray content(reinterpret_cast(p), cLen); p += cLen; + + auto existing = m_db->getMessagesBySigs({sig}); + if (!existing.isEmpty()) continue; + + m_db->insertMessage(channelId, author, content, ts, 0); + emit messageReceived(channelId, author, content, ts); + } +} + +/* ======================================================================== */ + +bool ChatPropagator::parseNewMsg(const QByteArray& payload, QString& channelId, + QByteArray& content, qint64& timestamp, + QByteArray& sig) { + if (payload.size() < 14) return false; /* minimum: 1+1+1+8+2+1 */ + + const uint8_t* p = reinterpret_cast(payload.constData()); + if (*p != ChatProp::kNewMsg) return false; + p++; + + uint8_t cidLen = *p++; + if (p + cidLen + 8 + 2 > reinterpret_cast(payload.constData()) + payload.size()) + return false; + channelId = QString::fromUtf8(reinterpret_cast(p), cidLen); p += cidLen; + + timestamp = static_cast(read64(p)); p += 8; + + uint16_t contentLen = read16(p); p += 2; + if (p + contentLen + ChatProp::kSignSize > + reinterpret_cast(payload.constData()) + payload.size()) + return false; + + content = QByteArray(reinterpret_cast(p), contentLen); p += contentLen; + sig = QByteArray(reinterpret_cast(p), static_cast(ChatProp::kSignSize)); + return true; +} + +QList ChatPropagator::pickRandomPeers(int count, const QList& exclude) { + QList result; + if (!m_db) return result; + + auto accs = m_db->getAccounts(true); + QList pool; + for (auto& a : accs) { + if (!exclude.contains(a.nodeId)) + pool.append(a.nodeId); + } + if (pool.isEmpty()) return result; + + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(pool.begin(), pool.end(), g); + + for (int i = 0; i < count && i < pool.size(); i++) + result.append(pool[i]); + return result; +} diff --git a/tools/chatgui/transport/chat_propagator.h b/tools/chatgui/transport/chat_propagator.h new file mode 100644 index 00000000..8645c214 --- /dev/null +++ b/tools/chatgui/transport/chat_propagator.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class MsgClient; +class DbManager; +struct MessageRow; + +class ChatPropagator : public QObject { + Q_OBJECT +public: + ChatPropagator(MsgClient* client, DbManager* db, QObject* parent = nullptr); + + void setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId); + void start(); + void propagateNewMessage(const QString& channelId, const QByteArray& content, + qint64 timestamp, const QByteArray& signature); + +signals: + void messageReceived(const QString& channelId, quint64 authorNodeId, + const QByteArray& content, qint64 timestamp); + +private: + MsgClient* m_client; + DbManager* m_db; + QTimer* m_ackTimer; + + QByteArray m_edPriv; + QByteArray m_edPub; + quint64 m_myNodeId = 0; + + struct GossipEntry { + QString channelId; + QByteArray packet; /* full NEW_MSG packet */ + QByteArray sig; /* message signature (key) */ + QList peers; /* target peers */ + QList acked; /* peers that acknowledged */ + qint64 sentAt; /* when sent (ms) */ + int retries = 0; + }; + QHash m_pending; /* sig → entry */ + QByteArray m_mySig; /* sig we're waiting for ACK on */ + + void onReceived(quint64 peerId, const QByteArray& data); + void handleNewMsg(quint64 fromPeer, const QByteArray& payload); + void handleNewAck(const QByteArray& payload); + void onAckTimeout(); + + void startCatchUp(); + void handleBucketsRsp(const QByteArray& payload); + void handleHashesRsp(const QByteArray& payload); + void handleMsgsRsp(const QByteArray& payload); + + void fanoutGossip(const QString& channelId, const QByteArray& packet, + const QByteArray& sig, int fanout, const QList& exclude); + void sendGossip(quint64 peer, const QByteArray& packet); + + QList pickRandomPeers(int count, const QList& exclude); + + QByteArray buildNewMsgPacket(const QString& channelId, const QByteArray& content, + qint64 timestamp, const QByteArray& signature); + QByteArray buildAckPacket(const QByteArray& sig); + bool parseNewMsg(const QByteArray& payload, QString& channelId, QByteArray& content, + qint64& timestamp, QByteArray& sig); +}; diff --git a/tools/chatgui/transport/chat_proto.h b/tools/chatgui/transport/chat_proto.h new file mode 100644 index 00000000..eb209986 --- /dev/null +++ b/tools/chatgui/transport/chat_proto.h @@ -0,0 +1,48 @@ +/* + * chat_proto.h — протокол распространения сообщений чата (gossip + catch-up) + * + * Все пакеты идут через MsgClient::sendTo() / received() поверх msg_transport. + * Подписи — внутри пейлоада. utun — чистый транспорт. + */ + +#ifndef CHAT_PROPAGATE_PROTO_H +#define CHAT_PROPAGATE_PROTO_H + +#include + +namespace ChatProp { + +static constexpr uint8_t kNewMsg = 0x01; /* новое сообщение */ +static constexpr uint8_t kNewAck = 0x02; /* подтверждение получения */ +static constexpr uint8_t kBucketsReq = 0x03; /* запрос bucket-ов канала */ +static constexpr uint8_t kBucketsRsp = 0x04; /* bucket-ы: день+час → count+hash */ +static constexpr uint8_t kHashesReq = 0x05; /* запрос подписей в bucket */ +static constexpr uint8_t kHashesRsp = 0x06; /* список подписей (64 байта каждая) */ +static constexpr uint8_t kMsgsReq = 0x07; /* запрос сообщений по подписям */ +static constexpr uint8_t kMsgsRsp = 0x08; /* сообщения + подписи */ + +static constexpr size_t kSignSize = 64; +static constexpr size_t kHashSize = 32; /* SHA-256 */ +static constexpr int kDefaultFanout = 4; +static constexpr int kDefaultAckTimeoutMs = 1000; + +/* Форматы пакетов (byte stream) ================================================ + + NEW_MSG: [0x01][channel_id_len:1][channel_id][timestamp:8][content_len:2][content][sig:64] + NEW_ACK: [0x02][sig_of_confirmed_msg:64] + + BUCKETS_REQ: [0x03][channel_id_len:1][channel_id] + BUCKETS_RSP: [0x04][channel_id_len:1][channel_id][bucket_count:2][buckets...] + bucket = [day_unix:4][hour:1][msg_count:4][sig_hash:32] + + HASHES_REQ: [0x05][channel_id_len:1][channel_id][day_unix:4][hour:1] + HASHES_RSP: [0x06][sig_count:2][sig:64, sig:64, ...] + + MSGS_REQ: [0x07][sig_count:2][sig:64, sig:64, ...] + MSGS_RSP: [0x08][channel_id_len:1][channel_id][msg_count:2][(sig:64, author:8, timestamp:8, content_len:2, content)...] + +=============================================================================== */ + +} // namespace ChatProp + +#endif diff --git a/tools/chatgui/transport/crypto.cpp b/tools/chatgui/transport/crypto.cpp new file mode 100644 index 00000000..f71ef5ba --- /dev/null +++ b/tools/chatgui/transport/crypto.cpp @@ -0,0 +1,221 @@ +/* + * crypto.cpp — Криптографические утилиты (Ed25519 через OpenSSL) + */ + +#include "crypto.h" + +#include +#include +#include +#include +#include +#include + +namespace Crypto { + +/* ============================================================================ + * Помощники + * ============================================================================ */ + +namespace { + +void postToMain(std::function fn) { + auto* app = QCoreApplication::instance(); + if (app) { + QMetaObject::invokeMethod(app, std::move(fn), Qt::QueuedConnection); + } else { + fn(); /* нет event loop — синхронно */ + } +} + +QByteArray sha512File(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) return {}; + + EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + if (!md_ctx || EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL) != 1) { + if (md_ctx) EVP_MD_CTX_free(md_ctx); + return {}; + } + + QByteArray buf(65536, 0); + while (true) { + qint64 n = file.read(buf.data(), buf.size()); + if (n <= 0) break; + EVP_DigestUpdate(md_ctx, reinterpret_cast(buf.constData()), + static_cast(n)); + } + file.close(); + + QByteArray hash; + hash.resize(static_cast(kSha512Size)); + unsigned int hash_len = static_cast(kSha512Size); + if (EVP_DigestFinal_ex(md_ctx, reinterpret_cast(hash.data()), &hash_len) != 1) + hash.clear(); + EVP_MD_CTX_free(md_ctx); + return hash; +} + +} // anonymous + +/* ============================================================================ + * Синхронные: derive ключей + * ============================================================================ */ + +QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey) { + if (x25519Privkey.size() != static_cast(kKeySize)) return QByteArray(); + + uint8_t hash[kSha512Size]; + SHA512(reinterpret_cast(x25519Privkey.constData()), kKeySize, hash); + + EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize); + if (!pkey) return QByteArray(); + + QByteArray result; + result.resize(static_cast(kKeySize)); + size_t priv_len = kKeySize; + if (EVP_PKEY_get_raw_private_key(pkey, reinterpret_cast(result.data()), &priv_len) <= 0 + || priv_len != kKeySize) { + EVP_PKEY_free(pkey); + return QByteArray(); + } + EVP_PKEY_free(pkey); + return result; +} + +QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey) { + if (x25519Privkey.size() != static_cast(kKeySize)) return QByteArray(); + + uint8_t hash[kSha512Size]; + SHA512(reinterpret_cast(x25519Privkey.constData()), kKeySize, hash); + + EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize); + if (!pkey) return QByteArray(); + + QByteArray result; + result.resize(static_cast(kKeySize)); + size_t pub_len = kKeySize; + if (EVP_PKEY_get_raw_public_key(pkey, reinterpret_cast(result.data()), &pub_len) <= 0 + || pub_len != kKeySize) { + EVP_PKEY_free(pkey); + return QByteArray(); + } + EVP_PKEY_free(pkey); + return result; +} + +/* ============================================================================ + * Синхронные: подпись / проверка (малые блоки) + * ============================================================================ */ + +QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data) { + if (ed25519Privkey.size() != static_cast(kKeySize)) return QByteArray(); + + EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, + reinterpret_cast(ed25519Privkey.constData()), kKeySize); + if (!pkey) return QByteArray(); + + EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + if (!md_ctx || EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, pkey) != 1) { + if (md_ctx) EVP_MD_CTX_free(md_ctx); + EVP_PKEY_free(pkey); + return QByteArray(); + } + + size_t sig_len = kSignSize; + QByteArray result; + result.resize(static_cast(kSignSize)); + if (EVP_DigestSign(md_ctx, reinterpret_cast(result.data()), &sig_len, + reinterpret_cast(data.constData()), data.size()) != 1) { + EVP_MD_CTX_free(md_ctx); + EVP_PKEY_free(pkey); + return QByteArray(); + } + + EVP_MD_CTX_free(md_ctx); + EVP_PKEY_free(pkey); + result.resize(static_cast(sig_len)); + return result; +} + +bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig) { + if (ed25519Pubkey.size() != static_cast(kKeySize) + || sig.size() > static_cast(kSignSize)) return false; + + EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, + reinterpret_cast(ed25519Pubkey.constData()), kKeySize); + if (!pkey) return false; + + EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); + if (!md_ctx || EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, pkey) != 1) { + if (md_ctx) EVP_MD_CTX_free(md_ctx); + EVP_PKEY_free(pkey); + return false; + } + + int ret = EVP_DigestVerify(md_ctx, reinterpret_cast(sig.constData()), sig.size(), + reinterpret_cast(data.constData()), data.size()); + EVP_MD_CTX_free(md_ctx); + EVP_PKEY_free(pkey); + return ret == 1; +} + +/* ============================================================================ + * Асинхронные: файлы + * ============================================================================ */ + +void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb) { + std::thread([=]() { + SignResult r; + QByteArray hash = sha512File(path); + if (hash.isEmpty()) { + r.ok = false; r.error = QStringLiteral("failed to read/hash file"); + } else { + r.signature = sign(ed25519Privkey, hash); + r.ok = !r.signature.isEmpty(); + if (!r.ok) r.error = QStringLiteral("sign failed"); + } + postToMain([cb, r]() { cb(r); }); + }).detach(); +} + +void verifyFile(const QString& path, const QByteArray& ed25519Pubkey, + const QByteArray& signature, SignCallback cb) { + std::thread([=]() { + SignResult r; + QByteArray hash = sha512File(path); + if (hash.isEmpty()) { + r.ok = false; r.error = QStringLiteral("failed to read/hash file"); + } else { + r.ok = verify(ed25519Pubkey, hash, signature); + if (!r.ok) r.error = QStringLiteral("signature mismatch"); + } + postToMain([cb, r]() { cb(r); }); + }).detach(); +} + +/* ============================================================================ + * Асинхронные: in-memory блоки + * ============================================================================ */ + +void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb) { + std::thread([=]() { + SignResult r; + r.signature = sign(ed25519Privkey, data); + r.ok = !r.signature.isEmpty(); + if (!r.ok) r.error = QStringLiteral("sign failed"); + postToMain([cb, r]() { cb(r); }); + }).detach(); +} + +void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data, + const QByteArray& signature, SignCallback cb) { + std::thread([=]() { + SignResult r; + r.ok = verify(ed25519Pubkey, data, signature); + if (!r.ok) r.error = QStringLiteral("signature mismatch"); + postToMain([cb, r]() { cb(r); }); + }).detach(); +} + +} // namespace Crypto diff --git a/tools/chatgui/transport/crypto.h b/tools/chatgui/transport/crypto.h new file mode 100644 index 00000000..dd9fd5c0 --- /dev/null +++ b/tools/chatgui/transport/crypto.h @@ -0,0 +1,48 @@ +/* + * crypto.h — Криптографические утилиты для chat GUI + * + * Ключи: X25519 → SHA-512 → Ed25519 (аналог sc_derive_ed25519_key / pubkey в utun) + * Подпись: Ed25519 (EVP_DigestSign / EVP_DigestVerify) + */ + +#ifndef MSG_CRYPTO_H +#define MSG_CRYPTO_H + +#include +#include +#include + +namespace Crypto { + +static constexpr size_t kKeySize = 32; // X25519 / Ed25519 key size +static constexpr size_t kSignSize = 64; // Ed25519 signature size +static constexpr size_t kSha512Size = 64; // SHA-512 digest size + +// ---------------- синхронные (малые блоки) ---------------- + +QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey); +QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey); + +QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data); +bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig); + +// ---------------- асинхронные (файлы / большие блоки) ---------------- + +struct SignResult { + bool ok = false; + QByteArray signature; // 64 bytes + QString error; +}; + +using SignCallback = std::function; + +void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb); +void verifyFile(const QString& path, const QByteArray& ed25519Pubkey, + const QByteArray& signature, SignCallback cb); +void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb); +void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data, + const QByteArray& signature, SignCallback cb); + +} // namespace Crypto + +#endif // MSG_CRYPTO_H diff --git a/tools/chatgui/transport/msg_client.cpp b/tools/chatgui/transport/msg_client.cpp index e0463eff..72cc684e 100644 --- a/tools/chatgui/transport/msg_client.cpp +++ b/tools/chatgui/transport/msg_client.cpp @@ -71,6 +71,26 @@ void MsgClient::sendSigned(const QByteArray& data) { m_socket->write(msg); } +void MsgClient::sendTo(quint64 dstPeerId, const QByteArray& data) { + QByteArray msg = buildMessage(kDataSend, dstPeerId, 0, data); + m_socket->write(msg); +} + +void MsgClient::requestPublicKey() { + QByteArray msg = buildMessage(kCtlGetPubkey, 0, 0, QByteArray()); + m_socket->write(msg); +} + +void MsgClient::requestPrivateKey() { + QByteArray msg = buildMessage(kCtlGetPrivkey, 0, 0, QByteArray()); + m_socket->write(msg); +} + +void MsgClient::requestPeerInfo(quint64 peerId) { + QByteArray msg = buildMessage(kCtlGetPeerInfo, peerId, 0, QByteArray()); + m_socket->write(msg); +} + /* ============================================================================ * Внутренние * ============================================================================ */ @@ -118,6 +138,17 @@ void MsgClient::dispatch(uint8_t type, quint64 id, const QByteArray& payload) { m_boundPeerId = id; emit bound(id); break; + case kRspPubkey: + emit publicKeyReceived(payload); + break; + case kRspPrivkey: + emit privateKeyReceived(payload); + break; + case kRspPeerInfo: + if (payload.size() >= 64) { + emit peerInfoReceived(id, payload.left(32), payload.mid(32, 32)); + } + break; case kDataRecv: emit received(id, payload); break; diff --git a/tools/chatgui/transport/msg_client.h b/tools/chatgui/transport/msg_client.h index ff24f67a..33224d1b 100644 --- a/tools/chatgui/transport/msg_client.h +++ b/tools/chatgui/transport/msg_client.h @@ -31,6 +31,11 @@ public: void send(const QByteArray& data); void send(const uint8_t* data, size_t len); void sendSigned(const QByteArray& data); + void sendTo(quint64 dstPeerId, const QByteArray& data); + + void requestPublicKey(); + void requestPrivateKey(); + void requestPeerInfo(quint64 peerId); signals: void connected(); @@ -39,14 +44,24 @@ signals: void unbound(); void received(quint64 peerId, const QByteArray& data); void errorOccurred(uint8_t code, const QString& message); + void publicKeyReceived(const QByteArray& key); // 32 байта X25519 + void privateKeyReceived(const QByteArray& key); // 32 байта X25519 + void peerInfoReceived(quint64 peerId, const QByteArray& x25519Pubkey, + const QByteArray& ed25519Pubkey); private: static constexpr size_t kMaxMsgSize = 8192; static constexpr uint8_t kCtlBind = 0x01; static constexpr uint8_t kCtlUnbind = 0x02; + static constexpr uint8_t kCtlGetPubkey = 0x03; + static constexpr uint8_t kCtlGetPrivkey = 0x04; + static constexpr uint8_t kCtlGetPeerInfo = 0x05; static constexpr uint8_t kDataSend = 0x10; static constexpr uint8_t kDataRecv = 0x11; static constexpr uint8_t kRspBindOk = 0xE1; + static constexpr uint8_t kRspPubkey = 0xE2; + static constexpr uint8_t kRspPrivkey = 0xE3; + static constexpr uint8_t kRspPeerInfo = 0xE4; static constexpr uint8_t kRspError = 0xFF; static constexpr uint8_t kOptSigned = 0x01; static constexpr size_t kHdrSize = 12; diff --git a/tools/chatgui/zxing-cpp/.clang-format b/tools/chatgui/zxing-cpp/.clang-format new file mode 100644 index 00000000..50580847 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.clang-format @@ -0,0 +1,43 @@ +--- +Language: Cpp +Standard: c++17 +BasedOnStyle: LLVM + +IndentWidth: 4 +TabWidth: 4 +UseTab: ForContinuationAndIndentation # ForIndentation + +AccessModifierOffset: -4 +ColumnLimit: 135 + +#AlignConsecutiveAssignments: true +AlignConsecutiveBitFields: true +AlignEscapedNewlines: DontAlign +AlignTrailingComments: true + +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +#AllowShortLambdasOnASingleLine: Inline +AllowShortEnumsOnASingleLine: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true + +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterEnum: true + AfterStruct: true + AfterUnion: true + AfterFunction: true + SplitEmptyFunction: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeTernaryOperators: true +ConstructorInitializerAllOnOneLineOrOnePerLine: true +FixNamespaceComments: true +IncludeBlocks: Regroup +KeepEmptyLinesAtTheStartOfBlocks: false +PointerAlignment: Left +ReflowComments: true +SortIncludes: true diff --git a/tools/chatgui/zxing-cpp/.clang-tidy b/tools/chatgui/zxing-cpp/.clang-tidy new file mode 100644 index 00000000..e41d7957 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.clang-tidy @@ -0,0 +1,58 @@ +--- +Checks: > + -*, + bugprone-*, + -bugprone-easily-swappable-parameters, + -bugprone-exception-escape, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-integer-division, + -bugprone-macro-parentheses, + -bugprone-narrowing-conversions, + -bugprone-signed-char-misuse, + -bugprone-use-after-move, + -bugprone-assignment-in-if-condition, + performance-*, + -performance-enum-size, + -performance-move-const-arg, + portability-*, + -portability-avoid-pragma-once, + readability-avoid-const-params-in-decls, + readability-const-return-type, + readability-delete-null-pointer, + readability-deleted-default, + readability-inconsistent-declaration-parameter-name, + readability-misleading-indentation, + readability-misplaced-array-index, + readability-non-const-parameter, + readability-redundant-*, + readability-simplify-*, + -readability-simplify-boolean-expr, + readability-static-*, + -readability-static-definition-in-anonymous-namespace, + readability-string-compare, + readability-suspicious-call-argument, + readability-uniqueptr-delete-release, + readability-use-anyofallof, + -readability-redundant-inline-specifier, + -readability-redundant-member-init, + modernize-deprecated-*, + modernize-loop-convert, + modernize-make-*, + modernize-pass-by-value, + modernize-raw-string-literal, + modernize-redundant-void-arg, + modernize-replace-*, + modernize-return-braced-init-list, + modernize-shrink-to-fit, + modernize-unary-static-assert, + modernize-use-*, + -modernize-use-constraints, + -modernize-use-designated-initializers, + -modernize-use-nodiscard, + -modernize-use-ranges, + -modernize-use-trailing-return-type, + -modernize-use-integer-sign-comparison, + +WarningsAsErrors: '' +HeaderFilterRegex: '^(?!.*zint\.h).*$' +FormatStyle: file diff --git a/tools/chatgui/zxing-cpp/.editorconfig b/tools/chatgui/zxing-cpp/.editorconfig new file mode 100644 index 00000000..f8db50e1 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{cpp,c,h,html,py,kt,cs,qlm}] +indent_style = tab +indent_size = 4 +max_line_length = 135 + +[CMakeLists.txt] +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + diff --git a/tools/chatgui/zxing-cpp/.gitattributes b/tools/chatgui/zxing-cpp/.gitattributes new file mode 100644 index 00000000..3266c089 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.gitattributes @@ -0,0 +1,3 @@ +test/samples export-ignore +zint export-ignore +.git* export-ignore diff --git a/tools/chatgui/zxing-cpp/.github/FUNDING.yml b/tools/chatgui/zxing-cpp/.github/FUNDING.yml new file mode 100644 index 00000000..7be90488 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: axxel +custom: 'www.paypal.com/donate/?hosted_button_id=8LDF4NNQF8Z3L' diff --git a/tools/chatgui/zxing-cpp/.github/create_release_archive.py b/tools/chatgui/zxing-cpp/.github/create_release_archive.py new file mode 100644 index 00000000..237cc044 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/create_release_archive.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +import subprocess +import tempfile +import shutil +import sys +import re +from pathlib import Path + + +def run_command(cmd, cwd=None): + try: + return subprocess.run(cmd, check=True, capture_output=True, text=True, cwd=cwd) + except subprocess.CalledProcessError as e: + print(f"✗ Error {cmd}: {e.stderr}") + sys.exit(1) + + +def main(repo_root=None, output_dir=None): + repo_root = Path.cwd() if repo_root is None else Path(repo_root).resolve() + output_dir = repo_root if output_dir is None else Path(output_dir).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + print(f"Working in repository: {repo_root}") + + # Extract version and optional suffix from core/CMakeLists.txt + cmake_txt = (repo_root / "core" / "CMakeLists.txt").read_text() + version = re.search(r'ZXing VERSION "?([0-9]+\.[0-9]+\.[0-9]+)"?', cmake_txt).group(1) + suffix = re.search(r'ZXING_VERSION_SUFFIX "?([^\)\s\"]*)"?', cmake_txt).group(1) + ver_tag = f"{version}{suffix}" + print(f"Project version from CMake: {ver_tag}") + + # Create output filenames with git tag + output_stem = f"zxing-cpp-{ver_tag}" + output_tgz = output_dir / f"{output_stem}.tar.gz" + output_zip = output_dir / f"{output_stem}.zip" + + # Step 1: Create initial tar ball via git archive + print("\n[1/4] Creating git archive...") + run_command(["git", "archive", f"--prefix={output_stem}/", f"--output={output_tgz}", "HEAD", repo_root]) + print(f"✓ Created git archive: {output_tgz}") + + # Step 2: Extract tar ball into temporary folder + print("\n[2/4] Extracting archive to temporary folder...") + with tempfile.TemporaryDirectory() as temp_dir: + print(f"✓ Created temporary directory: {temp_dir}") + + run_command(["tar", "-xf", str(output_tgz), "-C", temp_dir]) + print(f"✓ Extracted archive to temporary folder") + + # Step 3: Replace files in core/src/libzint/ with files from zint/backend/ + print("\n[3/4] Replacing files in core/src/libzint/ with zint/backend/ files...") + dst_dir = Path(temp_dir) / output_stem / "core" / "src" / "libzint" + src_dir = repo_root / "zint" / "backend" # Use actual repo directory + + # Find all files/symlinks currently in core/src/libzint/ (including subdirectories) + libzint_files = list(dst_dir.rglob("*")) + # Include both regular files and symlinks + libzint_files = [f for f in libzint_files if f.is_file() or f.is_symlink()] + + replaced_count = 0 + not_found_count = 0 + + for libzint_file in libzint_files: + # Get relative path from core/src/libzint/ + rel_path = libzint_file.relative_to(dst_dir) + + # Source path in zint/backend/ + source_file = src_dir / rel_path + + # Only replace if the file exists in zint/backend/ + if source_file.exists() and source_file.is_file(): + # Remove symlink if it exists + if libzint_file.is_symlink(): + libzint_file.unlink() + # Copy the actual file + shutil.copy2(source_file, libzint_file) + replaced_count += 1 + else: + not_found_count += 1 + print(f" - Skipped (not in zint/backend): {rel_path}") + + print(f"✓ Replaced {replaced_count} files, skipped {not_found_count} files") + + # Step 4: Create tar.gz and zip archives + print("\n[4/4] Creating archives (tar.gz and zip)...") + run_command(["tar", "-czf", str(output_tgz), "-C", temp_dir, output_stem]) + + if (output_zip.exists()): + output_zip.unlink() + + run_command(["zip", "-q", "-r", str(output_zip), output_stem], cwd=temp_dir) + + # Cleanup initial tar file + print(f"\n✅ Done! Source archives created:") + print(f" - {output_tgz}") + print(f" - {output_zip}") + + +if __name__ == "__main__": + repo_root = sys.argv[1] if len(sys.argv) > 1 else None + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + main(repo_root=repo_root, output_dir=output_dir) diff --git a/tools/chatgui/zxing-cpp/.github/workflows/ci.yml b/tools/chatgui/zxing-cpp/.github/workflows/ci.yml new file mode 100644 index 00000000..602de571 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/ci.yml @@ -0,0 +1,435 @@ +name: CI + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + # don't cancel all jobs just because one of them failed + fail-fast: false + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Setup Python 3 + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Configure + run: > + cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON + -DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_DOTNET=ON -DZXING_TEST_GO=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=ON + -DZXING_PYTHON_MODULE=ON + + - name: Build + run: cmake --build build --parallel --config ${{env.BUILD_TYPE}} + + - name: Set PATH for Tests + shell: bash # to make the $GITHUB_PATH update work + if: runner.os == 'Windows' + run: | + echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH + echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH + + - name: Test + run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}} + + - name: Install + run: | + cmake -E make_directory install + cmake --install build --config ${{env.BUILD_TYPE}} --prefix ${{github.workspace}}/install + + - uses: actions/upload-artifact@v6 + with: + name: ${{matrix.os}}-artifacts + path: install + + build-exp: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest, ubuntu-24.04-arm, macos-15-intel, windows-11-arm] + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Setup Python 3 + id: setup-python + if: matrix.os == 'windows-11-arm' # the default python on the arm runner is an x64 build + uses: actions/setup-python@v6 + with: + python-version: '3.13' + architecture: arm64 + + - name: Configure + run: > + cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON + -DZXING_READERS=ON -DZXING_WRITERS=BOTH -DZXING_EXPERIMENTAL_API=ON + -DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=OFF -DZXING_PYTHON_MODULE=ON + ${{ steps.setup-python.outputs.python-path != '' && format('-DPython_EXECUTABLE={0}', steps.setup-python.outputs.python-path) || '' }} + + - name: Build + run: cmake --build build --parallel --config ${{env.BUILD_TYPE}} + + - name: Set PATH for Tests + shell: bash # to make the $GITHUB_PATH update work + if: runner.os == 'Windows' + run: | + echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH + echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH + + - name: Test + run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}} + + - name: Install + run: | + cmake -E make_directory install + cmake --install build --config ${{env.BUILD_TYPE}} --prefix ${{github.workspace}}/install + + - uses: actions/upload-artifact@v6 + with: + name: ${{matrix.os}}-exp-artifacts + path: install + + build-ubuntu-gcc-12: # keep homebrew happy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Configure + run: > + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DZXING_WRITERS=BOTH + -DCMAKE_CXX_COMPILER=g++-12 -DCMAKE_C_COMPILER=gcc-12 + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest -V --test-dir build + + build-ubuntu-asan-ubsan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Configure + run: > + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DZXING_WRITERS=BOTH + -DZXING_BLACKBOX_TESTS=ON -DZXING_UNIT_TESTS=ON -DZXING_PYTHON_MODULE=OFF -DZXING_C_API=OFF + -DCMAKE_CXX_COMPILER=clang++ + -DCMAKE_CXX_FLAGS="-march=native -fsanitize=address,undefined -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer" + -DCMAKE_C_COMPILER=clang + -DCMAKE_C_FLAGS="-march=native -fsanitize=address,undefined -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer" + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest -V --test-dir build + + build-ubuntu-tsan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Configure + run: > + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DZXING_WRITERS=OFF -DZXING_EXAMPLES=OFF + -DZXING_BLACKBOX_TESTS=ON -DZXING_UNIT_TESTS=OFF -DZXING_PYTHON_MODULE=OFF -DZXING_C_API=OFF + -DCMAKE_CXX_COMPILER=clang++ + -DCMAKE_CXX_FLAGS="-march=native -fsanitize=thread -fno-optimize-sibling-calls -fno-omit-frame-pointer -g" + -DCMAKE_C_COMPILER=clang + -DCMAKE_C_FLAGS="-march=native -fsanitize=thread -fno-optimize-sibling-calls -fno-omit-frame-pointer -g" + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest -V --test-dir build + + build-old-writers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Configure + run: > + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo + -DZXING_READERS=ON -DZXING_WRITERS=OLD -DZXING_PYTHON_MODULE=ON -DZXING_C_API=ON + -DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=ON -DZXING_PYTHON_MODULE=ON + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest -V --test-dir build + + build-ios: + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Build the swift package + run: swift build + + - name: Build the demo app + working-directory: wrappers/ios/demo + run: xcodebuild build -scheme demo -sdk "iphonesimulator" + + # - name: Validate the Pod + # run: pod lib lint --allow-warnings + + build-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - name: Build the lib/app + working-directory: wrappers/android + run: ./gradlew assembleRelease + + - uses: actions/upload-artifact@v6 + with: + name: android-artifacts + path: "wrappers/android/zxingcpp/build/outputs/aar/zxingcpp-release.aar" + + - name: Publish Library Snapshot + if: github.repository == 'zxing-cpp/zxing-cpp' && github.event_name != 'pull_request' + working-directory: wrappers/android + env: + ORG_GRADLE_PROJECT_publishSnapshot: true + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} + run: ./gradlew publishToMavenCentral --no-configuration-cache + + build-kn: + runs-on: ubuntu-latest + defaults: + run: + working-directory: wrappers/kn + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: true + + - name: Prepare Zig toolchain + uses: mlugg/setup-zig@v2 + with: + version: 0.13.0 + + - name: Checkout toolchain initializer repository + uses: actions/checkout@v6 + with: + repository: ISNing/kn-toolchain-initializer + path: wrappers/kn/.kn-toolchain-initializer + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: 17 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v5 + + - name: Initialize Kotlin/Native toolchain + working-directory: wrappers/kn/.kn-toolchain-initializer + run: ./gradlew build -DkotlinVersion=2.2.0 + + - name: Run test for linuxX64 target + run: | + echo -e "konan.dir=$HOME/.konan/kotlin-native-prebuilt-linux-x86_64-2.2.0" >> local.properties + ./gradlew linuxX64Test + + build-python: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ['3.12'] + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install numpy pillow build + + - name: Build module + working-directory: wrappers/python + run: python -m pip -v install -e . + + - name: Test module + working-directory: wrappers/python + run: python -m unittest -v + + - name: Build wheel + working-directory: wrappers/python + run: python -m build --wheel + + - uses: actions/upload-artifact@v6 + with: + name: ${{matrix.os}}-python-artifacts + path: wrappers/python/dist/* + + build-rust: + runs-on: ubuntu-latest + defaults: + run: + working-directory: wrappers/rust + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Lint + run: | + cargo fmt --check + cargo clippy -- -Dwarnings + + - name: Build + run: cargo build --release --verbose --all-features --examples + + - name: Test + run: cargo test --release --all-features + + - name: Package + # --allow-dirty is required on the windows build (but not the ubuntu build?!) + run: | + cargo package --all-features + cargo publish --dry-run + + - uses: actions/upload-artifact@v6 + with: + name: rust-artifacts + path: wrappers/rust/target/package/*.crate + + build-wasm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - uses: emscripten-core/setup-emsdk@v15 + + - name: Configure + run: emcmake cmake -Swrappers/wasm -Bbuild + + - name: Build + run: cmake --build build --parallel + +# - name: Test +# run: node build/EmGlueTests.js + + - uses: actions/upload-artifact@v6 + with: + name: wasm-artifacts + path: | + build/zxing* + build/demo* + +# disable WinRT build for now, as it does not compile with C++20 (see wrappers/winrt/CMakeLists.txt) + # build-winrt: + # runs-on: windows-latest + + # steps: + # - uses: actions/checkout@v6 + # with: + # submodules: true + + # - name: Configure + # shell: cmd # powershell messes up the arguments containing a '.' ?!? + # run: > + # cmake -S wrappers/winrt -B build -A ARM64 + # -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_BUILD_TYPE=Release + # -DBUILD_WINRT_LIB=ON -DZXING_EXAMPLES=OFF -DZXING_BLACKBOX_TESTS=OFF -DZXING_C_API=OFF + # -DEXTENSION_SDK_OUTPUT=dist/UAP/v0.8.0.0/ExtensionSDKs/ZXingWinRT/1.0.0.0 + + # - name: Build + # run: cmake --build build -j8 --config Release + + # - uses: actions/upload-artifact@v6 + # with: + # name: winrt-ARM64-artifacts + # path: build/dist + + build-tarball: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Build + run: python .github/create_release_archive.py + + - name: Find generated archives + id: find + run: | + tgz=$(ls zxing-cpp-*.tar.gz 2>/dev/null | head -n1 || true) + zip=$(ls zxing-cpp-*.zip 2>/dev/null | head -n1 || true) + echo "tgz=$tgz" >> $GITHUB_OUTPUT + echo "zip=$zip" >> $GITHUB_OUTPUT + + - name: Upload source_artifacts + uses: actions/upload-artifact@v6 + with: + name: source_artifacts + path: | + ${{ steps.find.outputs.tgz }} + ${{ steps.find.outputs.zip }} diff --git a/tools/chatgui/zxing-cpp/.github/workflows/codeql-analysis.yml b/tools/chatgui/zxing-cpp/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..59b3b74f --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/codeql-analysis.yml @@ -0,0 +1,59 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: CodeQL + +on: + workflow_dispatch: + # push: + # branches: [ master ] + # pull_request: + # # The branches below must be a subset of the branches above + # branches: [ master ] + # schedule: + # - cron: '18 21 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] # , 'java-kotlin', 'swift' currently fail with autobuild, 'python' is not worth the extra build time + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: true + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: manual + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + - name: Build + run: | + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DZXING_EXPERIMENTAL_API=ON + cmake --build build -j + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/tools/chatgui/zxing-cpp/.github/workflows/msvc-analysis.yml b/tools/chatgui/zxing-cpp/.github/workflows/msvc-analysis.yml new file mode 100644 index 00000000..f491908a --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/msvc-analysis.yml @@ -0,0 +1,71 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# +# Find more information at: +# https://github.com/microsoft/msvc-code-analysis-action + +name: Microsoft C++ Code Analysis + +on: workflow_dispatch + +# on: +# push: +# branches: [ "master" ] +# pull_request: +# branches: [ "master" ] +# schedule: +# - cron: '34 6 * * 1' + +env: + # Path to the CMake build directory. + build: '${{ github.workspace }}/build' + +permissions: + contents: read + +jobs: + analyze: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Analyze + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: true + + - name: Configure CMake + run: cmake -B ${{ env.build }} -DZXING_EXPERIMENTAL_API=ON -DZXING_DISABLE_PCH=ON + + # Build is not required unless generated source files are used + # - name: Build CMake + # run: cmake --build ${{ env.build }} + + - name: Initialize MSVC Code Analysis + uses: microsoft/msvc-code-analysis-action@v0.1.1 + # Provide a unique ID to access the sarif output path + id: run-analysis + with: + cmakeBuildDirectory: ${{ env.build }} + # Ruleset file that will determine what checks will be run + ruleset: NativeRecommendedRules.ruleset + + # Upload SARIF file to GitHub Code Scanning Alerts + # this currently fails with the Error: The CodeQL Action does not support uploading multiple SARIF runs with the same category. + # - name: Upload SARIF to GitHub + # uses: github/codeql-action/upload-sarif@v4 + # with: + # sarif_file: ${{ steps.run-analysis.outputs.sarif }} + + # Upload SARIF file as an Artifact to download and view + - name: Upload SARIF as an Artifact + uses: actions/upload-artifact@v6 + with: + name: sarif-file + path: ${{ steps.run-analysis.outputs.sarif }} diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-android.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-android.yml new file mode 100644 index 00000000..c1ebb3f4 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-android.yml @@ -0,0 +1,41 @@ +name: publish-android + +on: +# release: +# types: [published] + + workflow_dispatch: + inputs: + publish: + description: 'Publish package (y/n)' + default: 'n' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' +# working-directory: wrappers/android +# run: ./gradlew versionDisplay + + - name: Build Library + working-directory: wrappers/android + run: ./gradlew assembleRelease + + - name: Publish Library + if: ${{ github.event.inputs.publish == 'y' }} + working-directory: wrappers/android + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} + run: ./gradlew publishToMavenCentral --no-configuration-cache + + +# To actually publish the the blob, login to https://central.sonatype.com/publishing/deployments. diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-dotnet.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-dotnet.yml new file mode 100644 index 00000000..b68823ee --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-dotnet.yml @@ -0,0 +1,130 @@ +name: publish-dotnet + +on: +# release: +# types: [published] + + workflow_dispatch: + inputs: + publish: + description: 'Publish package (y/n)' + default: 'n' + +jobs: + build-native: + name: build-native ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + lib_name: ZXing.dll + rid: win-x64 + lib_path: build/core/Release/ZXing.dll + - os: windows-11-arm + lib_name: ZXing.dll + rid: win-arm64 + lib_path: build/core/Release/ZXing.dll + - os: ubuntu-latest + lib_name: libZXing.so + rid: linux-x64 + lib_path: build/core/libZXing.so + - os: ubuntu-24.04-arm + lib_name: libZXing.so + rid: linux-arm64 + lib_path: build/core/libZXing.so + - os: macos-15-intel + lib_name: libZXing.dylib + rid: osx-x64 + lib_path: build/core/libZXing.dylib + - os: macos-latest + lib_name: libZXing.dylib + rid: osx-arm64 + lib_path: build/core/libZXing.dylib + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Configure CMake + run: > + cmake -S . -B build + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=ON + -DZXING_EXAMPLES=OFF + -DZXING_TEST_DOTNET=ON + + - name: Build + run: cmake --build build --config Release --parallel + + - name: Test + run: ctest --test-dir build -V -C Release -R DotNetTest + + - name: Stage native library + run: | + cmake -E make_directory artifact/runtimes/${{ matrix.rid }}/native + cmake -E copy_if_different ${{ matrix.lib_path }} artifact/runtimes/${{ matrix.rid }}/native/${{ matrix.lib_name }} + + - name: Upload native library + uses: actions/upload-artifact@v6 + with: + name: native-${{ matrix.rid }} + path: artifact + + publish-nuget: + needs: build-native + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Download native libraries + uses: actions/download-artifact@v8 + with: + pattern: native-* + path: wrappers/dotnet + merge-multiple: true + + - name: Validate runtime files + shell: bash + run: | + test -f wrappers/dotnet/runtimes/win-x64/native/ZXing.dll + test -f wrappers/dotnet/runtimes/win-arm64/native/ZXing.dll + test -f wrappers/dotnet/runtimes/linux-x64/native/libZXing.so + test -f wrappers/dotnet/runtimes/linux-arm64/native/libZXing.so + test -f wrappers/dotnet/runtimes/osx-x64/native/libZXing.dylib + test -f wrappers/dotnet/runtimes/osx-arm64/native/libZXing.dylib + + - name: Pack NuGet package + run: dotnet pack wrappers/dotnet/ZXingCpp/ZXingCpp.csproj --configuration Release --output wrappers/dotnet/artifacts + + - name: Smoke test local NuGet package + shell: bash + run: | + set -euo pipefail + pkg_file="$(ls wrappers/dotnet/artifacts/ZXingCpp.*.nupkg | head -n 1)" + pkg_version="$(basename "$pkg_file" | sed -E 's/^ZXingCpp\.//; s/\.nupkg$//')" + + tmp_dir="$(mktemp -d)" + dotnet new console -n ZxingPkgTest -o "$tmp_dir/ZxingPkgTest" + echo "Console.WriteLine(global::ZXingCpp.BarcodeFormat.QRCode.ToString());" > "$tmp_dir/ZxingPkgTest/Program.cs" + + dotnet add "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" package ZXingCpp --version "$pkg_version" --no-restore + dotnet restore "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" \ + --source "${{ github.workspace }}/wrappers/dotnet/artifacts" \ + --source https://api.nuget.org/v3/index.json + dotnet run --project "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" --no-restore + + - name: Upload NuGet package artifact + uses: actions/upload-artifact@v6 + with: + name: dotnet-nuget-package + path: wrappers/dotnet/artifacts/*.nupkg + + - name: Push NuGet package + if: ${{ github.event_name == 'release' || github.event.inputs.publish == 'y' }} + run: dotnet nuget push wrappers/dotnet/artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-gh-pages.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-gh-pages.yml new file mode 100644 index 00000000..eb5fbad0 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-gh-pages.yml @@ -0,0 +1,131 @@ +name: publish-gh-pages + +on: + # Runs on pushes targeting the default branch +# push: +# branches: ["master"] + + # Runs on new releases + release: + types: [published] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-wasm: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: true + + - name: Setup emsdk + uses: emscripten-core/setup-emsdk@v15 + # with: + # # Make sure to set a version number! + # version: 1.38.40 + # # This is the name of the cache folder. + # # The cache folder will be placed in the build directory, + # # so make sure it doesn't conflict with anything! + # actions-cache-folder: 'emsdk-cache' + + - name: Configure + run: emcmake cmake -Swrappers/wasm -Bbuild + + - name: Build + run: cmake --build build --parallel + + - name: Prepare Archive + shell: sh + run: | + mkdir -p public/demo + mv build/zxing_* build/*.html public/demo/ + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + path: 'public' + name: 'artifact-wasm' + + build-docs: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Install dependencies + run: brew install doxygen + + - name: Configure + run: cmake -S . -B build + + - name: Build docs + run: cmake --build build --target docs + + - name: Prepare deploy folder + run: | + mkdir -p public/docs + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/} + mkdir -p public/docs/$VERSION + cp -r build/docs/html/* public/docs/$VERSION/ + else + mkdir -p public/docs/latest + cp -r build/docs/html/* public/docs/latest/ + fi + + - uses: actions/upload-artifact@v7 + with: + path: public + name: 'artifact-docs' + + deploy: + needs: [build-wasm, build-docs] + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + + permissions: + contents: read + pages: write + id-token: write + + steps: + - name: Download WASM artifact + uses: actions/download-artifact@v8 + with: + name: artifact-wasm + path: wasm + + - name: Download Docs artifact + uses: actions/download-artifact@v8 + with: + name: artifact-docs + path: docs + + - name: Merge artifacts + run: | + mkdir public + cp -r wasm/* public/ + cp -r docs/* public/ + + - name: Upload merged artifact + uses: actions/upload-pages-artifact@v4 + with: + path: public + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-kn.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-kn.yml new file mode 100644 index 00000000..eb8857a9 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-kn.yml @@ -0,0 +1,97 @@ +name: publish-kn + +on: + workflow_dispatch: + inputs: + publish: + description: 'Publish package (y/n)' + default: 'n' + +jobs: + publish: + name: Library Publish + + runs-on: macos-latest # at least macos-13 is required to enable c++20 support + + defaults: + run: + working-directory: ./wrappers/kn + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: true + + - name: Prepare Zig toolchain + uses: mlugg/setup-zig@v2 + with: + version: 0.13.0 + + - name: Checkout toolchain initializer repository + uses: actions/checkout@v6 + with: + repository: ISNing/kn-toolchain-initializer + path: wrappers/kn/.kn-toolchain-initializer + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: 17 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v5 + + - name: Initialize Kotlin/Native toolchain + working-directory: wrappers/kn/.kn-toolchain-initializer + run: ./gradlew build -DkotlinVersion=2.2.0 + + - name: Export Toolchain properties + run: | + echo -e "konan.dir=$HOME/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0" >> local.properties + + - name: Build Library + run: | + export PATH="$PATH:$(brew --prefix llvm@15)/bin" + ./gradlew assemble + + - name: Run All Library Tests + run: | + export PATH="$PATH:$(brew --prefix llvm@15)/bin" + ./gradlew allTests + + - name: Upload Library Test Reports + uses: actions/upload-artifact@v6 + with: + name: allTests-reports + path: wrappers/kn/build/reports/tests/allTests + + - name: Upload Built LinuxArm64 shared library + uses: actions/upload-artifact@v6 + with: + name: linuxArm64-shared + path: wrappers/kn/build/cmake/libZXing/linuxArm64 + + - name: Upload Built LinuxX64 shared library + uses: actions/upload-artifact@v6 + with: + name: linuxX64-shared + path: wrappers/kn/build/cmake/libZXing/linuxX64 + + - name: Publish Library + if: ${{ github.event.inputs.publish == 'y' }} + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }} + run: | + export PATH="$PATH:$(brew --prefix llvm@15)/bin" + ./gradlew publishToMavenCentral + +# To actually publish the blob, login to https://central.sonatype.com/publishing/deployments. diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-python.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-python.yml new file mode 100644 index 00000000..0a3b2018 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-python.yml @@ -0,0 +1,100 @@ +name: publish-python + +on: +# release: +# types: [published] + + workflow_dispatch: + inputs: + publish: + description: 'Publish package (y/n)' + default: 'n' +# push: +# branches: [master] +# tags: ["v*.*.*"] +# pull_request: +# branches: [master] + +jobs: + build-wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest, macos-15-intel, ubuntu-24.04-arm, windows-11-arm] + + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install cibuildwheel + run: python3 -m pip install cibuildwheel==3.3.0 + + - name: Build wheels + run: python3 -m cibuildwheel --output-dir wheelhouse wrappers/python + + - name: Upload wheels + uses: actions/upload-artifact@v6 + with: + name: cibw-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + build-sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Install dependencies + working-directory: wrappers/python + run: python -m pip install --upgrade build + + - name: Build sdist + working-directory: wrappers/python + run: python3 -m build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v6 + with: + name: cibw-sdist + path: wrappers/python/dist/*.tar.gz + + upload-pypi: + name: Upload to PyPI + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/zxing-cpp + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + # only run if the commit is tagged... +# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'release' || github.event.inputs.publish == 'y' + steps: + - uses: actions/download-artifact@v8 + with: + pattern: cibw-* + path: dist + merge-multiple: true + + - name: List wheels + run: ls dist + + - uses: pypa/gh-action-pypi-publish@release/v1 +# with: +# repository-url: https://test.pypi.org/legacy/ diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-source.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-source.yml new file mode 100644 index 00000000..a668bbe5 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-source.yml @@ -0,0 +1,39 @@ +name: publish-source + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + publish-source: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + + - name: Create source archives + run: python .github/create_release_archive.py + + - name: Create test samples tarball + run: tar -czf test_samples.tar.gz test/samples + + - name: Upload workflow artifact + if: github.event_name != 'release' + uses: actions/upload-artifact@v6 + with: + name: source-archives + path: | + *.zip + *.gz + if-no-files-found: error + + - name: Upload to release + if: github.event_name == 'release' + run: | + tag='${{ github.event.release.tag_name }}' + version="${tag#v}" + gh release upload --clobber "$tag" "$GITHUB_WORKSPACE/zxing-cpp-$version.tar.gz" "$GITHUB_WORKSPACE/zxing-cpp-$version.zip" "$GITHUB_WORKSPACE/test_samples.tar.gz" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tools/chatgui/zxing-cpp/.github/workflows/publish-winrt.yml b/tools/chatgui/zxing-cpp/.github/workflows/publish-winrt.yml new file mode 100644 index 00000000..9d9a1d98 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.github/workflows/publish-winrt.yml @@ -0,0 +1,82 @@ +name: publish-winrt +on: +# release: +# types: [published] + + workflow_dispatch: + inputs: + publish: + description: 'Publish package (y/n)' + default: 'n' + package_version: + description: 'Package version (major.minor.patch)' + default: '1.0.0' + +jobs: + build: + runs-on: windows-latest + strategy: + matrix: + target: [Win32, x64, ARM64] + + steps: + - uses: actions/checkout@v6 + + - name: Create build environment + shell: cmd + run: | + cmake -E make_directory ${{runner.workspace}}/build + cmake -E copy_directory ${{github.workspace}}/wrappers/winrt/UAP ${{runner.workspace}}/build/dist/UAP + cmake -E copy_directory ${{github.workspace}}/wrappers/winrt/nuget ${{runner.workspace}}/build/dist/nuget + + - name: Configure CMake + shell: cmd + working-directory: ${{runner.workspace}}/build + run: cmake ${{github.workspace}}/wrappers/winrt -A ${{matrix.target}} -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_BUILD_TYPE=Release -DBUILD_WINRT_LIB=ON -DZXING_EXAMPLES=OFF -DZXING_BLACKBOX_TESTS=OFF -DEXTENSION_SDK_OUTPUT=dist/UAP/v0.8.0.0/ExtensionSDKs/ZXingWinRT/1.0.0.0 + + - name: Build + shell: cmd + working-directory: ${{runner.workspace}}/build + run: cmake --build . -j8 --config Release + + - uses: actions/upload-artifact@v6 + with: + name: winrt-${{matrix.target}}-artifacts + path: ${{runner.workspace}}/build/dist + + publish-package: + needs: build + runs-on: windows-latest + if: ${{ github.event_name == 'release' || github.event.inputs.publish == 'y' }} + steps: + - uses: actions/download-artifact@v8 + with: + name: winrt-Win32-artifacts + - uses: actions/download-artifact@v8 + with: + name: winrt-x64-artifacts + - uses: actions/download-artifact@v8 + with: + name: winrt-ARM64-artifacts + + - name: Create NuGet package + if: ${{ github.event_name != 'release' }} + shell: cmd + run: nuget pack -Version ${{ github.event.inputs.package_version }} -OutputFileNamesWithoutVersion nuget/ZXingWinRT.nuspec + + - name: Create NuGet package + if: ${{ github.event_name == 'release' }} + shell: bash + env: + RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} + run: nuget pack -Version ${RELEASE_TAG_NAME:1} -OutputFileNamesWithoutVersion nuget/ZXingWinRT.nuspec + + - name: Publish NuGet package + shell: cmd + run: nuget push huycn.zxingcpp.winrt.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json + + - uses: actions/upload-artifact@v6 + with: + name: nuget-package + path: huycn.zxingcpp.winrt.nupkg + diff --git a/tools/chatgui/zxing-cpp/.gitignore b/tools/chatgui/zxing-cpp/.gitignore new file mode 100644 index 00000000..bf9ffaf6 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.gitignore @@ -0,0 +1,27 @@ +*.o +*.so +*.lib +*.d +*.a +compile_commands.json +/build + +# QtCreator +CMakeLists.txt.user + +# Vim +*.swp + +# XCode/Swift +.swiftpm +.build + +# JetBrain (AndroidStudio, clion) +.idea + +# Visual Studio +/.vs +/CMakeUserPresets.json + +# Visual Studio Code +/.vscode diff --git a/tools/chatgui/zxing-cpp/.gitmodules b/tools/chatgui/zxing-cpp/.gitmodules new file mode 100644 index 00000000..23eb3946 --- /dev/null +++ b/tools/chatgui/zxing-cpp/.gitmodules @@ -0,0 +1,4 @@ +[submodule "zint"] + path = zint + url = https://github.com/zint/zint.git + shallow = true diff --git a/tools/chatgui/zxing-cpp/CMakeLists.txt b/tools/chatgui/zxing-cpp/CMakeLists.txt new file mode 100644 index 00000000..d6ee235d --- /dev/null +++ b/tools/chatgui/zxing-cpp/CMakeLists.txt @@ -0,0 +1,121 @@ +cmake_minimum_required(VERSION 3.16) + +project(ZXing) + +option (ZXING_READERS "Build with reader support (decoders)" ON) +set (ZXING_WRITERS "ON" CACHE STRING "Build with old and/or new writer (encoder) backend (OFF/ON/OLD/NEW/BOTH)") +option (ZXING_C_API "Build the C-API" ON) +option (ZXING_EXPERIMENTAL_API "Build with experimental API" OFF) +option (ZXING_EXAMPLES "Build the example barcode reader/writer applications" ON) +option (ZXING_EXAMPLES_QT "Build the Qt based example barcode reader/writer applications" OFF) +option (ZXING_BLACKBOX_TESTS "Build the black box reader/writer tests" OFF) +option (ZXING_UNIT_TESTS "Build the unit tests (don't enable for production builds)" OFF) +option (ZXING_TEST_DOTNET "Test if the .NET wrapper works" OFF) +option (ZXING_TEST_GO "Test if the Go wrapper works" OFF) +option (ZXING_TEST_INSTALL "Test if the installed/public API works" OFF) +option (ZXING_PYTHON_MODULE "Build the python module" OFF) +set (ZXING_DEPENDENCIES "AUTO" CACHE STRING "Fetch from github or use locally installed (AUTO/GITHUB/LOCAL)") +option (ZXING_CLANG_TIDY "Enable clang-tidy analysis" OFF) + +if (WIN32) + option (BUILD_SHARED_LIBS "Build and link as shared library" OFF) +else() + option (BUILD_SHARED_LIBS "Build and link as shared library" ON) +endif() + +if (MSVC) + add_definitions (-DUNICODE -D_UNICODE -D_CRT_SECURE_NO_WARNINGS) + option (ZXING_LINK_CPP_STATICALLY "MSVC only, link standard library statically (/MT and /MTd)" OFF) + if (LINK_CPP_STATICALLY OR ZXING_LINK_CPP_STATICALLY) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() +endif() + +if (NOT CMAKE_BUILD_TYPE) + set (DEFAULT_BUILD_TYPE "Release") + message (STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.") + set (CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) + set_property (CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif() + +if (NOT (ZXING_READERS OR ZXING_WRITERS)) + message(FATAL_ERROR "At least one of ZXING_READERS/ZXING_WRITERS must be enabled.") +endif() + +set(ZXING_WRITERS_LIST OFF ON OLD NEW BOTH) +set_property(CACHE ZXING_WRITERS PROPERTY STRINGS ${ZXING_WRITERS_LIST}) +if(NOT ZXING_WRITERS IN_LIST ZXING_WRITERS_LIST) + message(FATAL_ERROR "ZXING_WRITERS must be one of ${ZXING_WRITERS_LIST}") +endif() + +set(ZXING_DEPENDENCIES_LIST AUTO GITHUB LOCAL) +set_property(CACHE ZXING_DEPENDENCIES PROPERTY STRINGS ${ZXING_DEPENDENCIES_LIST}) +if(NOT ZXING_DEPENDENCIES IN_LIST ZXING_DEPENDENCIES_LIST) + message(FATAL_ERROR "ZXING_DEPENDENCIES must be one of ${ZXING_DEPENDENCIES_LIST}") +endif() + +if (NOT DEFINED CMAKE_CXX_STANDARD) + set (CMAKE_CXX_STANDARD 20) + set (CMAKE_CXX_STANDARD_REQUIRED ON) + set (CMAKE_CXX_EXTENSIONS OFF) +endif() + +set(CMAKE_COLOR_DIAGNOSTICS ON) + +if (ZXING_CLANG_TIDY) + find_program(CMAKE_CXX_CLANG_TIDY NAMES "clang-tidy" DOC "Path to clang-tidy executable") + if (NOT CMAKE_CXX_CLANG_TIDY) + message(WARNING "clang-tidy not found.") + else() + message(STATUS "clang-tidy found: ${CMAKE_CXX_CLANG_TIDY}") + set(ZXING_DISABLE_PCH ON) + endif() +endif() + +# make installed binaries relocatable on macOS and Linux +set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) # improve build reproducibility +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) +set(CMAKE_INSTALL_RPATH "$<$:@loader_path/../lib>$<$:$ORIGIN/../lib>") + +add_subdirectory (core) +add_subdirectory (docs) + +enable_testing() + +include(zxing.cmake) + +if (ZXING_EXAMPLES) + add_subdirectory (example) +endif() +if (ZXING_EXAMPLES_QT) + add_subdirectory (wrappers/qt) +endif() +if (ZXING_BLACKBOX_TESTS) + add_subdirectory (test/blackbox) +endif() +if (ZXING_UNIT_TESTS) + add_subdirectory (test/unit) +endif() +if (ZXING_TEST_DOTNET) + add_subdirectory (wrappers/dotnet) +endif() +if (ZXING_TEST_GO) + add_subdirectory (wrappers/go) +endif() +if (ZXING_TEST_INSTALL) + add_test( + NAME InstallTest + COMMAND + ${CMAKE_COMMAND} + -DPROJECT_BINARY_DIR=${CMAKE_BINARY_DIR} + -DSOURCE_DIR=${CMAKE_SOURCE_DIR}/test/install + -DZXING_WRITERS=${ZXING_WRITERS} + -P ${CMAKE_SOURCE_DIR}/test/install/run.cmake + ) +endif() +if (ZXING_PYTHON_MODULE) + add_subdirectory (wrappers/python) +endif() +if (ZXING_C_API) + add_subdirectory (wrappers/c) +endif() diff --git a/tools/chatgui/zxing-cpp/LICENSE b/tools/chatgui/zxing-cpp/LICENSE new file mode 100644 index 00000000..8f71f43f --- /dev/null +++ b/tools/chatgui/zxing-cpp/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/tools/chatgui/zxing-cpp/Package.swift b/tools/chatgui/zxing-cpp/Package.swift new file mode 100644 index 00000000..edfacec4 --- /dev/null +++ b/tools/chatgui/zxing-cpp/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version:5.7.1 +import PackageDescription + +let package = Package( + name: "ZXingCpp", + platforms: [ + .macOS(.v13), .iOS(.v12) + ], + products: [ + .library( + name: "ZXingCpp", + targets: ["ZXingCpp"]) + ], + targets: [ + .target( + name: "ZXingCppCore", + path: "core/src", + exclude: ["libzint", "ZXingC.cpp", "ZXingCpp.cpp"], + publicHeadersPath: ".", + cxxSettings: [ + .headerSearchPath("../../wrappers/ios/Sources/Wrapper"), + .define("ZXING_INTERNAL") + ] + ), + .target( + name: "ZXingCpp", + dependencies: ["ZXingCppCore"], + path: "wrappers/ios/Sources/Wrapper", + publicHeadersPath: ".", + linkerSettings: [ + .linkedFramework("CoreGraphics"), + .linkedFramework("CoreImage"), + .linkedFramework("CoreVideo") + ] + ) + ], + cxxLanguageStandard: CXXLanguageStandard.cxx20 +) diff --git a/tools/chatgui/zxing-cpp/README.md b/tools/chatgui/zxing-cpp/README.md new file mode 100644 index 00000000..659cbf71 --- /dev/null +++ b/tools/chatgui/zxing-cpp/README.md @@ -0,0 +1,154 @@ +[![Build Status](https://github.com/zxing-cpp/zxing-cpp/actions/workflows/ci.yml/badge.svg)](https://github.com/zxing-cpp/zxing-cpp/actions?query=workflow%3ACI) + +# ZXing-C++ + +ZXing-C++ ("zebra crossing") is an open-source, multi-format linear/matrix barcode image processing library implemented in C++. + +It was originally ported from the Java [ZXing library](https://github.com/zxing/zxing) but has been developed further and now includes many improvements in terms of runtime and detection performance. It can both read and write barcodes in a number of formats. Since version 3.0 the default writing backend is provided by the [zint library](https://sourceforge.net/projects/zint/). + +## Features + +* Written in pure C++20 (public API is C++17 compatible), no third-party dependencies (for the library itself) +* Thread safe +* Wrappers/Bindings for: + + | | | | + |:-|:-|:-| + | [Android](wrappers/android/README.md) | [C](wrappers/c/README.md) | [Go](wrappers/go/README.md) | + | [iOS](wrappers/ios/README.md) | [Kotlin/Native](wrappers/kn/README.md) | [.NET](wrappers/dotnet/README.md) | + | [Python](wrappers/python/README.md) | [Qt](wrappers/qt/README.md) | [Rust](wrappers/rust/README.md) | + | [WebAssembly](wrappers/wasm/README.md) | [WinRT](wrappers/winrt/README.md) | | + | External projects: | + | [Flutter](https://pub.dev/packages/flutter_zxing) | [Julia](https://github.com/rakeshksr/ZXingCPP.jl) | | + + +## Supported Formats + +| Symbology | Variants | +|:----------|:---------| +| ***Retail:*** | *(Point-of-Sale, Coupons)* +| EAN/UPC | EAN-13, EAN-8, EAN-5ᵂ, EAN-2ᵂ, UPC-A, UPC-E, ISBN +| DataBar | Omnidirectional, Stacked, Limited, Expanded, Expanded Stacked +| ***Industrial:*** | *(Logistics, Tracking, Pharma)* +| Code39 | Standard, Extended, PZN, Code32, (VIN, LOGMARS) +| Code93 | +| Code128 | +| ITF | ITF-14, (DHL Leitcode, DHL Identcode) +| Telepen | Telepen Alpha, Telepen Numeric +| ***Matrix:*** | *(Documents, Tickets, Logistics, IDs)* +| Aztec Code | Aztec Code, Aztec Rune +| Data Matrix | ECC200 +| MaxiCode | (partial read support) +| PDF417 | PDF417, Compact PDF417, MicroPDF417 +| QR Code | Model 1ᴿ, Model 2, Micro QR Code, rMQR +| ***Other:*** | *(Legacy, Niche)* +| Codabar | +| DXFilmEdge | + + +[Note:] + * ᵂ : write support only + * ᴿ : read support only + * DataBar used to be called RSS. + * DataBar, DX Film Edge, MaxiCode, Micro QR Code and rMQR Code are not supported for writing if the library is configured with `ZXING_WRITERS=OLD`. + +## Sponsors + +You can sponsor this library at [GitHub Sponsors](https://github.com/sponsors/axxel). + +| | Named Sponsors: | +|:-:|:-| +| [![ArAllAll](https://www.alliedvision.com/favicon.ico)](https://www.alliedvision.com/) | [Allied Vision Konstanz GmbH](https://www.alliedvision.com/) | +| [![jknlsn](https://avatars.githubusercontent.com/u/9172090?s=32)](https://github.com/jknlsn) | [Jake Nelson](https://github.com/jknlsn) | +| [![KURZ](https://avatars.githubusercontent.com/u/25196688?s=32)](https://github.com/kurzdigital) | [KURZ Digital Solutions GmbH & Co. KG](https://github.com/kurzdigital) | +| [![Moonshine AI](https://avatars.githubusercontent.com/u/98664891?s=32)](https://github.com/moonshine-ai) | [Moonshine AI](https://github.com/moonshine-ai) | +| [![SAP](https://avatars.githubusercontent.com/u/2531208?s=32)](https://www.sap.com/germany/about/company/innovation/open-source.html) | [SAP (Open Source Program Office)](https://www.sap.com/germany/about/company/innovation/open-source.html) | +| [![Somco Software](https://avatars.githubusercontent.com/u/41477867?s=32)](https://github.com/somcosoftware) | [Somco Software](https://somcosoftware.com/en) | +| [![synedra](https://www.synedra.com/favicon.ico)](https://synedra.com/) | [synedra information technologies GmbH](https://synedra.com/) | + +Thanks a lot for your contribution! + +## Getting Started + +### To read barcodes: +1. Load your image into memory (3rd-party library required). +2. Call `ReadBarcodes()` from [`ReadBarcode.h`](core/src/ReadBarcode.h), the simplest API to get a list of `Barcode` objects. + +A very simple example looks like this: +```c++ +#include "ZXing/ZXingCpp.h" +#include + +int main(int argc, char** argv) +{ + int width, height; + unsigned char* data; + // load your image data from somewhere. ImageFormat::Lum assumes grey scale image data. + + auto image = ZXing::ImageView(data, width, height, ZXing::ImageFormat::Lum); + auto options = ZXing::ReaderOptions().formats(ZXing::BarcodeFormat::QRCode); + auto barcodes = ZXing::ReadBarcodes(image, options); + + for (const auto& b : barcodes) + std::cout << ZXing::ToString(b.format()) << ": " << b.text() << "\n"; + + return 0; +} +``` +To see the full capability of the API, have a look at [`ZXingReader.cpp`](example/ZXingReader.cpp). + +### To write barcodes: +1. Create a `Barcode` object with `CreateBarcodeFrom...()` from [`CreateBarcode.h`](core/src/CreateBarcode.h). +2. The `Barcode::symbol()` can be used to get access to the bit matrix (1 module == 1 pixel, no quiet zone) +3. Alternatively the 3 `WriteBarcodeTo...()` functions from [`WriteBarcode.h`](core/src/WriteBarcode.h) can be used to create an `Image`, a SVG string or a UTF-8 string representation. + +A very simple example looks like this: +```c++ +#include "ZXing/ZXingCpp.h" +#include + +int main(int argc, char** argv) +{ + auto barcode = ZXing::CreateBarcodeFromText("some text", ZXing::BarcodeFormat::QRCode); + auto svg = ZXing::WriteBarcodeToSVG(barcode); + + // see also ZXing::WriteBarcodeToImage() + + std::cout << svg << "\n"; + + return 0; +} +``` + +As an example for how to parameterize the process with `CreatorOptions` and `WriterOptions`, have a look at [`ZXingWriter.cpp`](example/ZXingWriter.cpp). + +### Online API documentation + +The latest API documentation can be found here: https://zxing-cpp.github.io/zxing-cpp/docs/latest + +## Web Demos +- [Read barcodes](https://zxing-cpp.github.io/zxing-cpp/demo/demo_reader.html) +- [Write barcodes](https://zxing-cpp.github.io/zxing-cpp/demo/demo_writer.html) +- [Read barcodes from camera](https://zxing-cpp.github.io/zxing-cpp/demo/demo_cam_reader.html) + +[Note: those live demos are not necessarily fully up-to-date at all times.] + +## Build Instructions +These are the generic instructions to build the library on Windows/macOS/Linux. For details on how to build the individual wrappers, follow the links above. + +1. Make sure [CMake](https://cmake.org) version 3.16 or newer is installed. The python module requires 3.18 or higher. +2. Make sure a sufficiently C++20 compliant compiler is installed (minimum VS 2019 16.10? / gcc 11 / clang 12?). +3. See the cmake `ZXING_...` options to enable the testing code, python wrapper, etc. + +``` +git clone https://github.com/zxing-cpp/zxing-cpp.git --recursive --depth 1 +cmake -S zxing-cpp -B zxing-cpp/build -DCMAKE_BUILD_TYPE=Release +cmake --build zxing-cpp/build --parallel --config Release +``` + +[Note: ready to use packages are available for/as +[vcpkg](https://github.com/Microsoft/vcpkg/tree/master/ports/nu-book-zxing-cpp), +[conan](https://github.com/conan-io/conan-center-index/tree/master/recipes/zxing-cpp), +[mingw](https://github.com/msys2/MINGW-packages/tree/master/mingw-w64-zxing-cpp), +[homebrew](https://formulae.brew.sh/formula/zxing-cpp) and a bunch of +[linux distributions](https://repology.org/project/zxing-cpp/versions).] diff --git a/tools/chatgui/zxing-cpp/docs/CMakeLists.txt b/tools/chatgui/zxing-cpp/docs/CMakeLists.txt new file mode 100644 index 00000000..ad1cbd35 --- /dev/null +++ b/tools/chatgui/zxing-cpp/docs/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.16) + +project(ZXingDocs VERSION 3.1.0) + +find_package(Doxygen) + +if(DOXYGEN_FOUND) + include(FetchContent) + FetchContent_Declare( + doxygen-awesome-css + URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/heads/main.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + FetchContent_MakeAvailable(doxygen-awesome-css) + + # Save the location the files were cloned into + # This allows us to get the path to doxygen-awesome.css + FetchContent_GetProperties(doxygen-awesome-css SOURCE_DIR AWESOME_CSS_DIR) + + # Generate the Doxyfile + set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) + set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) + configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) + + add_custom_target(docs + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + VERBATIM) +endif() diff --git a/tools/chatgui/zxing-cpp/docs/Doxyfile.in b/tools/chatgui/zxing-cpp/docs/Doxyfile.in new file mode 100644 index 00000000..45af1b31 --- /dev/null +++ b/tools/chatgui/zxing-cpp/docs/Doxyfile.in @@ -0,0 +1,1373 @@ +# Doxyfile 1.16.1 + +# This file describes the settings to be used by the documentation system +# Doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use Doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use Doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "zxing-cpp" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = "v@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@" + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewers a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# With the PROJECT_ICON tag one can specify an icon that is included in the tabs +# when the HTML document is shown. Doxygen will copy the logo to the output +# directory. + +PROJECT_ICON = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where Doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the ALLOW_UNICODE_NAMES tag is set to YES, Doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# If the BRIEF_MEMBER_DESC tag is set to YES, Doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = NO + +# If the REPEAT_BRIEF tag is set to YES, Doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, Doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, Doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = NO + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which Doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where Doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = @CMAKE_SOURCE_DIR@/core/src/ + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen will interpret the +# first line (until the first dot, question mark or exclamation mark) of a +# Javadoc-style comment as the brief description. If set to NO, the Javadoc- +# style will behave just like regular Qt-style comments (thus requiring an +# explicit @brief command for a brief description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then Doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by Doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will interpret the first +# line (until the first dot, question mark or exclamation mark) of a Qt-style +# comment as the brief description. If set to NO, the Qt-style will behave just +# like regular Qt-style comments (thus requiring an explicit \brief command for +# a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and Doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# Doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as Doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then Doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# If the MARKDOWN_SUPPORT tag is enabled then Doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by Doxygen, so you can +# mix Doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# If the MARKDOWN_STRICT tag is enabled then Doxygen treats text in comments as +# Markdown formatted also in cases where Doxygen's native markup format +# conflicts with that of Markdown. This is only relevant in cases where +# backticks are used. Doxygen's native markup style allows a single quote to end +# a text fragment started with a backtick and then treat it as a piece of quoted +# text, whereas in Markdown such text fragment is treated as verbatim and only +# ends when a second matching backtick is found. Also, Doxygen's native markup +# format requires double quotes to be escaped when they appear in a backtick +# section, whereas this is not needed for Markdown. +# The default value is: YES. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_STRICT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 6. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 6 + +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + +# When enabled Doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. Words listed in the +# AUTOLINK_IGNORE_WORDS tag are excluded from automatic linking. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# This tag specifies a list of words that, when matching the start of a word in +# the documentation, will suppress auto links generation, if it is enabled via +# AUTOLINK_SUPPORT. This list does not affect links explicitly created using # +# or the \link or \ref commands. +# This tag requires that the tag AUTOLINK_SUPPORT is set to YES. + +AUTOLINK_IGNORE_WORDS = + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let Doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also makes the inheritance and +# collaboration diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then Doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, Doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_UNDOC_NAMESPACES tag is set to YES, Doxygen will hide all +# undocumented namespaces that are normally visible in the namespace hierarchy. +# If set to NO, these namespaces will be included in the various overviews. This +# option has no effect if EXTRACT_ALL is enabled. +# The default value is: YES. + +HIDE_UNDOC_NAMESPACES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES Doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and macOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. + +CASE_SENSE_NAMES = SYSTEM + +# If the HIDE_SCOPE_NAMES tag is set to NO then Doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then Doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then Doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then Doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = NO + +# If the SORT_BRIEF_DOCS tag is set to YES then Doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then Doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then Doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and Doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING Doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = NO + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = NO + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= NO + +# The GENERATE_REQUIREMENTS tag can be used to enable (YES) or disable (NO) the +# requirements page. When enabled, this page is automatically created when at +# least one comment block with a \requirement command appears in the input. +# The default value is: YES. + +GENERATE_REQUIREMENTS = NO + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = NO + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by Doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents Doxygen's defaults, run Doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run Doxygen from a directory containing a file called +# DoxygenLayout.xml, Doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by Doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by Doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then Doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, Doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, Doxygen will warn about incomplete +# function parameter documentation. If set to NO, Doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, Doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, Doxygen will warn about +# undocumented enumeration values. If set to NO, Doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + +# If WARN_LAYOUT_FILE option is set to YES, Doxygen will warn about issues found +# while parsing the user defined layout file, such as missing or wrong elements. +# See also LAYOUT_FILE for details. If set to NO, problems with the layout file +# will be suppressed. +# The default value is: YES. + +WARN_LAYOUT_FILE = YES + +# If the WARN_AS_ERROR tag is set to YES then Doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then Doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the Doxygen process Doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then Doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined Doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that Doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of Doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = @CMAKE_SOURCE_DIR@/core/src/Barcode.h \ + @CMAKE_SOURCE_DIR@/core/src/BarcodeFormat.h \ + @CMAKE_SOURCE_DIR@/core/src/ReaderOptions.h \ + @CMAKE_SOURCE_DIR@/core/src/CreateBarcode.h \ + @CMAKE_SOURCE_DIR@/core/src/WriteBarcode.h \ + @CMAKE_SOURCE_DIR@/core/src/ReadBarcode.h \ + @CMAKE_SOURCE_DIR@/core/src/ImageView.h \ + @CMAKE_SOURCE_DIR@/core/src/Quadrilateral.h \ + @CMAKE_SOURCE_DIR@/core/src/Point.h \ + @CMAKE_SOURCE_DIR@/core/src/Error.h \ + @CMAKE_SOURCE_DIR@/core/src/ContentType.h \ + @CMAKE_SOURCE_DIR@/core/src/CharacterSet.h \ + @CMAKE_SOURCE_DIR@/core/src/ZXingCpp.h \ + # core/src/ZXingC.h \ + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by Doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.ccm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, +# *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, +# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be +# provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.h + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which Doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# ANamespace::AClass, ANamespace::*Test + +EXCLUDE_SYMBOLS = ZX_* + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the Doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +# If the IMPLICIT_DIR_DOCS tag is set to YES, any README.md file found in sub- +# directories of the project's root, is used as the documentation for that sub- +# directory, except when the README.md starts with a \dir, \page or \mainpage +# command. If set to NO, the README.md file needs to start with an explicit \dir +# command in order to be used as directory documentation. +# The default value is: YES. + +IMPLICIT_DIR_DOCS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = NO + +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, Doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank Doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that Doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that Doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of Doxygen. For a description +# of the possible markers and block names see the documentation. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank Doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that Doxygen normally uses. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank Doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that Doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by Doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. + +HTML_EXTRA_STYLESHEET = @AWESOME_CSS_DIR@/doxygen-awesome.css \ + # @AWESOME_CSS_DIR@/doxygen-awesome-sidebar-only.css + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generates light mode output, DARK always +# generates dark mode output, AUTO_LIGHT automatically sets the mode according +# to the user preference, uses light mode if no preference is set (the default), +# AUTO_DARK automatically sets the mode according to the user preference, uses +# dark mode if no preference is set and TOGGLE allows a user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. + +HTML_COLORSTYLE = AUTO_LIGHT + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. + +HTML_DYNAMIC_SECTIONS = YES + +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. + +HTML_CODE_FOLDING = YES + +# If the HTML_COPY_CLIPBOARD tag is set to YES then Doxygen will show an icon in +# the top right corner of code and text fragments that allows the user to copy +# its content to the clipboard. Note this only works if supported by the browser +# and the web page is served via a secure context (see: +# https://www.w3.org/TR/secure-contexts/), i.e. using the https: or file: +# protocol. +# The default value is: YES. + +HTML_COPY_CLIPBOARD = YES + +# Doxygen stores a couple of settings persistently in the browser (via e.g. +# cookies). By default these settings apply to all HTML pages generated by +# Doxygen across all projects. The HTML_PROJECT_COOKIE tag can be used to store +# the settings under a project specific key, such that the user preferences will +# be stored separately. + +HTML_PROJECT_COOKIE = + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. + +HTML_INDEX_NUM_ENTRIES = 100 + + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by Doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has more details information than the tab index, you +# could consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: YES. + +GENERATE_TREEVIEW = YES + +# When GENERATE_TREEVIEW is set to YES, the PAGE_OUTLINE_PANEL option determines +# if an additional navigation panel is shown at the right hand side of the +# screen, displaying an outline of the contents of the main page, similar to +# e.g. https://developer.android.com/reference If GENERATE_TREEVIEW is set to +# NO, this option has no effect. +# The default value is: YES. + +PAGE_OUTLINE_PANEL = YES + +# When GENERATE_TREEVIEW is set to YES, the FULL_SIDEBAR option determines if +# the side bar is limited to only the treeview area (value NO) or if it should +# extend to the full height of the window (value YES). Setting this to YES gives +# a layout similar to e.g. https://docs.readthedocs.io with more room for +# contents, but less room for the project logo, title, and description. If +# GENERATE_TREEVIEW is set to NO, this option has no effect. +# The default value is: NO. + +FULL_SIDEBAR = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# Doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. + +ENUM_VALUES_PER_LINE = 0 + +# When the SHOW_ENUM_VALUES tag is set doxygen will show the specified +# enumeration values besides the enumeration mnemonics. +# The default value is: NO. + +SHOW_ENUM_VALUES = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, Doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. + +EXT_LINKS_IN_WINDOW = NO + +# If the OBFUSCATE_EMAILS tag is set to YES, Doxygen will obfuscate email +# addresses. +# The default value is: YES. + +OBFUSCATE_EMAILS = YES + + +# When the SEARCHENGINE tag is enabled Doxygen will generate a search box for +# the HTML output. The underlying search engine uses JavaScript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the JavaScript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /