summaryrefslogtreecommitdiff
path: root/src/core/command.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/command.c')
-rw-r--r--src/core/command.c2450
1 files changed, 2450 insertions, 0 deletions
diff --git a/src/core/command.c b/src/core/command.c
new file mode 100644
index 0000000..b60ee35
--- /dev/null
+++ b/src/core/command.c
@@ -0,0 +1,2450 @@
+#include "hybbx/hybbx.h"
+#include "hybbx/command.h"
+#include "hybbx/commands_registry.h"
+#include "hybbx/service.h"
+#include "hybbx/session.h"
+#include "hybbx/texts.h"
+#include "hybbx/storage.h"
+#include "hybbx/auth.h"
+#include "hybbx/chat.h"
+#include "hybbx/conference.h"
+#include "hybbx/mail.h"
+#include "hybbx/proxymail.h"
+#include "hybbx/proxychat.h"
+#include "hybbx/broadcast.h"
+#include "hybbx/security.h"
+#include "hybbx/security_ban.h"
+#include "hybbx/service.h"
+#include "hybbx/password.h"
+#include "hybbx/traffic.h"
+#include "hybbx/monitor.h"
+#include "hybbx/util.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+static hybbx_result_t cmd_proxymail(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd);
+static hybbx_result_t cmd_proxychat(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd);
+
+static int str_ieq(const char *a, const char *b)
+{
+ if (a == NULL || b == NULL) {
+ return 0;
+ }
+
+ while (*a != '\0' && *b != '\0') {
+ char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a);
+ char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b);
+
+ if (ca != cb) {
+ return 0;
+ }
+ a++;
+ b++;
+ }
+
+ return *a == '\0' && *b == '\0';
+}
+
+static void cmd_deny_privilege(hybbx_session_t *session);
+
+static const char *cmd_help_unknown(hybbx_session_t *session)
+{
+ if (hybbx_session_is_guest(session) ||
+ (hybbx_session_login_prompt(session) &&
+ !hybbx_session_logged_in(session))) {
+ return "Unknown command. Try /help.";
+ }
+
+ return "Unknown command. /help for list.";
+}
+
+static int cmd_verb_allowed_login_prompt(const char *verb)
+{
+ if (verb == NULL || verb[0] == '\0') {
+ return 1;
+ }
+
+ if (hybbx_commands_registry_verb_allowed(HYBBX_LEVEL_GUEST, verb)) {
+ return 1;
+ }
+
+ return str_ieq(verb, "exit") || str_ieq(verb, "logout") ||
+ str_ieq(verb, "bye") || str_ieq(verb, "quit");
+}
+
+static hybbx_result_t cmd_check_access(hybbx_session_t *session,
+ const char *verb)
+{
+ hybbx_user_level_t level = hybbx_session_user_level(session);
+
+ if (!hybbx_session_logged_in(session) &&
+ hybbx_session_login_prompt(session)) {
+ if (cmd_verb_allowed_login_prompt(verb)) {
+ return HYBBX_OK;
+ }
+ hybbx_session_write_line(session,
+ "Log in with /login or use /register.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (hybbx_commands_registry_verb_allowed(level, verb)) {
+ return HYBBX_OK;
+ }
+
+ /* Optional [monitor] allow= grants beyond commands.yaml min level. */
+ if ((str_ieq(verb, "monitor") || str_ieq(verb, "mon")) &&
+ hybbx_monitor_enabled() &&
+ hybbx_monitor_user_allowed(hybbx_session_username(session))) {
+ return HYBBX_OK;
+ }
+
+ if (level == HYBBX_LEVEL_GUEST) {
+ hybbx_session_write_line(session,
+ "Not available to guests. Try /help.");
+ } else if (str_ieq(verb, "register")) {
+ hybbx_session_write_line(session,
+ "Only guests may self-register with /register.");
+ } else if (str_ieq(verb, "changeme")) {
+ hybbx_session_write_line(session,
+ "Only registered users may use /changeme.");
+ } else {
+ hybbx_session_write_line(session, "Insufficient privileges.");
+ }
+
+ return HYBBX_ERR_DENIED;
+}
+
+static int cmd_deleteme_confirmed(const char *arg)
+{
+ return arg != NULL && hybbx_bool_is_true(arg);
+}
+
+static void cmd_registry_usage(hybbx_session_t *session, const char *verb)
+{
+ const char *canonical = hybbx_commands_registry_canonical(verb);
+
+ hybbx_commands_registry_show_help(session, canonical);
+}
+
+static hybbx_result_t cmd_help_topic(hybbx_session_t *session, const char *topic)
+{
+ hybbx_user_level_t level = hybbx_session_user_level(session);
+ const char *canonical;
+ const hybbx_command_def_t *def;
+
+ if (topic == NULL || topic[0] == '\0') {
+ hybbx_commands_registry_show_menu(session);
+ return HYBBX_OK;
+ }
+
+ canonical = hybbx_commands_registry_canonical(topic);
+
+ if (!hybbx_commands_registry_help_allowed(level, canonical)) {
+ /* Allow-list grants may read /help monitor without Sysop min. */
+ if (!((str_ieq(canonical, "monitor") || str_ieq(canonical, "mon")) &&
+ hybbx_monitor_session_may_use(session))) {
+ hybbx_session_write_line(session, cmd_help_unknown(session));
+ return HYBBX_ERR_NOT_FOUND;
+ }
+ }
+
+ def = hybbx_commands_registry_find(canonical);
+ if (def == NULL || def->line1[0] == '\0') {
+ hybbx_session_write_line(session, cmd_help_unknown(session));
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ hybbx_commands_registry_show_help(session, canonical);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_help(hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ if (cmd->argc == 0) {
+ hybbx_commands_registry_show_menu(session);
+ return HYBBX_OK;
+ }
+
+ return cmd_help_topic(session, cmd->argv[0]);
+}
+
+static hybbx_result_t cmd_news(hybbx_service_t *service, hybbx_session_t *session)
+{
+ const hybbx_texts_config_t *texts = hybbx_service_get_texts(service);
+
+ if (texts == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (hybbx_texts_send_file(texts, session, HYBBX_TEXT_NEWS) != HYBBX_OK) {
+ hybbx_session_write_line(session, "(no news available)");
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_banner(hybbx_service_t *service, hybbx_session_t *session)
+{
+ const hybbx_texts_config_t *texts = hybbx_service_get_texts(service);
+
+ if (texts == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ (void)hybbx_texts_send_banner(texts, session, HYBBX_VERSION_STRING,
+ hybbx_service_get_name(service));
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_motd(hybbx_service_t *service, hybbx_session_t *session)
+{
+ const hybbx_texts_config_t *texts = hybbx_service_get_texts(service);
+
+ if (texts == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (hybbx_texts_send_motd(texts, session) != HYBBX_OK) {
+ hybbx_session_write_line(session, "(no motd available)");
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_rules(hybbx_service_t *service, hybbx_session_t *session)
+{
+ const hybbx_texts_config_t *texts = hybbx_service_get_texts(service);
+
+ if (texts == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (hybbx_texts_send_file(texts, session, HYBBX_TEXT_RULES) != HYBBX_OK) {
+ hybbx_session_write_line(session, "(no rules available)");
+ }
+
+ return HYBBX_OK;
+}
+
+static const char *who_transport_label(const char *transport)
+{
+ if (transport == NULL || transport[0] == '\0') {
+ return "unknown";
+ }
+
+ if (str_ieq(transport, "packet_radio")) {
+ return "ax25";
+ }
+ if (str_ieq(transport, "telnet")) {
+ return "telnet";
+ }
+ if (str_ieq(transport, "circuit")) {
+ return "circuit";
+ }
+ if (str_ieq(transport, "ssh")) {
+ return "ssh";
+ }
+ if (str_ieq(transport, "websocket")) {
+ return "websocket";
+ }
+
+ return transport;
+}
+
+typedef struct who_ctx {
+ hybbx_session_t *requester;
+ unsigned count;
+ int at_plugin;
+} who_ctx_t;
+
+static void who_list_visitor(hybbx_session_t *session, void *userdata)
+{
+ who_ctx_t *ctx = (who_ctx_t *)userdata;
+ const hybbx_session_record_t *rec;
+ const char *name;
+ const char *transport;
+ char line[96];
+ char user_at[80];
+
+ if (ctx == NULL || session == NULL || !hybbx_session_logged_in(session)) {
+ return;
+ }
+
+ if (!hybbx_session_is_interactive_user(session)) {
+ return;
+ }
+
+ if (hybbx_session_hidden_from_who(session)) {
+ return;
+ }
+
+ if (ctx->at_plugin) {
+ if (hybbx_session_format_user_at_plugin(session, user_at,
+ sizeof(user_at)) != HYBBX_OK) {
+ return;
+ }
+ snprintf(line, sizeof(line), " %s", user_at);
+ } else {
+ name = hybbx_session_display_name(session);
+ rec = hybbx_session_record(session);
+ transport = who_transport_label(rec != NULL ? rec->transport : NULL);
+ if (rec == NULL || rec->transport[0] == '\0') {
+ transport = who_transport_label(session->transport != NULL ?
+ session->transport->name : NULL);
+ }
+ snprintf(line, sizeof(line), " %-16s %s", name, transport);
+ }
+
+ hybbx_session_write_line(ctx->requester, line);
+ ctx->count++;
+}
+
+static hybbx_result_t cmd_who(hybbx_service_t *service, hybbx_session_t *session)
+{
+ who_ctx_t ctx;
+ char total[32];
+
+ ctx.requester = session;
+ ctx.count = 0;
+ ctx.at_plugin = hybbx_service_login_announce(service);
+
+ hybbx_session_write_line(session, "Online users:");
+ hybbx_service_visit_sessions(service, who_list_visitor, &ctx);
+
+ if (ctx.count == 0) {
+ hybbx_session_write_line(session, " (none)");
+ }
+
+ snprintf(total, sizeof(total), "Total: %u", ctx.count);
+ hybbx_session_write_line(session, total);
+ return HYBBX_OK;
+}
+
+typedef struct users_stats_ctx {
+ size_t by_level[6];
+ size_t total;
+} users_stats_ctx_t;
+
+static hybbx_result_t users_stats_cb(const hybbx_user_record_t *user, void *ctx)
+{
+ users_stats_ctx_t *stats = (users_stats_ctx_t *)ctx;
+
+ if (user == NULL || stats == NULL) {
+ return HYBBX_OK;
+ }
+
+ if (user->level < HYBBX_LEVEL_SYSOP || user->level > HYBBX_LEVEL_USER) {
+ return HYBBX_OK;
+ }
+
+ stats->by_level[user->level]++;
+ stats->total++;
+ return HYBBX_OK;
+}
+
+static void users_compute_percents(const size_t *counts, size_t total,
+ unsigned *percents)
+{
+ hybbx_user_level_t level;
+ hybbx_user_level_t adjust_level = HYBBX_LEVEL_SYSOP;
+ unsigned sum = 0;
+ unsigned max_remainder = 0;
+
+ if (total == 0) {
+ return;
+ }
+
+ for (level = HYBBX_LEVEL_SYSOP; level <= HYBBX_LEVEL_USER; level++) {
+ unsigned scaled = (unsigned)((counts[level] * 1000u) / total);
+ unsigned rem = scaled % 10u;
+
+ percents[level] = scaled / 10u;
+ sum += percents[level];
+ if (rem > max_remainder) {
+ max_remainder = rem;
+ adjust_level = level;
+ }
+ }
+
+ if (sum < 100u) {
+ percents[adjust_level] += 100u - sum;
+ }
+}
+
+static hybbx_result_t cmd_users(hybbx_service_t *service, hybbx_session_t *session)
+{
+ hybbx_storage_t *storage;
+ users_stats_ctx_t stats;
+ unsigned percents[6];
+ hybbx_user_level_t level;
+ char line[48];
+ char total[40];
+ hybbx_result_t rc;
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ hybbx_session_write_line(session, "User storage unavailable.");
+ return HYBBX_ERR_INVALID;
+ }
+
+ memset(&stats, 0, sizeof(stats));
+ memset(percents, 0, sizeof(percents));
+
+ rc = hybbx_storage_foreach_user(storage, users_stats_cb, &stats);
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Could not read user database.");
+ return rc;
+ }
+
+ hybbx_session_write_line(session, "Registered users:");
+
+ if (stats.total == 0) {
+ hybbx_session_write_line(session, " (none)");
+ } else {
+ users_compute_percents(stats.by_level, stats.total, percents);
+ for (level = HYBBX_LEVEL_SYSOP; level <= HYBBX_LEVEL_USER; level++) {
+ if (stats.by_level[level] == 0) {
+ continue;
+ }
+
+ snprintf(line, sizeof(line), " %-8s %3u%%",
+ hybbx_user_level_name(level), percents[level]);
+ hybbx_session_write_line(session, line);
+ }
+ }
+
+ snprintf(total, sizeof(total), "Total users: %zu", stats.total);
+ hybbx_session_write_line(session, total);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_session(hybbx_session_t *session)
+{
+ char buf[128];
+ const hybbx_session_record_t *rec = hybbx_session_record(session);
+
+ if (rec == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ snprintf(buf, sizeof(buf), "User: %s", hybbx_session_display_name(session));
+ hybbx_session_write_line(session, buf);
+ snprintf(buf, sizeof(buf), "Level: %s",
+ hybbx_user_level_name(hybbx_session_user_level(session)));
+ hybbx_session_write_line(session, buf);
+ snprintf(buf, sizeof(buf), "Session: %llu",
+ (unsigned long long)rec->session_id);
+ hybbx_session_write_line(session, buf);
+ snprintf(buf, sizeof(buf), "Transport: %s", rec->transport);
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t parse_profile_fields(const hybbx_parsed_command_t *cmd,
+ unsigned name_start,
+ unsigned tail_extra,
+ hybbx_user_registration_t *reg)
+{
+ size_t i;
+ size_t pos = 0;
+ unsigned suffix;
+
+ if (cmd == NULL || reg == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ suffix = 3u + tail_extra;
+ if (cmd->argc < name_start + suffix) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_strlcpy(reg->email, cmd->argv[cmd->argc - 1u - tail_extra],
+ sizeof(reg->email));
+ hybbx_strlcpy(reg->location, cmd->argv[cmd->argc - 2u - tail_extra],
+ sizeof(reg->location));
+ hybbx_strlcpy(reg->country, cmd->argv[cmd->argc - 3u - tail_extra],
+ sizeof(reg->country));
+
+ reg->full_name[0] = '\0';
+ for (i = name_start; i + suffix < cmd->argc; i++) {
+ size_t part_len;
+ const char *part = cmd->argv[i];
+
+ if (part == NULL) {
+ continue;
+ }
+
+ part_len = strlen(part);
+ if (pos > 0) {
+ if (pos + 1 >= sizeof(reg->full_name)) {
+ return HYBBX_ERR_INVALID;
+ }
+ reg->full_name[pos++] = ' ';
+ }
+
+ if (pos + part_len >= sizeof(reg->full_name)) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ memcpy(reg->full_name + pos, part, part_len);
+ pos += part_len;
+ }
+
+ reg->full_name[pos] = '\0';
+ return HYBBX_OK;
+}
+
+static hybbx_result_t parse_registration(const hybbx_parsed_command_t *cmd,
+ hybbx_user_registration_t *reg,
+ int with_password)
+{
+ hybbx_result_t rc;
+ unsigned min_args = with_password ? 6u : 5u;
+ unsigned tail_extra = with_password ? 1u : 0u;
+
+ if (cmd == NULL || reg == NULL || cmd->argc < min_args) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ memset(reg, 0, sizeof(*reg));
+ hybbx_strlcpy(reg->nickname, cmd->argv[0], sizeof(reg->nickname));
+ hybbx_strlcpy(reg->username, cmd->argv[0], sizeof(reg->username));
+ hybbx_username_normalize(reg->username);
+
+ if (with_password) {
+ hybbx_strlcpy(reg->password, cmd->argv[cmd->argc - 1],
+ sizeof(reg->password));
+ }
+
+ rc = parse_profile_fields(cmd, 1, tail_extra, reg);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t parse_changeme(const hybbx_parsed_command_t *cmd,
+ const char *username,
+ hybbx_user_registration_t *reg,
+ const char **old_password,
+ const char **new_password)
+{
+ hybbx_result_t rc;
+
+ if (cmd == NULL || username == NULL || reg == NULL ||
+ old_password == NULL || new_password == NULL || cmd->argc < 6) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ memset(reg, 0, sizeof(*reg));
+ hybbx_strlcpy(reg->username, username, sizeof(reg->username));
+ hybbx_username_normalize(reg->username);
+ *old_password = cmd->argv[0];
+ *new_password = cmd->argv[1];
+
+ rc = parse_profile_fields(cmd, 2, 0, reg);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t parse_userchange(const hybbx_parsed_command_t *cmd,
+ hybbx_user_registration_t *reg,
+ const char **new_password)
+{
+ hybbx_result_t rc;
+
+ if (cmd == NULL || reg == NULL || new_password == NULL || cmd->argc < 7) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ memset(reg, 0, sizeof(*reg));
+ hybbx_strlcpy(reg->username, cmd->argv[0], sizeof(reg->username));
+ hybbx_username_normalize(reg->username);
+ *new_password = cmd->argv[1];
+
+ rc = parse_profile_fields(cmd, 2, 0, reg);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_lookup_user(hybbx_service_t *service,
+ const char *username,
+ hybbx_user_record_t *out)
+{
+ hybbx_storage_t *storage;
+ hybbx_result_t rc;
+
+ if (service == NULL || username == NULL || out == NULL ||
+ username[0] == '\0') {
+ return HYBBX_ERR_INVALID;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_resolve_user(storage, username, out);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ return rc;
+}
+
+static void cmd_deny_privilege(hybbx_session_t *session)
+{
+ hybbx_session_write_line(session, "Insufficient privileges.");
+}
+
+static hybbx_result_t cmd_activate(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_user_record_t target;
+ hybbx_user_level_t actor_level;
+ char buf[128];
+ hybbx_result_t rc;
+
+ if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') {
+ hybbx_session_write_line(session, "Usage: /activate <username>");
+ return HYBBX_OK;
+ }
+
+ actor_level = hybbx_session_user_level(session);
+ if (!hybbx_commands_registry_verb_allowed(actor_level, "activate")) {
+ cmd_deny_privilege(session);
+ return HYBBX_ERR_DENIED;
+ }
+
+ rc = cmd_lookup_user(service, cmd->argv[0], &target);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (target.level != HYBBX_LEVEL_USER) {
+ hybbx_session_write_line(session,
+ "Only pending registered user accounts can be activated.");
+ return HYBBX_OK;
+ }
+
+ if (target.active) {
+ hybbx_session_write_line(session, "Account is already active.");
+ return HYBBX_OK;
+ }
+
+ target.active = 1;
+ rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ snprintf(buf, sizeof(buf), "Activated '%s'.",
+ hybbx_user_display_name(&target));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_promote(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_user_record_t target;
+ hybbx_user_level_t actor_level;
+ hybbx_user_level_t new_level;
+ char buf[128];
+ hybbx_result_t rc;
+
+ if (cmd->argc < 2 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0' ||
+ cmd->argv[1] == NULL || cmd->argv[1][0] == '\0') {
+ hybbx_session_write_line(session,
+ "Usage: /promote <username> admin|mod");
+ return HYBBX_OK;
+ }
+
+ actor_level = hybbx_session_user_level(session);
+ new_level = hybbx_user_level_parse(cmd->argv[1]);
+ if (new_level != HYBBX_LEVEL_ADMIN && new_level != HYBBX_LEVEL_MOD) {
+ hybbx_session_write_line(session,
+ "Level must be admin or mod.");
+ return HYBBX_OK;
+ }
+
+ rc = cmd_lookup_user(service, cmd->argv[0], &target);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (!hybbx_commands_registry_may_promote(actor_level, target.level, target.active,
+ new_level)) {
+ if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) {
+ cmd_deny_privilege(session);
+ } else if (new_level == HYBBX_LEVEL_ADMIN) {
+ hybbx_session_write_line(session,
+ "Only the Sysop may add Admins.");
+ } else {
+ hybbx_session_write_line(session,
+ "Only Sysop or Admin may add Mods.");
+ }
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (target.level == new_level) {
+ snprintf(buf, sizeof(buf), "'%s' is already %s.",
+ hybbx_user_display_name(&target),
+ hybbx_user_level_name(new_level));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+ }
+
+ target.level = new_level;
+ rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ snprintf(buf, sizeof(buf), "Promoted '%s' to %s.",
+ hybbx_user_display_name(&target),
+ hybbx_user_level_name(new_level));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_demote(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_user_record_t target;
+ hybbx_user_level_t actor_level;
+ hybbx_user_level_t old_level;
+ char buf[128];
+ hybbx_result_t rc;
+
+ if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') {
+ hybbx_session_write_line(session, "Usage: /demote <username>");
+ return HYBBX_OK;
+ }
+
+ actor_level = hybbx_session_user_level(session);
+
+ rc = cmd_lookup_user(service, cmd->argv[0], &target);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (str_ieq(target.username, hybbx_session_username(session))) {
+ hybbx_session_write_line(session, "You cannot demote your own account.");
+ return HYBBX_OK;
+ }
+
+ old_level = target.level;
+ if (!hybbx_commands_registry_may_demote(actor_level, target.level)) {
+ if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) {
+ cmd_deny_privilege(session);
+ } else if (target.level == HYBBX_LEVEL_ADMIN) {
+ hybbx_session_write_line(session,
+ "Only the Sysop may remove Admins.");
+ } else if (target.level == HYBBX_LEVEL_MOD) {
+ hybbx_session_write_line(session,
+ "Only Sysop or Admin may remove Mods.");
+ } else {
+ hybbx_session_write_line(session,
+ "That account is not an Admin or Mod.");
+ }
+ return HYBBX_ERR_DENIED;
+ }
+
+ target.level = HYBBX_LEVEL_USER;
+ rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ snprintf(buf, sizeof(buf), "Demoted '%s' from %s to user.",
+ hybbx_user_display_name(&target),
+ hybbx_user_level_name(old_level));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_delete(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_user_record_t target;
+ hybbx_user_level_t actor_level;
+ char buf[128];
+ hybbx_result_t rc;
+
+ if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') {
+ hybbx_session_write_line(session, "Usage: /delete <username>");
+ return HYBBX_OK;
+ }
+
+ actor_level = hybbx_session_user_level(session);
+
+ rc = cmd_lookup_user(service, cmd->argv[0], &target);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (str_ieq(target.username, hybbx_session_username(session))) {
+ hybbx_session_write_line(session, "You cannot delete your own account.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_user_level_is_sysop(target.level)) {
+ hybbx_session_write_line(session,
+ "The Sysop account is permanent and cannot be deleted.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_commands_registry_may_delete(actor_level, target.level)) {
+ if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) {
+ cmd_deny_privilege(session);
+ } else if (target.level == HYBBX_LEVEL_ADMIN) {
+ hybbx_session_write_line(session,
+ "Admins cannot delete other Admins.");
+ } else {
+ cmd_deny_privilege(session);
+ }
+ return HYBBX_ERR_DENIED;
+ }
+
+ rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), target.id);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ snprintf(buf, sizeof(buf), "Deleted account '%s'.",
+ hybbx_user_display_name(&target));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_deleteme(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ const hybbx_session_record_t *rec;
+ hybbx_user_level_t level;
+ const char *arg;
+ hybbx_result_t rc;
+
+ (void)service;
+
+ level = hybbx_session_user_level(session);
+ if (hybbx_user_level_is_sysop(level)) {
+ hybbx_session_write_line(session,
+ "The Sysop account is permanent and cannot be deleted.");
+ return HYBBX_OK;
+ }
+
+ arg = cmd->argc > 0 ? cmd->argv[0] : NULL;
+ if (arg != NULL && hybbx_bool_is_false(arg)) {
+ hybbx_session_write_line(session, "Account deletion cancelled.");
+ return HYBBX_OK;
+ }
+
+ if (!cmd_deleteme_confirmed(arg)) {
+ hybbx_session_write_line(session,
+ "Usage: /deleteme yes|no");
+ return HYBBX_OK;
+ }
+
+ rec = hybbx_session_record(session);
+ if (rec == NULL || rec->user_id == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_delete_user(hybbx_service_get_storage(service),
+ rec->user_id);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Account not found.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ hybbx_session_write_line(session, "Account deleted. Goodbye.");
+ return HYBBX_SESSION_END;
+}
+
+static hybbx_result_t cmd_register_user(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd,
+ int staff_created)
+{
+ hybbx_storage_t *storage;
+ hybbx_user_registration_t reg;
+ hybbx_user_record_t user;
+ char buf[HYBBX_USER_FULL_NAME_MAX + 64];
+ hybbx_result_t rc;
+
+ if (staff_created) {
+ if (cmd->argc < 5) {
+ hybbx_session_write_line(session,
+ "Usage: /usercreate <username> <full-name> <country> <location> <email>");
+ return HYBBX_OK;
+ }
+ } else if (cmd->argc < 6) {
+ hybbx_session_write_line(session,
+ "Usage: /register <username> <full-name> <country> <location> <email> <password>");
+ return HYBBX_OK;
+ }
+
+ rc = parse_registration(cmd, &reg, staff_created ? 0 : 1);
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Registration fields too long.");
+ return HYBBX_OK;
+ }
+
+ if (!staff_created && !hybbx_password_plain_valid(reg.password)) {
+ hybbx_session_write_line(session,
+ "Password must be 8-24 characters (- not allowed).");
+ return HYBBX_OK;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_register_user(storage, &reg, &user);
+ if (rc == HYBBX_ERR_BUSY) {
+ hybbx_session_write_line(session, "Username or nickname already taken.");
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_INVALID) {
+ hybbx_session_write_line(session,
+ "Invalid username (4-12 chars, one _ or one -, max 4 digits) or bad profile.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (staff_created) {
+ snprintf(buf, sizeof(buf), "User '%s' created.",
+ hybbx_user_display_name(&user));
+ } else {
+ snprintf(buf, sizeof(buf), "Registration received for '%s'.",
+ hybbx_user_display_name(&user));
+ }
+ hybbx_session_write_line(session, buf);
+ snprintf(buf, sizeof(buf), "Name: %s", user.full_name);
+ hybbx_session_write_line(session, buf);
+ if (staff_created) {
+ hybbx_session_write_line(session,
+ "Account is inactive. Use /activate before the user can log in.");
+ } else {
+ const hybbx_mail_config_t *mail_cfg = hybbx_service_get_mail(service);
+
+ (void)hybbx_mail_notify_staff_registration(service, &reg, &user);
+ hybbx_session_write_line(session,
+ "A Sysop or Admin must activate your account before you can log in.");
+ if (mail_cfg != NULL && mail_cfg->enabled) {
+ hybbx_session_write_line(session,
+ "Sysop and Admin were notified by mail.");
+ }
+ }
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_register(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ if (!hybbx_commands_registry_verb_allowed(hybbx_session_user_level(session),
+ "register") &&
+ !(hybbx_session_login_prompt(session) &&
+ !hybbx_session_logged_in(session))) {
+ hybbx_session_write_line(session,
+ "Only guests or login-prompt sessions may self-register with /register.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ return cmd_register_user(service, session, cmd, 0);
+}
+
+static hybbx_result_t cmd_createuser(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ return cmd_register_user(service, session, cmd, 1);
+}
+
+static hybbx_result_t cmd_changeme(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_storage_t *storage;
+ hybbx_user_registration_t reg;
+ hybbx_user_record_t user;
+ const char *old_password;
+ const char *new_password;
+ const char *username;
+ hybbx_result_t rc;
+
+ if (hybbx_session_is_guest(session)) {
+ hybbx_session_write_line(session,
+ "Only registered users may use /changeme.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc < 6) {
+ hybbx_session_write_line(session,
+ "Usage: /changeme <oldpass> <newpass> <full-name> <country> <location> <email>");
+ return HYBBX_OK;
+ }
+
+ username = hybbx_session_username(session);
+ if (username == NULL || username[0] == '\0') {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = parse_changeme(cmd, username, &reg, &old_password, &new_password);
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Profile fields too long.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_password_plain_valid(new_password)) {
+ hybbx_session_write_line(session,
+ "New password must be 8-24 characters (- not allowed).");
+ return HYBBX_OK;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_find_user(storage, reg.username, &user);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Account not found.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ {
+ const hybbx_session_record_t *rec = hybbx_session_record(session);
+
+ if (rec == NULL || user.id != rec->user_id) {
+ hybbx_session_write_line(session,
+ "You can only change your own account.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_user_profile_valid(&reg)) {
+ hybbx_session_write_line(session,
+ "Invalid profile (check name, country, location, email).");
+ return HYBBX_OK;
+ }
+ }
+
+ if (user.password[0] == '\0') {
+ hybbx_session_write_line(session,
+ "Account has no password. Ask Sysop or Admin to set one.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_password_match(user.password, old_password)) {
+ hybbx_session_write_line(session, "Old password incorrect.");
+ return HYBBX_OK;
+ }
+
+ hybbx_strlcpy(user.full_name, reg.full_name, sizeof(user.full_name));
+ hybbx_strlcpy(user.country, reg.country, sizeof(user.country));
+ hybbx_strlcpy(user.location, reg.location, sizeof(user.location));
+ hybbx_strlcpy(user.email, reg.email, sizeof(user.email));
+
+ rc = hybbx_password_hash(new_password, user.password, sizeof(user.password));
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Could not store new password.");
+ return rc;
+ }
+
+ rc = hybbx_storage_update_user(storage, &user);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ hybbx_session_write_line(session, "Account updated.");
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_userchange(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_storage_t *storage;
+ hybbx_user_registration_t reg;
+ hybbx_user_record_t user;
+ const char *new_password;
+ hybbx_user_level_t actor_level;
+ hybbx_result_t rc;
+ char buf[160];
+
+ actor_level = hybbx_session_user_level(session);
+ if (!hybbx_user_level_is_sysop_or_admin(actor_level)) {
+ cmd_deny_privilege(session);
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc < 7) {
+ hybbx_session_write_line(session,
+ "Usage: /changeuser <user> <newpass> <full-name> <country> <location> <email>");
+ return HYBBX_OK;
+ }
+
+ rc = parse_userchange(cmd, &reg, &new_password);
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Profile fields too long.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_password_plain_valid(new_password)) {
+ hybbx_session_write_line(session,
+ "New password must be 8-24 characters (- not allowed).");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_user_profile_valid(&reg)) {
+ hybbx_session_write_line(session,
+ "Invalid profile (check name, country, location, email).");
+ return HYBBX_OK;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_find_user(storage, reg.username, &user);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (str_ieq(user.username, hybbx_session_username(session))) {
+ hybbx_session_write_line(session,
+ "Use /changeme to update your own account.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_user_level_is_sysop(user.level)) {
+ hybbx_session_write_line(session,
+ "The Sysop account cannot be changed with /changeuser.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_commands_registry_may_userchange(actor_level, user.level)) {
+ if (actor_level == HYBBX_LEVEL_ADMIN &&
+ user.level == HYBBX_LEVEL_ADMIN) {
+ hybbx_session_write_line(session,
+ "Admins cannot change other Admins. Sysop only.");
+ } else {
+ cmd_deny_privilege(session);
+ }
+ return HYBBX_ERR_DENIED;
+ }
+
+ hybbx_strlcpy(user.full_name, reg.full_name, sizeof(user.full_name));
+ hybbx_strlcpy(user.country, reg.country, sizeof(user.country));
+ hybbx_strlcpy(user.location, reg.location, sizeof(user.location));
+ hybbx_strlcpy(user.email, reg.email, sizeof(user.email));
+
+ rc = hybbx_password_hash(new_password, user.password, sizeof(user.password));
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Could not store new password.");
+ return rc;
+ }
+
+ rc = hybbx_storage_update_user(storage, &user);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ snprintf(buf, sizeof(buf), "Updated account '%s'.",
+ hybbx_user_display_name(&user));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_userdelete(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_user_record_t target;
+ hybbx_user_level_t actor_level;
+ char buf[128];
+ hybbx_result_t rc;
+
+ actor_level = hybbx_session_user_level(session);
+ if (!hybbx_user_level_is_sysop(actor_level)) {
+ cmd_deny_privilege(session);
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') {
+ hybbx_session_write_line(session, "Usage: /deleteuser <username>");
+ return HYBBX_OK;
+ }
+
+ rc = cmd_lookup_user(service, cmd->argv[0], &target);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (str_ieq(target.username, hybbx_session_username(session))) {
+ hybbx_session_write_line(session, "You cannot delete your own account.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_commands_registry_may_userdelete(actor_level, target.level)) {
+ hybbx_session_write_line(session,
+ "The Sysop account is permanent and cannot be deleted.");
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), target.id);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ snprintf(buf, sizeof(buf), "Deleted account '%s'.",
+ hybbx_user_display_name(&target));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_login(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_storage_t *storage;
+ hybbx_user_record_t user;
+ const hybbx_auth_config_t *auth;
+ const char *guest_prefix;
+ char username[HYBBX_USER_NAME_MAX];
+ unsigned guest_slot;
+ hybbx_result_t rc;
+
+ if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') {
+ hybbx_session_write_line(session,
+ "Usage: /login <username> <password>");
+ return HYBBX_OK;
+ }
+
+ auth = hybbx_service_get_auth(service);
+ guest_prefix = auth != NULL ? auth->guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX;
+
+ hybbx_strlcpy(username, cmd->argv[0], sizeof(username));
+ hybbx_username_normalize(username);
+
+ if (hybbx_guest_slot_from_username(guest_prefix, username, &guest_slot)) {
+ if (auth != NULL && auth->auto_login) {
+ hybbx_session_write_line(session,
+ "Guest access is automatic on connect (auto_login). "
+ "/login is for registered accounts only.");
+ } else {
+ hybbx_session_write_line(session,
+ "/login is for registered accounts only.");
+ }
+ return HYBBX_OK;
+ }
+
+ if (cmd->argc < 2 || cmd->argv[1] == NULL || cmd->argv[1][0] == '\0') {
+ hybbx_session_write_line(session,
+ "Usage: /login <username> <password>");
+ return HYBBX_OK;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_find_user(storage, username, &user);
+
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (hybbx_user_level_is_guest(user.level)) {
+ hybbx_session_write_line(session,
+ "Guests are ephemeral and use auto-login on connect.");
+ return HYBBX_OK;
+ }
+
+ if (!user.active) {
+ hybbx_session_write_line(session,
+ "Account pending activation by Sysop or Admin.");
+ return HYBBX_OK;
+ }
+
+ if (user.password[0] == '\0') {
+ hybbx_session_write_line(session,
+ "Account has no password. Contact Sysop or Admin.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_password_match(user.password, cmd->argv[1])) {
+ const hybbx_session_record_t *rec = hybbx_session_record(session);
+
+ hybbx_security_log_write("login_fail ip=%s user=%s transport=%s",
+ rec != NULL && rec->remote[0] != '\0' ?
+ rec->remote : "?",
+ username,
+ rec != NULL && rec->transport[0] != '\0' ?
+ rec->transport : "?");
+ if (rec != NULL && rec->remote[0] != '\0') {
+ hybbx_security_ban_login_fail(rec->remote, rec->transport);
+ }
+ hybbx_session_write_line(session, "Invalid password.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_service_find_registered_session(service, user.id, session) !=
+ NULL) {
+ hybbx_session_write_line(session,
+ "That account is already logged in elsewhere.");
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_session_switch_user(session, &user);
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Login failed.");
+ return rc;
+ }
+
+ {
+ time_t since_login = user.last_login_at;
+ const hybbx_texts_config_t *texts;
+
+ texts = hybbx_service_get_texts(service);
+ if (texts != NULL) {
+ (void)hybbx_texts_send_motd(texts, session);
+ }
+
+ hybbx_mail_announce_since_last_login(service, session, since_login);
+
+ user.last_login_at = time(NULL);
+ (void)hybbx_storage_update_user(storage, &user);
+ }
+ hybbx_session_show_prompt(session);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_chat(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ const hybbx_chat_config_t *chat;
+ unsigned channel_index;
+ unsigned current;
+ const char *name;
+ char line[HYBBX_CHAT_CHANNEL_NAME_MAX + 48];
+ hybbx_result_t rc;
+
+ if (hybbx_session_is_guest(session)) {
+ hybbx_session_write_line(session, "Guests cannot use chat.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc > 0 && str_ieq(cmd->argv[0], "proxychat")) {
+ hybbx_parsed_command_t proxy_cmd;
+ char proxy_verb[] = "proxychat";
+
+ memset(&proxy_cmd, 0, sizeof(proxy_cmd));
+ proxy_cmd.verb = proxy_verb;
+ return cmd_proxychat(service, session, &proxy_cmd);
+ }
+
+ chat = hybbx_service_get_chat(service);
+ if (chat == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (cmd->argc == 0) {
+ hybbx_chat_list_channels(session, chat);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "show")) {
+ if (cmd->argc != 1) {
+ hybbx_session_write_line(session, "Usage: /chat show");
+ return HYBBX_OK;
+ }
+ hybbx_chat_show_channel(service, session);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "showall")) {
+ if (cmd->argc != 1) {
+ hybbx_session_write_line(session, "Usage: /chat showall");
+ return HYBBX_OK;
+ }
+ hybbx_chat_show_all(service, session);
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_chat_resolve_channel(chat, cmd->argv[0], &channel_index);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown chat channel.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ current = hybbx_session_chat_channel(session);
+ if (current == channel_index) {
+ snprintf(line, sizeof(line), "Already in channel %u %s.",
+ channel_index,
+ hybbx_chat_channel_name(chat, channel_index));
+ hybbx_session_write_line(session, line);
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_session_join_chat_channel(session, channel_index);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ name = hybbx_chat_channel_name(chat, channel_index);
+ snprintf(line, sizeof(line), "Channel %u: %s", channel_index, name);
+ hybbx_session_write_line(session, line);
+ hybbx_session_write_line(session,
+ "Each line is a message; /leave or /main to exit.");
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_conference(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ char topic[HYBBX_CONFERENCE_TOPIC_MAX];
+ const char *user;
+ size_t i;
+ size_t pos = 0;
+
+ if (hybbx_session_is_guest(session)) {
+ hybbx_session_write_line(session, "Guests cannot use conference.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc < 2) {
+ hybbx_session_write_line(session,
+ "Usage: /conference <topic> <user>");
+ return HYBBX_OK;
+ }
+
+ user = cmd->argv[cmd->argc - 1];
+ topic[0] = '\0';
+
+ for (i = 0; i + 1 < cmd->argc; i++) {
+ size_t part_len = strlen(cmd->argv[i]);
+
+ if (i > 0) {
+ if (pos + 1 >= sizeof(topic)) {
+ hybbx_session_write_line(session, "Topic too long.");
+ return HYBBX_OK;
+ }
+ topic[pos++] = ' ';
+ topic[pos] = '\0';
+ }
+
+ if (pos + part_len >= sizeof(topic)) {
+ hybbx_session_write_line(session, "Topic too long.");
+ return HYBBX_OK;
+ }
+
+ memcpy(topic + pos, cmd->argv[i], part_len);
+ pos += part_len;
+ topic[pos] = '\0';
+ }
+
+ return hybbx_conference_start(service, session, topic, user);
+}
+
+static void mail_join_subject(const hybbx_parsed_command_t *cmd,
+ char *out, size_t out_len)
+{
+ size_t i;
+ size_t pos = 0;
+
+ if (out == NULL || out_len == 0) {
+ return;
+ }
+
+ out[0] = '\0';
+ if (cmd == NULL || cmd->argc < 3) {
+ return;
+ }
+
+ for (i = 2; i < cmd->argc; i++) {
+ size_t part_len;
+
+ if (cmd->argv[i] == NULL) {
+ continue;
+ }
+
+ if (pos > 0) {
+ if (pos + 1 >= out_len) {
+ break;
+ }
+ out[pos++] = ' ';
+ out[pos] = '\0';
+ }
+
+ part_len = strlen(cmd->argv[i]);
+ if (pos + part_len >= out_len) {
+ part_len = out_len - pos - 1;
+ }
+ memcpy(out + pos, cmd->argv[i], part_len);
+ pos += part_len;
+ out[pos] = '\0';
+ }
+}
+
+static hybbx_result_t cmd_proxymail(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ char subject[HYBBX_MAIL_SUBJECT_MAX + 1];
+ hybbx_result_t rc;
+
+ if (hybbx_session_is_guest(session)) {
+ hybbx_session_write_line(session, "Guests cannot use proxymail.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc == 0) {
+ (void)hybbx_session_enter_proxymail(session);
+ hybbx_proxymail_list_inbox(service, session);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "list")) {
+ unsigned from = 1;
+ unsigned to = 0;
+
+ if (cmd->argc >= 2) {
+ if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) {
+ hybbx_session_write_line(session,
+ "Usage: /proxymail list [<from>-<to>]");
+ return HYBBX_OK;
+ }
+ }
+
+ (void)hybbx_session_enter_proxymail(session);
+ hybbx_proxymail_list_inbox_range(service, session, from, to);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "delete") || str_ieq(cmd->argv[0], "del")) {
+ unsigned from = 1;
+ unsigned to = 0;
+
+ if (cmd->argc < 2 || cmd->argv[1] == NULL) {
+ hybbx_session_write_line(session,
+ "Usage: /proxymail delete <n|from-to>");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) {
+ hybbx_session_write_line(session,
+ "Usage: /proxymail delete <n|from-to>");
+ return HYBBX_OK;
+ }
+
+ (void)hybbx_session_enter_proxymail(session);
+ return hybbx_proxymail_delete_range(service, session, from, to);
+ }
+
+ if (str_ieq(cmd->argv[0], "recycle")) {
+ (void)hybbx_session_enter_proxymail(session);
+ return hybbx_proxymail_recycle_empty(service, session);
+ }
+
+ if (str_ieq(cmd->argv[0], "read")) {
+ unsigned index;
+
+ if (cmd->argc < 2 || cmd->argv[1] == NULL) {
+ hybbx_session_write_line(session, "Usage: /proxymail read <n>");
+ return HYBBX_OK;
+ }
+
+ index = (unsigned)strtoul(cmd->argv[1], NULL, 10);
+ if (index == 0) {
+ hybbx_session_write_line(session, "Usage: /proxymail read <n>");
+ return HYBBX_OK;
+ }
+
+ (void)hybbx_session_enter_proxymail(session);
+ return hybbx_proxymail_read(service, session, index);
+ }
+
+ if (str_ieq(cmd->argv[0], "send")) {
+ if (cmd->argc < 3) {
+ hybbx_session_write_line(session,
+ "Usage: /proxymail send <user>@<main> <subject>");
+ return HYBBX_OK;
+ }
+
+ mail_join_subject(cmd, subject, sizeof(subject));
+ if (subject[0] == '\0') {
+ hybbx_session_write_line(session,
+ "Usage: /proxymail send <user>@<main> <subject>");
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_session_proxymail_compose_start(session, cmd->argv[1],
+ subject);
+ if (rc == HYBBX_ERR_INVALID) {
+ hybbx_session_write_line(session,
+ "Invalid address or subject. Use user@remote-main.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ hybbx_session_write_line(session,
+ "Compose body. /proxymail done to send.");
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "done")) {
+ const char *body;
+ const char *to_address;
+ const char *mail_subject;
+
+ if (!hybbx_session_proxymail_composing(session)) {
+ hybbx_session_write_line(session, "Not composing proxymail.");
+ return HYBBX_OK;
+ }
+
+ to_address = hybbx_session_proxymail_compose_to(session);
+ mail_subject = hybbx_session_proxymail_compose_subject(session);
+ body = hybbx_session_proxymail_compose_body(session);
+
+ rc = hybbx_proxymail_deliver(service, hybbx_session_display_name(session),
+ to_address, mail_subject, body);
+
+ if (rc == HYBBX_ERR_INVALID) {
+ hybbx_session_write_line(session, "Invalid address — use user@mainname.");
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_UNSUPPORTED) {
+ hybbx_session_write_line(session,
+ "Proxymail is not available — enable mains_proxy and peer links.");
+ hybbx_session_proxymail_compose_cancel(session);
+ hybbx_session_leave_area(session);
+ hybbx_session_show_prompt(session);
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session,
+ "Unknown remote main — check user@mainname and peer_id.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Send failed.");
+ return rc;
+ }
+
+ hybbx_session_write_line(session, "Proxymail sent.");
+ hybbx_session_proxymail_compose_cancel(session);
+ hybbx_session_leave_area(session);
+ hybbx_session_show_prompt(session);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "cancel")) {
+ if (hybbx_session_proxymail_composing(session)) {
+ hybbx_session_proxymail_compose_cancel(session);
+ hybbx_session_leave_area(session);
+ hybbx_session_write_line(session, "Compose cancelled.");
+ } else {
+ hybbx_session_write_line(session, "Not composing proxymail.");
+ }
+ return HYBBX_OK;
+ }
+
+ hybbx_session_write_line(session,
+ "Unknown /proxymail subcommand. Try /help proxymail.");
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_proxychat(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ hybbx_result_t rc;
+
+ (void)service;
+
+ if (hybbx_session_is_guest(session)) {
+ hybbx_session_write_line(session, "Guests cannot use proxychat.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc > 0) {
+ hybbx_session_write_line(session,
+ "Usage: /proxychat — chat with users on other mains.");
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_session_enter_proxychat(session);
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ hybbx_proxychat_show_banner(session);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_mail(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ char subject[HYBBX_MAIL_SUBJECT_MAX + 1];
+ hybbx_result_t rc;
+
+ if (hybbx_session_is_guest(session)) {
+ hybbx_session_write_line(session, "Guests cannot use mail.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (cmd->argc > 0 && str_ieq(cmd->argv[0], "proxymail")) {
+ hybbx_parsed_command_t proxy_cmd;
+ char proxy_verb[] = "proxymail";
+ unsigned i;
+
+ memset(&proxy_cmd, 0, sizeof(proxy_cmd));
+ proxy_cmd.verb = proxy_verb;
+ proxy_cmd.argc = cmd->argc > 0 ? cmd->argc - 1 : 0;
+ for (i = 0; i < proxy_cmd.argc && i < HYBBX_CMD_TOKEN_MAX; i++) {
+ proxy_cmd.argv[i] = cmd->argv[i + 1];
+ }
+ return cmd_proxymail(service, session, &proxy_cmd);
+ }
+
+ if (cmd->argc == 0) {
+ hybbx_mail_list_inbox(service, session);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "list")) {
+ unsigned from = 1;
+ unsigned to = 0;
+
+ if (cmd->argc >= 2) {
+ if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) {
+ hybbx_session_write_line(session,
+ "Usage: /mail list [<from>-<to>] e.g. list 1-15 list 5-20");
+ return HYBBX_OK;
+ }
+ }
+
+ hybbx_mail_list_inbox_range(service, session, from, to);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "delete") || str_ieq(cmd->argv[0], "del")) {
+ unsigned from = 1;
+ unsigned to = 0;
+
+ if (cmd->argc < 2 || cmd->argv[1] == NULL) {
+ hybbx_session_write_line(session,
+ "Usage: /mail delete <n|from-to> e.g. delete 3 delete 5-20");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) {
+ hybbx_session_write_line(session,
+ "Usage: /mail delete <n|from-to> e.g. delete 3 delete 5-20");
+ return HYBBX_OK;
+ }
+
+ return hybbx_mail_delete_range(service, session, from, to);
+ }
+
+ if (str_ieq(cmd->argv[0], "recycle")) {
+ return hybbx_mail_recycle_empty(service, session);
+ }
+
+ if (str_ieq(cmd->argv[0], "read")) {
+ unsigned index;
+
+ if (cmd->argc < 2 || cmd->argv[1] == NULL) {
+ hybbx_session_write_line(session, "Usage: /mail read <n>");
+ return HYBBX_OK;
+ }
+
+ index = (unsigned)strtoul(cmd->argv[1], NULL, 10);
+ if (index == 0) {
+ hybbx_session_write_line(session, "Usage: /mail read <n>");
+ return HYBBX_OK;
+ }
+
+ return hybbx_mail_read(service, session, index);
+ }
+
+ if (str_ieq(cmd->argv[0], "send")) {
+ if (cmd->argc < 3) {
+ hybbx_session_write_line(session,
+ "Usage: /mail send <user> <subject>");
+ return HYBBX_OK;
+ }
+
+ mail_join_subject(cmd, subject, sizeof(subject));
+ if (subject[0] == '\0') {
+ hybbx_session_write_line(session,
+ "Usage: /mail send <user> <subject>");
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_session_mail_compose_start(session, cmd->argv[1], subject);
+ if (rc == HYBBX_ERR_INVALID) {
+ hybbx_session_write_line(session, "Subject too long.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ hybbx_session_write_line(session, "Compose body. /mail done to send.");
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "done")) {
+ const char *body;
+ const char *to_user;
+ const char *mail_subject;
+
+ if (!hybbx_session_mail_composing(session)) {
+ hybbx_session_write_line(session, "Not composing mail.");
+ return HYBBX_OK;
+ }
+
+ to_user = hybbx_session_mail_compose_to(session);
+ mail_subject = hybbx_session_mail_compose_subject(session);
+ body = hybbx_session_mail_compose_body(session);
+
+ rc = hybbx_mail_deliver(service, hybbx_session_display_name(session),
+ to_user, mail_subject, body);
+
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(session, "Unknown recipient.");
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_DENIED) {
+ hybbx_session_write_line(session, "Cannot mail that user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Send failed.");
+ return rc;
+ }
+
+ hybbx_session_write_line(session, "Message sent.");
+ hybbx_session_leave_area(session);
+ hybbx_session_show_prompt(session);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "cancel")) {
+ if (hybbx_session_mail_composing(session)) {
+ hybbx_session_leave_area(session);
+ hybbx_session_write_line(session, "Compose cancelled.");
+ } else {
+ hybbx_session_write_line(session, "Not composing mail.");
+ }
+ return HYBBX_OK;
+ }
+
+ hybbx_session_write_line(session, "Unknown /mail subcommand. Try /help mail.");
+ return HYBBX_OK;
+}
+
+static void cmd_emit_current_area(hybbx_session_t *session)
+{
+ const char *name = hybbx_session_area_name(hybbx_session_area(session));
+ char line[64];
+
+ snprintf(line, sizeof(line), "%c%s.",
+ (char)toupper((unsigned char)name[0]), name + 1);
+ hybbx_session_write_line(session, line);
+ hybbx_session_show_prompt(session);
+}
+
+static hybbx_result_t cmd_leave(hybbx_session_t *session)
+{
+ hybbx_session_area_t before = hybbx_session_area(session);
+
+ hybbx_session_leave_area(session);
+ if (hybbx_session_area(session) != before) {
+ cmd_emit_current_area(session);
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_main(hybbx_session_t *session)
+{
+ hybbx_session_area_t before = hybbx_session_area(session);
+
+ hybbx_session_go_main(session);
+ if (before != HYBBX_AREA_MAIN) {
+ cmd_emit_current_area(session);
+ }
+
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_exit(hybbx_session_t *session)
+{
+ hybbx_session_write_line(session, "Goodbye.");
+ return HYBBX_SESSION_END;
+}
+
+static hybbx_result_t cmd_broadcast(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ char message[HYBBX_BROADCAST_MESSAGE_MAX + 1];
+ size_t off = 0;
+ int i;
+ hybbx_result_t rc;
+
+ if (cmd->argc < 1) {
+ cmd_registry_usage(session, "broadcast");
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(cmd->argv[0], "ax25")) {
+ if (cmd->argc > 1) {
+ hybbx_session_write_line(session,
+ "RF beacon uses ax25_auto_message from INI. Use: /broadcast ax25");
+ return HYBBX_OK;
+ }
+
+ rc = hybbx_broadcast_ax25_manual(service);
+ if (rc == HYBBX_ERR_UNSUPPORTED) {
+ hybbx_session_write_line(session, "AX.25 broadcast is disabled.");
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_DENIED) {
+ hybbx_session_write_line(session,
+ "No qualifying packet-radio links for AX.25 broadcast.");
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_BUSY) {
+ hybbx_session_write_line(session,
+ "AX.25 broadcast deferred (link busy).");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "AX.25 broadcast failed.");
+ return HYBBX_ERR_IO;
+ }
+
+ hybbx_session_write_line(session, "AX.25 broadcast sent.");
+ return HYBBX_OK;
+ }
+
+ message[0] = '\0';
+ for (i = 0; i < (int)cmd->argc; i++) {
+ size_t part_len = strlen(cmd->argv[i]);
+
+ if (off > 0 && off < sizeof(message) - 1) {
+ message[off++] = ' ';
+ }
+ if (off + part_len >= sizeof(message)) {
+ part_len = sizeof(message) - off - 1;
+ }
+ if (part_len > 0) {
+ memcpy(message + off, cmd->argv[i], part_len);
+ off += part_len;
+ }
+ }
+ message[off] = '\0';
+
+ if (message[0] == '\0') {
+ hybbx_session_write_line(session, "Missing announce message.");
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_broadcast_announce(service, session, message);
+ if (rc == HYBBX_ERR_UNSUPPORTED) {
+ hybbx_session_write_line(session, "Broadcast is disabled.");
+ return HYBBX_OK;
+ }
+ if (rc == HYBBX_ERR_INVALID) {
+ hybbx_session_write_line(session, "Message too long.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ hybbx_session_write_line(session, "Broadcast failed.");
+ return HYBBX_ERR_IO;
+ }
+
+ hybbx_session_write_line(session, "Broadcast sent.");
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_shutdown(hybbx_service_t *service,
+ hybbx_session_t *session)
+{
+ if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) {
+ cmd_deny_privilege(session);
+ return HYBBX_ERR_DENIED;
+ }
+
+ hybbx_security_log_write("shutdown ip=%s user=%s transport=%s",
+ hybbx_session_record(session) != NULL &&
+ hybbx_session_record(session)->remote[0] != '\0' ?
+ hybbx_session_record(session)->remote : "?",
+ hybbx_session_display_name(session),
+ hybbx_session_record(session) != NULL ?
+ hybbx_session_record(session)->transport : "?");
+ hybbx_session_write_line(session, "Shutting down HyBBX.");
+ hybbx_service_request_shutdown(service, 0);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_restart(hybbx_service_t *service,
+ hybbx_session_t *session)
+{
+ if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) {
+ cmd_deny_privilege(session);
+ return HYBBX_ERR_DENIED;
+ }
+
+ hybbx_security_log_write("restart ip=%s user=%s transport=%s",
+ hybbx_session_record(session) != NULL &&
+ hybbx_session_record(session)->remote[0] != '\0' ?
+ hybbx_session_record(session)->remote : "?",
+ hybbx_session_display_name(session),
+ hybbx_session_record(session) != NULL ?
+ hybbx_session_record(session)->transport : "?");
+ hybbx_session_write_line(session, "Restarting HyBBX.");
+ hybbx_service_request_shutdown(service, 1);
+ return HYBBX_OK;
+}
+
+static hybbx_result_t cmd_version(hybbx_service_t *service,
+ hybbx_session_t *session)
+{
+ const hybbx_texts_config_t *texts = hybbx_service_get_texts(service);
+
+ if (texts == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ return hybbx_texts_send_version(texts, session);
+}
+
+static hybbx_result_t cmd_clear(hybbx_session_t *session)
+{
+ return hybbx_session_clear_terminal(session);
+}
+
+static hybbx_result_t cmd_echo(hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ char buf[48];
+
+ if (cmd->argc < 2) {
+ snprintf(buf, sizeof(buf), "Input echo is %s.",
+ hybbx_bool_to_string(hybbx_session_input_echo(session)));
+ hybbx_session_write_line(session, buf);
+ return HYBBX_OK;
+ }
+
+ if (hybbx_bool_is_true(cmd->argv[1])) {
+ return hybbx_session_set_input_echo(session, 1);
+ }
+
+ if (hybbx_bool_is_false(cmd->argv[1])) {
+ return hybbx_session_set_input_echo(session, 0);
+ }
+
+ hybbx_session_write_line(session, "Usage: /echo yes|no");
+ return HYBBX_ERR_INVALID;
+}
+
+typedef struct monitor_list_ctx {
+ hybbx_session_t *requester;
+ unsigned count;
+} monitor_list_ctx_t;
+
+static void monitor_list_visitor(hybbx_session_t *session, void *userdata)
+{
+ monitor_list_ctx_t *ctx = (monitor_list_ctx_t *)userdata;
+ const hybbx_session_record_t *rec;
+ const char *user;
+ const char *plugin;
+ char line[96];
+
+ if (ctx == NULL || session == NULL || !hybbx_session_logged_in(session)) {
+ return;
+ }
+
+ if (!hybbx_session_is_interactive_user(session)) {
+ return;
+ }
+
+ if (hybbx_session_hidden_from_who(session)) {
+ return;
+ }
+
+ user = hybbx_session_display_name(session);
+ rec = hybbx_session_record(session);
+ plugin = (rec != NULL && rec->transport[0] != '\0')
+ ? rec->transport
+ : (session->transport != NULL ? session->transport->name : "?");
+ snprintf(line, sizeof(line), " %s@%s", user, plugin);
+ hybbx_session_write_line(ctx->requester, line);
+ ctx->count++;
+}
+
+static void monitor_show_cmds(hybbx_session_t *session)
+{
+ hybbx_session_write_line(session, "Monitor admin commands:");
+ hybbx_session_write_line(session,
+ " /monitor meet <user> [force] — conference invite (20s) or force");
+ hybbx_session_write_line(session,
+ "Run any of these as normal /verb (stay invisible while Sysop hidden):");
+ hybbx_commands_registry_show_sysop_cmds(session);
+}
+
+static hybbx_result_t cmd_monitor(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ const char *sub;
+ monitor_list_ctx_t list_ctx;
+ const hybbx_monitor_config_t *mcfg;
+ char line[128];
+
+ if (!hybbx_monitor_enabled()) {
+ hybbx_session_write_line(session, "Monitor is disabled ([monitor] enabled=no).");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (!hybbx_monitor_session_may_use(session) &&
+ !hybbx_commands_registry_verb_allowed(
+ hybbx_session_user_level(session), "monitor")) {
+ hybbx_session_write_line(session, "Monitor access denied.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ sub = (cmd->argc >= 1) ? cmd->argv[0] : "list";
+
+ if (str_ieq(sub, "on")) {
+ return hybbx_monitor_set_active(session, 1);
+ }
+
+ if (str_ieq(sub, "off")) {
+ return hybbx_monitor_set_active(session, 0);
+ }
+
+ if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands")) {
+ monitor_show_cmds(session);
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(sub, "meet")) {
+ int force = 0;
+ const char *user;
+
+ if (cmd->argc < 2) {
+ hybbx_session_write_line(session,
+ "Usage: /monitor meet <user> [force]");
+ return HYBBX_ERR_INVALID;
+ }
+ user = cmd->argv[1];
+ if (cmd->argc >= 3 && str_ieq(cmd->argv[2], "force")) {
+ force = 1;
+ }
+ if (!hybbx_monitor_is_active(session)) {
+ hybbx_session_write_line(session,
+ "Turn monitor on first: /monitor on");
+ return HYBBX_ERR_DENIED;
+ }
+ return hybbx_conference_monitor_meet(service, session, user, force);
+ }
+
+ if (!str_ieq(sub, "list") && cmd->argc >= 1) {
+ hybbx_session_write_line(session,
+ "Usage: /monitor on|off|list|cmds|meet <user> [force]");
+ return HYBBX_ERR_INVALID;
+ }
+
+ mcfg = hybbx_monitor_config_get();
+ snprintf(line, sizeof(line), "Monitor: %s",
+ hybbx_monitor_is_active(session) ? "on" : "off");
+ hybbx_session_write_line(session, line);
+ if (mcfg != NULL) {
+ snprintf(line, sizeof(line),
+ "Follow: hybbx=%s security=%s invisible-sysop=%s "
+ "invite_timeout=%us",
+ mcfg->follow_hybbx ? "yes" : "no",
+ mcfg->follow_security ? "yes" : "no",
+ mcfg->invisible_sysop ? "yes" : "no",
+ mcfg->invite_timeout_sec);
+ hybbx_session_write_line(session, line);
+ }
+ if (hybbx_session_hidden_from_who(session)) {
+ hybbx_session_write_line(session, "You are hidden from /who.");
+ }
+ hybbx_session_write_line(session, "Online (visible):");
+ list_ctx.requester = session;
+ list_ctx.count = 0;
+ hybbx_service_visit_sessions(service, monitor_list_visitor, &list_ctx);
+ if (list_ctx.count == 0) {
+ hybbx_session_write_line(session, " (none)");
+ }
+ return HYBBX_OK;
+}
+
+hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const hybbx_parsed_command_t *cmd)
+{
+ if (cmd == NULL || service == NULL || session == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (cmd->scope != HYBBX_CMD_SCOPE_HYBBX) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ (void)hybbx_session_command_gap(session);
+
+ if (cmd->verb == NULL || cmd->verb[0] == '\0') {
+ return cmd_help(session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "help") || str_ieq(cmd->verb, "?")) {
+ return cmd_help(session, cmd);
+ }
+
+ {
+ hybbx_result_t access = cmd_check_access(session, cmd->verb);
+ if (access != HYBBX_OK) {
+ return access;
+ }
+ }
+
+ if (str_ieq(cmd->verb, "news")) {
+ return cmd_news(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "banner") || str_ieq(cmd->verb, "loginmsg")) {
+ return cmd_banner(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "motd")) {
+ return cmd_motd(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "rules") || str_ieq(cmd->verb, "legal")) {
+ return cmd_rules(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "who") || str_ieq(cmd->verb, "online")) {
+ return cmd_who(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "monitor") || str_ieq(cmd->verb, "mon")) {
+ return cmd_monitor(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "users")) {
+ return cmd_users(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "session") || str_ieq(cmd->verb, "info")) {
+ return cmd_session(session);
+ }
+
+ if (str_ieq(cmd->verb, "version") || str_ieq(cmd->verb, "ver")) {
+ return cmd_version(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "leave") || str_ieq(cmd->verb, "back")) {
+ return cmd_leave(session);
+ }
+
+ if (str_ieq(cmd->verb, "main")) {
+ return cmd_main(session);
+ }
+
+ if (str_ieq(cmd->verb, "menu")) {
+ if (cmd->argc == 0) {
+ hybbx_commands_registry_show_menu(session);
+ return HYBBX_OK;
+ }
+ return cmd_help_topic(session, cmd->argv[0]);
+ }
+
+ if (str_ieq(cmd->verb, "index")) {
+ if (cmd->argc == 0) {
+ hybbx_commands_registry_show_index(session);
+ return HYBBX_OK;
+ }
+ return cmd_help_topic(session, cmd->argv[0]);
+ }
+
+ if (str_ieq(cmd->verb, "alias")) {
+ if (cmd->argc == 0) {
+ hybbx_commands_registry_show_aliases(session);
+ return HYBBX_OK;
+ }
+ return cmd_help_topic(session, cmd->argv[0]);
+ }
+
+ if (str_ieq(cmd->verb, "clear") || str_ieq(cmd->verb, "cls") ||
+ str_ieq(cmd->verb, "reset")) {
+ return cmd_clear(session);
+ }
+
+ if (str_ieq(cmd->verb, "echo")) {
+ return cmd_echo(session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "chat")) {
+ return cmd_chat(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "conference") || str_ieq(cmd->verb, "meeting")) {
+ return cmd_conference(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "mail")) {
+ return cmd_mail(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "proxymail")) {
+ return cmd_proxymail(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "proxychat")) {
+ return cmd_proxychat(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "login")) {
+ return cmd_login(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "register")) {
+ return cmd_register(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "changeme")) {
+ return cmd_changeme(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "changeuser") || str_ieq(cmd->verb, "userchange")) {
+ return cmd_userchange(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "deleteuser") || str_ieq(cmd->verb, "userdelete")) {
+ return cmd_userdelete(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "usercreate") || str_ieq(cmd->verb, "createuser")) {
+ return cmd_createuser(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "activate")) {
+ return cmd_activate(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "promote")) {
+ return cmd_promote(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "demote")) {
+ return cmd_demote(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "delete") || str_ieq(cmd->verb, "del")) {
+ return cmd_delete(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "deleteme")) {
+ return cmd_deleteme(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "broadcast") || str_ieq(cmd->verb, "announce")) {
+ return cmd_broadcast(service, session, cmd);
+ }
+
+ if (str_ieq(cmd->verb, "shutdown")) {
+ return cmd_shutdown(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "restart")) {
+ return cmd_restart(service, session);
+ }
+
+ if (str_ieq(cmd->verb, "exit") || str_ieq(cmd->verb, "logout") ||
+ str_ieq(cmd->verb, "bye") || str_ieq(cmd->verb, "quit")) {
+ return cmd_exit(session);
+ }
+
+ hybbx_session_write_line(session, cmd_help_unknown(session));
+ return HYBBX_ERR_NOT_FOUND;
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com