mirror of
https://github.com/MeshEnvy/lobbs.git
synced 2026-03-28 16:22:33 +01:00
Initial commit
This commit is contained in:
691
LoBBSModule.cpp
Normal file
691
LoBBSModule.cpp
Normal file
@@ -0,0 +1,691 @@
|
||||
#include "LoBBSModule.h"
|
||||
#include "LoBBSDal.h"
|
||||
#include "MeshService.h"
|
||||
#include "airtime.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "lobbs.pb.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Static helper: case-insensitive substring search
|
||||
static const char *stristr(const char *haystack, const char *needle)
|
||||
{
|
||||
if (!*needle)
|
||||
return haystack;
|
||||
|
||||
for (; *haystack; haystack++) {
|
||||
const char *h = haystack;
|
||||
const char *n = needle;
|
||||
while (*h && *n && tolower(*h) == tolower(*n)) {
|
||||
h++;
|
||||
n++;
|
||||
}
|
||||
if (!*n)
|
||||
return haystack;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Static helper: comparator for case-insensitive alphabetical username sorting
|
||||
static int compareUsernames(const void *a, const void *b)
|
||||
{
|
||||
const meshtastic_LoBBSUser *u1 = (const meshtastic_LoBBSUser *)a;
|
||||
const meshtastic_LoBBSUser *u2 = (const meshtastic_LoBBSUser *)b;
|
||||
return strcasecmp(u1->username, u2->username);
|
||||
}
|
||||
|
||||
// Static helper: load a user by UUID
|
||||
static bool loadUserByUuid(LoBBSDal *dal, uint64_t uuid, meshtastic_LoBBSUser *outUser)
|
||||
{
|
||||
auto users = dal->getDb()->select(
|
||||
"users",
|
||||
[uuid](const void *rec) -> bool {
|
||||
const meshtastic_LoBBSUser *u = (const meshtastic_LoBBSUser *)rec;
|
||||
return u->uuid == uuid;
|
||||
},
|
||||
nullptr);
|
||||
|
||||
bool found = false;
|
||||
if (!users.empty()) {
|
||||
*outUser = *(const meshtastic_LoBBSUser *)users[0];
|
||||
found = true;
|
||||
}
|
||||
|
||||
for (auto *userPtr : users) {
|
||||
delete[] (uint8_t *)userPtr;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
// Static helper: format time ago (e.g., "2h ago", "5m ago", "1d ago")
|
||||
static void formatTimeAgo(uint32_t timestamp, char *buffer, size_t bufferSize)
|
||||
{
|
||||
uint32_t now = getTime();
|
||||
if (now < timestamp) {
|
||||
snprintf(buffer, bufferSize, "now");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t diff = now - timestamp;
|
||||
|
||||
if (diff < 60) {
|
||||
snprintf(buffer, bufferSize, "%ds ago", diff);
|
||||
} else if (diff < 3600) {
|
||||
snprintf(buffer, bufferSize, "%dm ago", diff / 60);
|
||||
} else if (diff < 86400) {
|
||||
snprintf(buffer, bufferSize, "%dh ago", diff / 3600);
|
||||
} else {
|
||||
snprintf(buffer, bufferSize, "%dd ago", diff / 86400);
|
||||
}
|
||||
}
|
||||
|
||||
// Static helper: truncate message for list view
|
||||
static void truncateMessage(const char *message, char *buffer, size_t bufferSize, size_t maxLen)
|
||||
{
|
||||
size_t msgLen = strlen(message);
|
||||
if (msgLen <= maxLen) {
|
||||
strncpy(buffer, message, bufferSize - 1);
|
||||
buffer[bufferSize - 1] = '\0';
|
||||
} else {
|
||||
size_t copyLen = maxLen < bufferSize - 4 ? maxLen : bufferSize - 4;
|
||||
strncpy(buffer, message, copyLen);
|
||||
buffer[copyLen] = '\0';
|
||||
strncat(buffer, "...", bufferSize - strlen(buffer) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void freeMailMessages(std::vector<void *> &mailMessages)
|
||||
{
|
||||
for (auto *mailPtr : mailMessages) {
|
||||
delete[] (uint8_t *)mailPtr;
|
||||
}
|
||||
mailMessages.clear();
|
||||
}
|
||||
|
||||
static void freeNewsEntries(std::vector<LoBBSNewsEntry> &newsItems)
|
||||
{
|
||||
for (auto &entry : newsItems) {
|
||||
delete[] (uint8_t *)entry.news;
|
||||
}
|
||||
newsItems.clear();
|
||||
}
|
||||
|
||||
LoBBSModule::LoBBSModule() : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_MESSAGE_APP)
|
||||
{
|
||||
// Create data access layer with host node ID as salt
|
||||
dal = new LoBBSDal(nodeDB->getNodeNum());
|
||||
}
|
||||
|
||||
ProcessMessage LoBBSModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
LOG_DEBUG("LoBBS received DM from=0x%0x, id=%d, msg=%.*s", mp.from, mp.id, mp.decoded.payload.size, mp.decoded.payload.bytes);
|
||||
|
||||
meshtastic_LoBBSUser existingUser = meshtastic_LoBBSUser_init_zero;
|
||||
bool isAuthenticated = dal->loadUserByNodeId(mp.from, &existingUser);
|
||||
if (isAuthenticated) {
|
||||
LOG_DEBUG("User node ID: %08x", mp.from);
|
||||
LOG_DEBUG("User UUID: " LODB_UUID_FMT, LODB_UUID_ARGS(existingUser.uuid));
|
||||
LOG_DEBUG("User: %s", existingUser.username);
|
||||
LOG_DEBUG("User is authenticated: %d", isAuthenticated);
|
||||
} else {
|
||||
LOG_DEBUG("User node ID: %08x", mp.from);
|
||||
LOG_DEBUG("User is not authenticated");
|
||||
}
|
||||
|
||||
// Copy payload to mutable buffer and null terminate (there is no null terminator in the payload)
|
||||
memcpy(msgBuffer, mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||
msgBuffer[mp.decoded.payload.size] = '\0';
|
||||
|
||||
// Check for @mention mail sending BEFORE tokenizing (authenticated users only)
|
||||
// This must happen before strtok destroys the buffer
|
||||
if (isAuthenticated && strchr(msgBuffer, '@')) {
|
||||
LOG_DEBUG("Processing @mention mail from node=0x%0x", mp.from);
|
||||
|
||||
// Extract message content and find all @mentions
|
||||
std::vector<std::string> recipients;
|
||||
std::string messageContent = std::string(msgBuffer);
|
||||
|
||||
// Parse the message for @mentions
|
||||
char *token = msgBuffer;
|
||||
bool foundAny = false;
|
||||
|
||||
while (*token) {
|
||||
if (*token == '@') {
|
||||
foundAny = true;
|
||||
// Found a mention, extract username
|
||||
token++; // Skip @
|
||||
char *usernameStart = token;
|
||||
while (*token && (isalnum(*token) || *token == '_')) {
|
||||
token++;
|
||||
}
|
||||
|
||||
// Save username (only if not already in list)
|
||||
char username[LOBBS_USERNAME_BUFFER_SIZE];
|
||||
size_t usernameLen = token - usernameStart;
|
||||
if (usernameLen > 0 && usernameLen < LOBBS_USERNAME_BUFFER_SIZE) {
|
||||
strncpy(username, usernameStart, usernameLen);
|
||||
username[usernameLen] = '\0';
|
||||
std::string usernameStr(username);
|
||||
// Only add if not already in recipients list
|
||||
if (std::find(recipients.begin(), recipients.end(), usernameStr) == recipients.end()) {
|
||||
recipients.push_back(usernameStr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token++;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAny && !recipients.empty()) {
|
||||
// Validate and send to each recipient
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
std::string failedUsers;
|
||||
|
||||
for (const auto &recipient : recipients) {
|
||||
// Get recipient UUID
|
||||
uint64_t recipientUuid = dal->getUserUuidByUsername(recipient.c_str());
|
||||
if (recipientUuid == 0) {
|
||||
LOG_WARN("User not found: %s", recipient.c_str());
|
||||
if (failCount > 0)
|
||||
failedUsers += ", ";
|
||||
failedUsers += "@" + recipient;
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send mail
|
||||
if (dal->sendMail(existingUser.uuid, recipientUuid, messageContent.c_str())) {
|
||||
LOG_INFO("Sent mail from %s to %s", existingUser.username, recipient.c_str());
|
||||
successCount++;
|
||||
} else {
|
||||
LOG_ERROR("Failed to send mail to %s", recipient.c_str());
|
||||
if (failCount > 0)
|
||||
failedUsers += ", ";
|
||||
failedUsers += "@" + recipient;
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Send confirmation
|
||||
if (successCount > 0 && failCount == 0) {
|
||||
if (successCount == 1) {
|
||||
snprintf(replyBuffer, sizeof(replyBuffer), "Mail sent to @%s", recipients[0].c_str());
|
||||
} else {
|
||||
snprintf(replyBuffer, sizeof(replyBuffer), "Mail sent to %d users", successCount);
|
||||
}
|
||||
sendReply(mp.from, replyBuffer);
|
||||
} else if (successCount > 0 && failCount > 0) {
|
||||
snprintf(replyBuffer, sizeof(replyBuffer), "Mail sent to %d users. Failed: %s", successCount,
|
||||
failedUsers.c_str());
|
||||
sendReply(mp.from, replyBuffer);
|
||||
} else {
|
||||
snprintf(replyBuffer, sizeof(replyBuffer), "Failed to send mail. Users not found: %s", failedUsers.c_str());
|
||||
sendReply(mp.from, replyBuffer);
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
}
|
||||
|
||||
// Tokenize for command processing (this modifies msgBuffer)
|
||||
char *cmdName = strtok(msgBuffer, " ");
|
||||
LOG_DEBUG("Token: %s", cmdName);
|
||||
|
||||
if (strcasecmp(cmdName, "/hi") == 0) {
|
||||
LOG_DEBUG("Processing /hi command from node=0x%0x", mp.from);
|
||||
|
||||
// Get username
|
||||
char *username = strtok(NULL, " ");
|
||||
if (!username) {
|
||||
sendReply(mp.from, "Usage: /hi <username> <password>");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Validate username length
|
||||
size_t usernameLen = strlen(username);
|
||||
if (usernameLen == 0 || usernameLen > LOBBS_MAX_USERNAME_LEN || !dal->isValidUsername(username)) {
|
||||
sendReply(
|
||||
mp.from,
|
||||
"Username not found or is invalid. Username must be 1-" LOBBS_XSTR(
|
||||
LOBBS_MAX_USERNAME_LEN) " characters and contain only letters, numbers, and common special characters.");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Get password (rest of the string)
|
||||
char *password = strtok(NULL, "");
|
||||
if (!password) {
|
||||
sendReply(mp.from, "Usage: /hi <username> <password>");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Validate password is at least 5 characters long
|
||||
if (strlen(password) < 5 || strlen(password) > 50 || !dal->isValidPassword(password)) {
|
||||
sendReply(mp.from, "Password incorrect or invalid. Password must be between 5 and 50 characters long and contain "
|
||||
"only letters, numbers, and common "
|
||||
"special characters.");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Processing /hi command: username=%s from node=0x%0x", username, mp.from);
|
||||
|
||||
// Try to load existing user
|
||||
meshtastic_LoBBSUser existingUser = meshtastic_LoBBSUser_init_zero;
|
||||
bool userExists = dal->loadUserByUsername(username, &existingUser);
|
||||
|
||||
if (userExists) {
|
||||
// User exists - verify password
|
||||
if (dal->verifyPassword(&existingUser, password)) {
|
||||
LOG_DEBUG("User %s authenticated successfully from node 0x%0x", username, mp.from);
|
||||
|
||||
// Log in user (create/update session)
|
||||
if (dal->loginUser(username, mp.from)) {
|
||||
snprintf(replyBuffer, sizeof(replyBuffer), "Welcome back %s!", username);
|
||||
sendReply(mp.from, replyBuffer);
|
||||
} else {
|
||||
sendReply(mp.from, "Error creating session");
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Invalid password for user %s from node 0x%0x", username, mp.from);
|
||||
sendReply(mp.from, "Invalid password");
|
||||
}
|
||||
} else {
|
||||
// New user - create account
|
||||
LOG_DEBUG("Creating new user: %s for node 0x%0x", username, mp.from);
|
||||
|
||||
if (dal->createUser(username, password, mp.from)) {
|
||||
snprintf(replyBuffer, sizeof(replyBuffer), "Welcome %s!", username);
|
||||
sendReply(mp.from, replyBuffer);
|
||||
} else {
|
||||
sendReply(mp.from, "Error creating account");
|
||||
}
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
const char *helpMsg = LOBBS_HEADER "/hi <user> <pass> - Login or create account\n";
|
||||
LOG_DEBUG("Help message: %s", helpMsg);
|
||||
sendReply(mp.from, helpMsg);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* Authenticated commands
|
||||
* ================================
|
||||
*/
|
||||
|
||||
if (strcasecmp(cmdName, "/bye") == 0) {
|
||||
LOG_INFO("Processing /bye command from node=0x%0x", mp.from);
|
||||
|
||||
dal->logoutUser(mp.from);
|
||||
sendReply(mp.from, "Goodbye!");
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
if (strcasecmp(cmdName, "/users") == 0) {
|
||||
LOG_INFO("Processing /users command from node=0x%0x", mp.from);
|
||||
|
||||
char *filterStr = strtok(NULL, " ");
|
||||
|
||||
// Parse optional filter argument
|
||||
if (filterStr && !dal->isValidUsername(filterStr)) {
|
||||
sendReply(mp.from, "Filter must contain only letters, numbers, and common special characters.");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Build filter lambda for username matching (captures filterStr)
|
||||
auto username_filter = [filterStr](const void *rec) -> bool {
|
||||
const meshtastic_LoBBSUser *u = (const meshtastic_LoBBSUser *)rec;
|
||||
return !filterStr || !filterStr[0] || stristr(u->username, filterStr) != nullptr;
|
||||
};
|
||||
|
||||
// Execute synchronous select with filter and sort
|
||||
auto users = dal->getDb()->select("users", username_filter, compareUsernames);
|
||||
|
||||
// Build and send response
|
||||
if (users.empty()) {
|
||||
std::string reply;
|
||||
LOG_DEBUG("No users match '%s'", filterStr);
|
||||
if (filterStr && filterStr[0]) {
|
||||
reply += "No users match '";
|
||||
reply += filterStr;
|
||||
reply += "'";
|
||||
} else {
|
||||
reply += "No users found";
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("# users: %d", users.size());
|
||||
// Build user directory message
|
||||
std::string userListMsg = "User directory:\n";
|
||||
for (size_t i = 0; i < users.size(); i++) {
|
||||
const meshtastic_LoBBSUser *u = (const meshtastic_LoBBSUser *)users[i];
|
||||
if (i > 0) {
|
||||
userListMsg += ", ";
|
||||
}
|
||||
userListMsg += u->username;
|
||||
}
|
||||
LOG_DEBUG("User list message: %s", userListMsg.c_str());
|
||||
|
||||
sendReply(mp.from, userListMsg.c_str());
|
||||
|
||||
// Free allocated records
|
||||
for (auto *userPtr : users) {
|
||||
delete[] (uint8_t *)userPtr;
|
||||
}
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
if (strcasecmp(cmdName, "/mail") == 0) {
|
||||
LOG_INFO("Processing /mail command from node=0x%0x", mp.from);
|
||||
|
||||
// Parse optional arguments
|
||||
char *arg1 = strtok(NULL, " ");
|
||||
|
||||
// Determine action: list (default), list with offset, or read specific message
|
||||
uint32_t offset = 0;
|
||||
int readMessageId = -1;
|
||||
bool isReadCommand = false;
|
||||
|
||||
if (arg1) {
|
||||
if (!isdigit(static_cast<unsigned char>(*arg1))) {
|
||||
sendReply(mp.from, "Invalid mail command");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
char *argCopy = arg1;
|
||||
size_t len = strlen(argCopy);
|
||||
if (len > 0 && argCopy[len - 1] == '-') {
|
||||
// "/mail <n>-" format for listing from offset
|
||||
argCopy[len - 1] = '\0';
|
||||
offset = atoi(argCopy);
|
||||
if (offset > 0)
|
||||
offset--; // Convert to 0-based
|
||||
} else {
|
||||
// "/mail <n>" format for reading message
|
||||
readMessageId = atoi(argCopy);
|
||||
isReadCommand = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get mail for current user
|
||||
const uint32_t MAIL_PAGE_SIZE = 10;
|
||||
auto mailMessages = dal->getMailForUser(existingUser.uuid, offset, MAIL_PAGE_SIZE);
|
||||
|
||||
if (isReadCommand && readMessageId > 0) {
|
||||
// Read specific message
|
||||
if (readMessageId > (int)mailMessages.size()) {
|
||||
sendReply(mp.from, "Invalid message number");
|
||||
freeMailMessages(mailMessages);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
const meshtastic_LoBBSMail *mail = (const meshtastic_LoBBSMail *)mailMessages[readMessageId - 1];
|
||||
|
||||
// Get sender username
|
||||
meshtastic_LoBBSUser sender = meshtastic_LoBBSUser_init_zero;
|
||||
bool foundSender = loadUserByUuid(dal, mail->from_user_uuid, &sender);
|
||||
|
||||
// Format time
|
||||
char timeStr[32];
|
||||
formatTimeAgo(mail->timestamp, timeStr, sizeof(timeStr));
|
||||
|
||||
// Build message
|
||||
std::string reply;
|
||||
reply += "From: @";
|
||||
reply += foundSender ? sender.username : "unknown";
|
||||
reply += " (";
|
||||
reply += timeStr;
|
||||
reply += ")\n";
|
||||
reply += mail->message;
|
||||
|
||||
sendReply(mp.from, reply.c_str());
|
||||
|
||||
// Mark as read
|
||||
dal->markMailAsRead(mail->uuid);
|
||||
|
||||
freeMailMessages(mailMessages);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// List mail (default action)
|
||||
if (mailMessages.empty()) {
|
||||
sendReply(mp.from, "No mail");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Count unread messages
|
||||
int unreadCount = 0;
|
||||
for (auto *mailPtr : mailMessages) {
|
||||
const meshtastic_LoBBSMail *mail = (const meshtastic_LoBBSMail *)mailPtr;
|
||||
if (!mail->read) {
|
||||
unreadCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Build mail list
|
||||
std::string mailList;
|
||||
if (unreadCount > 0) {
|
||||
char unreadStr[32];
|
||||
snprintf(unreadStr, sizeof(unreadStr), "(%d unread)\n", unreadCount);
|
||||
mailList += unreadStr;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < mailMessages.size(); i++) {
|
||||
const meshtastic_LoBBSMail *mail = (const meshtastic_LoBBSMail *)mailMessages[i];
|
||||
|
||||
// Get sender username
|
||||
meshtastic_LoBBSUser sender = meshtastic_LoBBSUser_init_zero;
|
||||
bool foundSender = loadUserByUuid(dal, mail->from_user_uuid, &sender);
|
||||
|
||||
// Format message entry
|
||||
char entryBuffer[256];
|
||||
char timeStr[32];
|
||||
char truncMsg[50];
|
||||
formatTimeAgo(mail->timestamp, timeStr, sizeof(timeStr));
|
||||
truncateMessage(mail->message, truncMsg, sizeof(truncMsg), 25);
|
||||
|
||||
snprintf(entryBuffer, sizeof(entryBuffer), "[%d]%s @%s: %s (%s)\n", (int)(offset + i + 1), mail->read ? "" : "*",
|
||||
foundSender ? sender.username : "unknown", truncMsg, timeStr);
|
||||
|
||||
mailList += entryBuffer;
|
||||
}
|
||||
|
||||
sendReply(mp.from, mailList.c_str());
|
||||
|
||||
freeMailMessages(mailMessages);
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
if (strcasecmp(cmdName, "/news") == 0) {
|
||||
LOG_INFO("Processing /news command from node=0x%0x", mp.from);
|
||||
|
||||
// Parse optional arguments
|
||||
char *arg1 = strtok(NULL, " ");
|
||||
char *arg2 = strtok(NULL, " ");
|
||||
|
||||
uint32_t offset = 0;
|
||||
int readNewsId = -1;
|
||||
bool isReadCommand = false;
|
||||
bool isPostCommand = false;
|
||||
|
||||
if (arg1) {
|
||||
if (strcasecmp(arg1, "r") == 0 && arg2) {
|
||||
readNewsId = atoi(arg2);
|
||||
isReadCommand = true;
|
||||
} else if (strcasecmp(arg1, "l") == 0 && arg2) {
|
||||
offset = atoi(arg2);
|
||||
if (offset > 0)
|
||||
offset--; // Convert to 0-based
|
||||
} else if (isdigit((unsigned char)arg1[0])) {
|
||||
// "/news <n>" or "/news <n>-"
|
||||
char *argCopy = arg1;
|
||||
size_t len = strlen(argCopy);
|
||||
if (len > 0 && argCopy[len - 1] == '-') {
|
||||
argCopy[len - 1] = '\0';
|
||||
offset = atoi(argCopy);
|
||||
if (offset > 0)
|
||||
offset--; // Convert to 0-based
|
||||
} else {
|
||||
readNewsId = atoi(argCopy);
|
||||
isReadCommand = true;
|
||||
}
|
||||
} else {
|
||||
isPostCommand = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPostCommand) {
|
||||
// This is a news post - get the full message from original payload
|
||||
const char *msgStart = (const char *)mp.decoded.payload.bytes;
|
||||
size_t payloadSize = mp.decoded.payload.size;
|
||||
|
||||
// Skip command prefix
|
||||
const char *payloadEnd = msgStart + payloadSize;
|
||||
while (msgStart < payloadEnd && *msgStart != ' ')
|
||||
msgStart++;
|
||||
while (msgStart < payloadEnd && *msgStart == ' ')
|
||||
msgStart++;
|
||||
|
||||
if (msgStart >= payloadEnd || *msgStart == '\0') {
|
||||
sendReply(mp.from, "News message cannot be empty");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
if (!isalpha((unsigned char)*msgStart)) {
|
||||
sendReply(mp.from, "News must start with a letter");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
std::string newsMessage(msgStart, payloadEnd - msgStart);
|
||||
if (dal->postNews(existingUser.uuid, newsMessage.c_str())) {
|
||||
sendReply(mp.from, "News posted");
|
||||
} else {
|
||||
sendReply(mp.from, "Failed to post news");
|
||||
}
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Get news for current user
|
||||
const uint32_t NEWS_PAGE_SIZE = 10;
|
||||
auto newsItems = dal->getNewsForUser(existingUser.uuid, offset, NEWS_PAGE_SIZE);
|
||||
|
||||
if (isReadCommand && readNewsId > 0) {
|
||||
if (readNewsId > (int)newsItems.size()) {
|
||||
sendReply(mp.from, "Invalid news number");
|
||||
freeNewsEntries(newsItems);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
auto &entry = newsItems[readNewsId - 1];
|
||||
const meshtastic_LoBBSNews *news = entry.news;
|
||||
|
||||
// Get author username
|
||||
meshtastic_LoBBSUser author = meshtastic_LoBBSUser_init_zero;
|
||||
bool foundAuthor = loadUserByUuid(dal, news->author_user_uuid, &author);
|
||||
|
||||
char timeStr[32];
|
||||
formatTimeAgo(news->timestamp, timeStr, sizeof(timeStr));
|
||||
|
||||
std::string reply;
|
||||
reply += "From: @";
|
||||
reply += foundAuthor ? author.username : "unknown";
|
||||
reply += " (";
|
||||
reply += timeStr;
|
||||
reply += ")\n";
|
||||
reply += news->message;
|
||||
|
||||
sendReply(mp.from, reply.c_str());
|
||||
|
||||
dal->markNewsAsRead(news->uuid, existingUser.uuid);
|
||||
|
||||
freeNewsEntries(newsItems);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
if (newsItems.empty()) {
|
||||
sendReply(mp.from, "No news");
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
int unreadCount = 0;
|
||||
for (const auto &entry : newsItems) {
|
||||
if (!entry.isRead)
|
||||
unreadCount++;
|
||||
}
|
||||
|
||||
std::string newsList;
|
||||
if (unreadCount > 0) {
|
||||
char unreadStr[32];
|
||||
snprintf(unreadStr, sizeof(unreadStr), "(%d unread)\n", unreadCount);
|
||||
newsList += unreadStr;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < newsItems.size(); i++) {
|
||||
const auto &entry = newsItems[i];
|
||||
const meshtastic_LoBBSNews *news = entry.news;
|
||||
|
||||
meshtastic_LoBBSUser author = meshtastic_LoBBSUser_init_zero;
|
||||
bool foundAuthor = loadUserByUuid(dal, news->author_user_uuid, &author);
|
||||
|
||||
char entryBuffer[256];
|
||||
char timeStr[32];
|
||||
char truncMsg[50];
|
||||
formatTimeAgo(news->timestamp, timeStr, sizeof(timeStr));
|
||||
truncateMessage(news->message, truncMsg, sizeof(truncMsg), 25);
|
||||
|
||||
snprintf(entryBuffer, sizeof(entryBuffer), "[%d]%s @%s: %s (%s)\n", (int)(offset + i + 1), entry.isRead ? "" : "*",
|
||||
foundAuthor ? author.username : "unknown", truncMsg, timeStr);
|
||||
|
||||
newsList += entryBuffer;
|
||||
}
|
||||
|
||||
sendReply(mp.from, newsList.c_str());
|
||||
|
||||
freeNewsEntries(newsItems);
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
std::string helpMsg = LOBBS_HEADER "/bye - Logout\n"
|
||||
"/users [filter] - List users (optional filter)\n"
|
||||
"/mail [<n>|r <n>|l <n>|<n>-] - List/read mail\n"
|
||||
"@user <msg> - Send mail\n"
|
||||
"/news [<n>|r <n>|l <n>|<n>-] - List/read news\n"
|
||||
"/news <msg> - Post news";
|
||||
LOG_DEBUG("Help message: %s", helpMsg.c_str());
|
||||
sendReply(mp.from, helpMsg);
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
void LoBBSModule::sendReply(NodeNum to, const std::string &msg)
|
||||
{
|
||||
meshtastic_MeshPacket *reply = allocDataPacket();
|
||||
static constexpr char truncMarker[] = "[...]";
|
||||
static constexpr size_t truncMarkerLen = sizeof(truncMarker) - 1;
|
||||
|
||||
bool isTruncated = msg.size() > MAX_REPLY_BYTES;
|
||||
size_t payloadSize = isTruncated ? MAX_REPLY_BYTES : msg.size();
|
||||
reply->decoded.payload.size = payloadSize;
|
||||
|
||||
if (isTruncated) {
|
||||
size_t copyLen = MAX_REPLY_BYTES > truncMarkerLen ? MAX_REPLY_BYTES - truncMarkerLen : 0;
|
||||
memcpy(reply->decoded.payload.bytes, msg.c_str(), copyLen);
|
||||
memcpy(reply->decoded.payload.bytes + copyLen, truncMarker, truncMarkerLen);
|
||||
} else {
|
||||
memcpy(reply->decoded.payload.bytes, msg.c_str(), payloadSize);
|
||||
}
|
||||
reply->to = to;
|
||||
reply->decoded.want_response = false;
|
||||
service->sendToMesh(reply);
|
||||
}
|
||||
Reference in New Issue
Block a user