Files
lobbs/apps/News/NewsCommands.cpp
T

264 lines
9.1 KiB
C++

#if !MESHTASTIC_EXCLUDE_LOBBS
#include "NewsCommands.h"
#include "../../LoBBSCommandRegistry.h"
#include "../../LoBBSConfig.h"
#include "../../LoBBSHooks.h"
#include "../../LoBBSModule.h"
#include "../../LoBBSReply.h"
#include "../../LoBBSResponse.h"
#include "../AppUtil.h"
#include "../Msg/MsgCommon.h"
#include "NewsDal.h"
#include "NewsRecords.h"
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <lodb/LoDB.h>
#include <vector>
#include "LoBBSStackGuard.h"
static void newsAppendListRecord(LoBBSModule *mod, LoBBSResponse &resp, uint32_t oneBasedIndex, const LoBBSNewsEntry &entry)
{
const LoScalar &news = entry.news;
char name[LOBBS_USERNAME_BUFFER_SIZE];
char when[LOBBS_TIME_AGO_BUFFER_SIZE];
char trunc[LOBBS_LIST_LINE_TRUNC_BUFFER_SIZE];
// One char past the preview limit is enough for lobbsAppTruncMsg to add "...".
char msg[LOBBS_LIST_LINE_TRUNC_MAX_CHARS + 2];
char line[LOBBS_REPLY_BYTES + 1];
lobbsAppUsernameForUuid(mod, NewsDal::newsAuthorUuid(news), name, sizeof(name));
lobbsAppTimeAgo(NewsDal::newsTimestamp(news), when, sizeof(when));
NewsDal::newsMessage(news, msg, sizeof(msg));
lobbsAppTruncMsg(msg, trunc, sizeof(trunc), LOBBS_LIST_LINE_TRUNC_MAX_CHARS);
snprintf(line, sizeof(line), "[%u]%s @%s: %s (%s)", (unsigned)oneBasedIndex, entry.isRead ? "" : "*", name, trunc, when);
LoScalar rec;
rec.setUint64(LODB_F_ID, NewsDal::newsUuid(news));
rec.setString(LODB_F_TITLE, line);
rec.setString(LODB_F_DESCRIPTION, trunc);
rec.setBool(NewsField::FIELD_LIST_READ, entry.isRead);
rec.setUint32(LODB_F_CREATED, NewsDal::newsTimestamp(news));
lobbsResponseAppendRecord(resp, rec);
}
static void newsSubList(LoBBSCommandCtx &ctx)
{
const char *sub = lobbsArgPeek(ctx);
if (sub && strcasecmp(sub, "list") == 0)
lobbsArgShift(ctx);
auto all = ctx.mod->news().dal().getAllNewsForUser(lobbsCtxUserUuid(ctx));
LoBBSResponse resp;
if (all.empty()) {
lobbsResponseSetError(resp, "No news");
lobbsCommandReplyResponse(ctx, resp);
return;
}
for (size_t i = 0; i < all.size(); i++)
newsAppendListRecord(ctx.mod, resp, (uint32_t)(i + 1), all[i]);
lobbsCommandReplyResponse(ctx, resp);
}
static void newsSubRead(LoBBSCommandCtx &ctx, bool numericShorthand)
{
if (!numericShorthand)
lobbsArgShift(ctx);
const char *beforeNum = lobbsArgPeek(ctx);
uint32_t idx = 0;
if (!lobbsArgShiftUint(ctx, idx)) {
LoBBSResponse resp;
lobbsResponseSetError(resp, beforeNum ? "Invalid news number." : "Usage: /news read N");
lobbsCommandReplyResponse(ctx, resp);
return;
}
NewsDal &news = ctx.mod->news().dal();
auto newsItems = news.getAllNewsForUser(lobbsCtxUserUuid(ctx));
if (idx == 0 || idx > newsItems.size()) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Invalid news number");
lobbsCommandReplyResponse(ctx, resp);
return;
}
const LoScalar &item = newsItems[idx - 1].news;
char name[LOBBS_USERNAME_BUFFER_SIZE];
char when[LOBBS_TIME_AGO_BUFFER_SIZE];
char body[LOBBS_MESSAGE_READ_BODY_BUFFER_SIZE];
char header[LOBBS_USERNAME_BUFFER_SIZE + LOBBS_TIME_AGO_BUFFER_SIZE + 16];
lobbsAppUsernameForUuid(ctx.mod, NewsDal::newsAuthorUuid(item), name, sizeof(name));
NewsDal::newsMessage(item, body, sizeof(body));
lobbsAppTimeAgo(NewsDal::newsTimestamp(item), when, sizeof(when));
snprintf(header, sizeof(header), "From: @%s (%s)", name, when);
LoScalar rec;
rec.setUint64(LODB_F_ID, NewsDal::newsUuid(item));
rec.setString(LODB_F_TITLE, header);
rec.setString(LODB_F_DESCRIPTION, body);
rec.setUint32(LODB_F_CREATED, NewsDal::newsTimestamp(item));
news.markNewsAsRead(NewsDal::newsUuid(item), lobbsCtxUserUuid(ctx));
LoBBSResponse resp;
lobbsResponseAppendRecord(resp, rec);
lobbsCommandReplyResponse(ctx, resp);
}
static void newsSubUnread(LoBBSCommandCtx &ctx)
{
uint32_t idx = 0;
if (!lobbsMsgShiftIndex(ctx, 1, "Usage: /news unread N", "Invalid news number.", idx))
return;
NewsDal &news = ctx.mod->news().dal();
auto newsItems = news.getAllNewsForUser(lobbsCtxUserUuid(ctx));
if (idx == 0 || idx > newsItems.size()) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Invalid news number");
lobbsCommandReplyResponse(ctx, resp);
return;
}
uint64_t uuid = NewsDal::newsUuid(newsItems[idx - 1].news);
LoBBSResponse resp;
lobbsRecordPush(resp.records, news.markNewsAsUnread(uuid, lobbsCtxUserUuid(ctx)) ? "Marked unread." : "Failed.");
lobbsCommandReplyResponse(ctx, resp);
}
static void newsSubDelete(LoBBSCommandCtx &ctx)
{
uint32_t idx = 0;
if (!lobbsMsgShiftIndex(ctx, 1, "Usage: /news delete N", "Invalid news number.", idx))
return;
NewsDal &news = ctx.mod->news().dal();
auto newsItems = news.getAllNewsForUser(lobbsCtxUserUuid(ctx));
if (idx == 0 || idx > newsItems.size()) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Invalid news number");
lobbsCommandReplyResponse(ctx, resp);
return;
}
uint64_t uuid = NewsDal::newsUuid(newsItems[idx - 1].news);
LoBBSResponse resp;
lobbsRecordPush(resp.records, news.deleteNewsUuid(uuid) ? "Deleted." : "Failed.");
lobbsCommandReplyResponse(ctx, resp);
}
static void newsSubPost(LoBBSCommandCtx &ctx)
{
const char *sub = lobbsArgShift(ctx);
if (!sub || strcasecmp(sub, "post") != 0) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Usage: /news post message...");
lobbsCommandReplyResponse(ctx, resp);
return;
}
const char *msgBody = lobbsArgRest(ctx);
if (!msgBody[0]) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Usage: /news post message...");
lobbsCommandReplyResponse(ctx, resp);
return;
}
if (!isalpha((unsigned char)msgBody[0])) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Start with a letter.");
lobbsCommandReplyResponse(ctx, resp);
return;
}
LoBBSResponse resp;
LoDbError err = ctx.mod->news().dal().postNews(lobbsCtxUserUuid(ctx), msgBody);
lobbsRecordPush(resp.records, err == LODB_OK ? "News posted." : lobbsDbErrorText(err, "Failed to post news."));
lobbsCommandReplyResponse(ctx, resp);
}
static void newsSubReadCmd(LoBBSCommandCtx &ctx)
{
newsSubRead(ctx, false);
}
static const LoBBSVerb newsVerbs[] = {
{"list", newsSubList, LOBBS_V_LOGIN, "list — news index"},
{"read", newsSubReadCmd, LOBBS_V_LOGIN, "read N — read and mark read"},
{"unread", newsSubUnread, LOBBS_V_LOGIN, "unread N — mark unread"},
{"delete", newsSubDelete, LOBBS_V_LOGIN | LOBBS_V_SYSOP, "delete N — delete (sysop)"},
{"post", newsSubPost, LOBBS_V_LOGIN, "post message... — post news"},
};
static void handleNews(LoBBSCommandCtx &ctx)
{
if (!lobbsCommandRequireLogin(ctx))
return;
if (lobbsArgPeekIsUint(ctx)) {
newsSubRead(ctx, true);
return;
}
if (!lobbsDispatchSub(ctx, newsVerbs, sizeof(newsVerbs) / sizeof(newsVerbs[0]))) {
LoBBSResponse resp;
lobbsResponseSetError(resp, "Unknown command. Try /help news");
lobbsCommandReplyResponse(ctx, resp);
}
}
static void slashNews(LoBBSCommandCtx *ctx, const LoScalar &args)
{
if (!ctx || !lobbsSlashVerbIs(args, "news"))
return;
handleNews(*ctx);
}
static void filterNewsHelpTopics(LoBBSCommandCtx *ctx, std::vector<LoScalar> &topics, const LoScalar &args)
{
(void)args;
if (!ctx || !lobbsCtxLoggedIn(*ctx))
return;
lobbsRecordPush(topics, "news", "read and post news");
}
static void filterNewsHelpForTopic(LoBBSCommandCtx *ctx, LoScalar &value, const LoScalar &args)
{
lobbsHelpForTable(ctx, value, args, "news", newsVerbs, sizeof(newsVerbs) / sizeof(newsVerbs[0]));
}
static void filterNewsStatusLines(LoBBSCommandCtx *ctx, std::vector<LoScalar> &lines, const LoScalar &args)
{
(void)args;
if (!ctx || !ctx->mod)
return;
char title[64];
char value[32];
if (lobbsCtxLoggedIn(*ctx)) {
uint32_t n = ctx->mod->news().dal().countUnreadNews(lobbsCtxUserUuid(*ctx));
snprintf(title, sizeof(title), "News");
snprintf(value, sizeof(value), "%u unread", (unsigned)n);
} else {
uint32_t n = ctx->mod->news().dal().countAllNews();
snprintf(title, sizeof(title), "News");
snprintf(value, sizeof(value), "%u total", (unsigned)n);
}
lobbsRecordPush(lines, title, value);
}
#if LOBBS_SEED
#include "NewsSeed.h"
static void actionNewsSeed(LoBBSCommandCtx *ctx, const LoScalar &args)
{
(void)args;
if (ctx && ctx->mod)
lobbsSeedNews(*ctx->mod);
}
#endif
void lobbsNewsRegisterCommands()
{
lobbsAddAction("slash_cmd", slashNews, LOBBS_HOOK_PRIORITY_FEATURE);
lobbsAddFilter("help_topics", filterNewsHelpTopics, LOBBS_HOOK_PRIORITY_FEATURE);
lobbsAddFilter("help_for_topic", filterNewsHelpForTopic, LOBBS_HOOK_PRIORITY_FEATURE);
lobbsAddFilter("status_lines", filterNewsStatusLines, LOBBS_HOOK_PRIORITY_FEATURE);
#if LOBBS_SEED
lobbsAddAction("seed", actionNewsSeed, LOBBS_HOOK_PRIORITY_FEATURE);
#endif
}
#endif