#include "LoBBSDal.h" #include "configuration.h" #include "gps/RTC.h" #include #include #include #include // Helper: normalize username to lowercase for case-insensitive lookups static void normalizeUsername(const char *username, char *normalized) { size_t len = strlen(username); if (len > LOBBS_MAX_USERNAME_LEN) len = LOBBS_MAX_USERNAME_LEN; for (size_t i = 0; i < len; i++) { normalized[i] = tolower(username[i]); } normalized[len] = '\0'; } static void buildNewsReadKey(uint64_t newsUuid, uint64_t userUuid, char *out, size_t outSize) { char newsHex[17]; char userHex[17]; lodb_uuid_to_hex(newsUuid, newsHex); lodb_uuid_to_hex(userUuid, userHex); snprintf(out, outSize, "%s:%s", newsHex, userHex); } static lodb_uuid_t usernameToUuid(const char *username, uint32_t hostNodeId) { char normalized[LOBBS_USERNAME_BUFFER_SIZE]; normalizeUsername(username, normalized); return lodb_new_uuid(normalized, hostNodeId); } LoBBSDal::LoBBSDal(uint32_t hostNodeId) : hostNodeId(hostNodeId) { // Initialize LoDB database db = new LoDb("lobbs"); db->registerTable("users", &meshtastic_LoBBSUser_msg, sizeof(meshtastic_LoBBSUser)); db->registerTable("sessions", &meshtastic_LoBBSSession_msg, sizeof(meshtastic_LoBBSSession)); db->registerTable("mail", &meshtastic_LoBBSMail_msg, sizeof(meshtastic_LoBBSMail)); db->registerTable("news", &meshtastic_LoBBSNews_msg, sizeof(meshtastic_LoBBSNews)); db->registerTable("news_reads", &meshtastic_LoBBSNewsRead_msg, sizeof(meshtastic_LoBBSNewsRead)); } LoBBSDal::~LoBBSDal() { delete db; } bool LoBBSDal::isValidUsername(const char *username) { // Must start with a letter if (!isalpha(username[0])) { return false; } // Check rest of characters: alphanumeric or underscore only for (size_t i = 1; username[i] != '\0'; i++) { if (!isalnum(username[i]) && username[i] != '_') { return false; } } return true; } /** * Validate a password. Must be between 5 and 50 characters long and contain only letters, numbers, underscores, and common * special characters. * @param password The password to validate * @return True if the password is valid, false otherwise */ bool LoBBSDal::isValidPassword(const char *password) { // Check each character: alphanumeric, underscore, or common special chars for (size_t i = 0; password[i] != '\0'; i++) { char c = password[i]; if (!isalnum(c) && c != '_' && c != '-' && c != '.' && c != '!' && c != '@' && c != '#' && c != '$' && c != '%') { return false; } } return true; } void LoBBSDal::hashPassword(const char *password, uint8_t *hash) { SHA256 sha256; sha256.reset(); sha256.update(password, strlen(password)); sha256.finalize(hash, 32); } bool LoBBSDal::loadUserByUsername(const char *username, meshtastic_LoBBSUser *user) { // Convert username to UUID with host node ID as salt lodb_uuid_t userUuid = usernameToUuid(username, hostNodeId); LoDbError err = db->get("users", userUuid, user); if (err == LODB_OK) { LOG_DEBUG("Loaded user by username: %s", username); return true; } LOG_DEBUG("User not found: %s", username); return false; } bool LoBBSDal::loadUserByNodeId(uint32_t nodeId, meshtastic_LoBBSUser *user) { // First, lookup session by node ID (use node ID directly as UUID) lodb_uuid_t sessionUuid = (lodb_uuid_t)nodeId; LOG_DEBUG("sessionUuid: " LODB_UUID_FMT, LODB_UUID_ARGS(sessionUuid)); meshtastic_LoBBSSession session = meshtastic_LoBBSSession_init_zero; LoDbError err = db->get("sessions", sessionUuid, &session); if (err != LODB_OK) { LOG_DEBUG("No session found for node 0x%08x", nodeId); return false; } else { LOG_DEBUG("Session found for node 0x%08x", nodeId); LOG_DEBUG("Session user UUID: " LODB_UUID_FMT, LODB_UUID_ARGS(session.user_uuid)); LOG_DEBUG("Session last login time: %d", session.last_login_time); LOG_DEBUG("Session node ID: %08x", nodeId); } // Now load the user by UUID from session err = db->get("users", session.user_uuid, user); if (err == LODB_OK) { LOG_DEBUG("Loaded user by node ID: 0x%08x -> UUID: " LODB_UUID_FMT, nodeId, LODB_UUID_ARGS(session.user_uuid)); return true; } LOG_DEBUG("User UUID not found: " LODB_UUID_FMT, LODB_UUID_ARGS(session.user_uuid)); return false; } bool LoBBSDal::createUser(const char *username, const char *password, uint32_t nodeId) { // Calculate UUID first (username with host node ID as salt) lodb_uuid_t userUuid = usernameToUuid(username, hostNodeId); // Create user record meshtastic_LoBBSUser user = meshtastic_LoBBSUser_init_zero; strncpy(user.username, username, sizeof(user.username) - 1); user.uuid = userUuid; user.password_hash.size = 32; hashPassword(password, user.password_hash.bytes); LoDbError err = db->insert("users", userUuid, &user); if (err != LODB_OK) { LOG_ERROR("Failed to create user: %s", username); return false; } LOG_INFO("Created user: %s", username); // Log in the user (create session) return loginUser(username, nodeId); } bool LoBBSDal::verifyPassword(const meshtastic_LoBBSUser *user, const char *password) { uint8_t providedHash[32]; hashPassword(password, providedHash); return memcmp(user->password_hash.bytes, providedHash, 32) == 0; } bool LoBBSDal::loginUser(const char *username, uint32_t nodeId) { // Create session record meshtastic_LoBBSSession session = meshtastic_LoBBSSession_init_zero; session.user_uuid = usernameToUuid(username, hostNodeId); session.node_id = nodeId; session.last_login_time = getTime(); // Use node ID directly as UUID lodb_uuid_t sessionUuid = (lodb_uuid_t)nodeId; // Delete existing session if any (upsert pattern) db->deleteRecord("sessions", sessionUuid); // Insert new session LoDbError err = db->insert("sessions", sessionUuid, &session); if (err != LODB_OK) { LOG_ERROR("Failed to create session for node 0x%08x", nodeId); return false; } LOG_INFO("Created session for user %s (UUID: " LODB_UUID_FMT ") on node 0x%08x", username, LODB_UUID_ARGS(session.user_uuid), nodeId); return true; } bool LoBBSDal::logoutUser(uint32_t nodeId) { // Use node ID directly as UUID lodb_uuid_t sessionUuid = (lodb_uuid_t)nodeId; LoDbError err = db->deleteRecord("sessions", sessionUuid); if (err == LODB_OK) { LOG_INFO("Logged out node 0x%08x", nodeId); return true; } LOG_WARN("No session found to log out for node 0x%08x", nodeId); return false; } uint64_t LoBBSDal::getUserUuidByUsername(const char *username) { // Convert username to UUID with host node ID as salt uint64_t userUuid = usernameToUuid(username, hostNodeId); // Verify user exists meshtastic_LoBBSUser user = meshtastic_LoBBSUser_init_zero; LoDbError err = db->get("users", userUuid, &user); if (err == LODB_OK) { LOG_DEBUG("Found user UUID for %s: " LODB_UUID_FMT, username, LODB_UUID_ARGS(userUuid)); return userUuid; } LOG_DEBUG("User not found: %s", username); return 0; } bool LoBBSDal::sendMail(uint64_t fromUserUuid, uint64_t toUserUuid, const char *message) { // Generate a unique UUID for the mail message (using timestamp and recipient UUID) lodb_uuid_t mailUuid = lodb_new_uuid((const char *)&toUserUuid, getTime()); // Create mail record meshtastic_LoBBSMail mail = meshtastic_LoBBSMail_init_zero; mail.uuid = mailUuid; mail.from_user_uuid = fromUserUuid; mail.to_user_uuid = toUserUuid; strncpy(mail.message, message, sizeof(mail.message) - 1); mail.message[sizeof(mail.message) - 1] = '\0'; mail.timestamp = getTime(); mail.read = false; LoDbError err = db->insert("mail", mailUuid, &mail); if (err != LODB_OK) { LOG_ERROR("Failed to send mail from " LODB_UUID_FMT " to " LODB_UUID_FMT, LODB_UUID_ARGS(fromUserUuid), LODB_UUID_ARGS(toUserUuid)); return false; } LOG_INFO("Sent mail from " LODB_UUID_FMT " to " LODB_UUID_FMT, LODB_UUID_ARGS(fromUserUuid), LODB_UUID_ARGS(toUserUuid)); return true; } // Comparator for sorting mail by timestamp descending (newest first) static int compareMailByTimestamp(const void *a, const void *b) { const meshtastic_LoBBSMail *m1 = (const meshtastic_LoBBSMail *)a; const meshtastic_LoBBSMail *m2 = (const meshtastic_LoBBSMail *)b; // Reverse order: newer (larger timestamp) first if (m2->timestamp > m1->timestamp) return 1; if (m2->timestamp < m1->timestamp) return -1; return 0; } std::vector LoBBSDal::getMailForUser(uint64_t userUuid, uint32_t offset, uint32_t limit) { // Build filter lambda for mail matching recipient auto mail_filter = [userUuid](const void *rec) -> bool { const meshtastic_LoBBSMail *m = (const meshtastic_LoBBSMail *)rec; return m->to_user_uuid == userUuid; }; // Execute select with filter and sort auto allMail = db->select("mail", mail_filter, compareMailByTimestamp); // Apply offset and limit std::vector result; for (size_t i = offset; i < allMail.size() && i < offset + limit; i++) { result.push_back(allMail[i]); } // Free records not included in result for (size_t i = 0; i < allMail.size(); i++) { if (i < offset || i >= offset + limit) { delete[] (uint8_t *)allMail[i]; } } LOG_DEBUG("Retrieved %d mail messages for user " LODB_UUID_FMT " (offset=%d, limit=%d)", result.size(), LODB_UUID_ARGS(userUuid), offset, limit); return result; } bool LoBBSDal::markMailAsRead(uint64_t mailUuid) { // Load the mail record meshtastic_LoBBSMail mail = meshtastic_LoBBSMail_init_zero; LoDbError err = db->get("mail", mailUuid, &mail); if (err != LODB_OK) { LOG_WARN("Mail not found: " LODB_UUID_FMT, LODB_UUID_ARGS(mailUuid)); return false; } // Update read flag mail.read = true; // Delete old record and insert updated one db->deleteRecord("mail", mailUuid); err = db->insert("mail", mailUuid, &mail); if (err != LODB_OK) { LOG_ERROR("Failed to mark mail as read: " LODB_UUID_FMT, LODB_UUID_ARGS(mailUuid)); return false; } LOG_DEBUG("Marked mail as read: " LODB_UUID_FMT, LODB_UUID_ARGS(mailUuid)); return true; } bool LoBBSDal::postNews(uint64_t authorUserUuid, const char *message) { // Generate a unique UUID for the news item lodb_uuid_t newsUuid = lodb_new_uuid(nullptr, authorUserUuid ^ (uint64_t)getTime()); // Create news record meshtastic_LoBBSNews news = meshtastic_LoBBSNews_init_zero; news.uuid = newsUuid; news.author_user_uuid = authorUserUuid; strncpy(news.message, message, sizeof(news.message) - 1); news.message[sizeof(news.message) - 1] = '\0'; news.timestamp = getTime(); LoDbError err = db->insert("news", newsUuid, &news); if (err != LODB_OK) { LOG_ERROR("Failed to post news from " LODB_UUID_FMT, LODB_UUID_ARGS(authorUserUuid)); return false; } LOG_INFO("Posted news from " LODB_UUID_FMT, LODB_UUID_ARGS(authorUserUuid)); return true; } bool LoBBSDal::isNewsReadByUser(uint64_t newsUuid, uint64_t userUuid) { char key[35]; buildNewsReadKey(newsUuid, userUuid, key, sizeof(key)); lodb_uuid_t readUuid = lodb_new_uuid(key, 0); meshtastic_LoBBSNewsRead readRecord = meshtastic_LoBBSNewsRead_init_zero; LoDbError err = db->get("news_reads", readUuid, &readRecord); return (err == LODB_OK); } bool LoBBSDal::markNewsAsRead(uint64_t newsUuid, uint64_t userUuid) { if (isNewsReadByUser(newsUuid, userUuid)) { LOG_DEBUG("News " LODB_UUID_FMT " already marked as read by user " LODB_UUID_FMT, LODB_UUID_ARGS(newsUuid), LODB_UUID_ARGS(userUuid)); return true; } char key[35]; buildNewsReadKey(newsUuid, userUuid, key, sizeof(key)); lodb_uuid_t readUuid = lodb_new_uuid(key, 0); meshtastic_LoBBSNewsRead readRecord = meshtastic_LoBBSNewsRead_init_zero; readRecord.news_uuid = newsUuid; readRecord.user_uuid = userUuid; readRecord.read_timestamp = getTime(); LoDbError err = db->insert("news_reads", readUuid, &readRecord); if (err != LODB_OK) { LOG_ERROR("Failed to mark news as read: " LODB_UUID_FMT, LODB_UUID_ARGS(newsUuid)); return false; } LOG_DEBUG("Marked news " LODB_UUID_FMT " as read by user " LODB_UUID_FMT, LODB_UUID_ARGS(newsUuid), LODB_UUID_ARGS(userUuid)); return true; } std::vector LoBBSDal::getNewsForUser(uint64_t userUuid, uint32_t offset, uint32_t limit) { auto allNews = db->select("news", nullptr, nullptr); std::vector newsWithStatus; newsWithStatus.reserve(allNews.size()); for (auto *newsPtr : allNews) { auto *news = (meshtastic_LoBBSNews *)newsPtr; bool isRead = isNewsReadByUser(news->uuid, userUuid); newsWithStatus.push_back({news, isRead}); } std::sort(newsWithStatus.begin(), newsWithStatus.end(), [](const LoBBSNewsEntry &a, const LoBBSNewsEntry &b) { if (a.isRead != b.isRead) return !a.isRead; return a.news->timestamp > b.news->timestamp; }); std::vector result; for (size_t i = offset; i < newsWithStatus.size() && i < offset + limit; i++) { result.push_back(newsWithStatus[i]); } for (size_t i = 0; i < newsWithStatus.size(); i++) { if (i < offset || i >= offset + limit) { delete[] (uint8_t *)newsWithStatus[i].news; } } LOG_DEBUG("Retrieved %d news items for user " LODB_UUID_FMT " (offset=%d, limit=%d)", result.size(), LODB_UUID_ARGS(userUuid), offset, limit); return result; }