diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 /src/core/chat.c | |
#2
Diffstat (limited to 'src/core/chat.c')
| -rw-r--r-- | src/core/chat.c | 463 |
1 files changed, 463 insertions, 0 deletions
diff --git a/src/core/chat.c b/src/core/chat.c new file mode 100644 index 0000000..fdd4fa4 --- /dev/null +++ b/src/core/chat.c @@ -0,0 +1,463 @@ +#include "hybbx/chat.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/traffic.h" +#include "hybbx/log.h" + +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 int channel_name_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '_'; +} + +static int channel_name_valid(const char *name) +{ + size_t len; + size_t i; + + if (name == NULL) { + return 0; + } + + len = strlen(name); + if (len < 2 || len >= HYBBX_CHAT_CHANNEL_NAME_MAX) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!channel_name_char_ok(name[i])) { + return 0; + } + } + + return 1; +} + +void hybbx_chat_config_defaults(hybbx_chat_config_t *chat) +{ + unsigned i; + + if (chat == NULL) { + return; + } + + chat->channel_count = HYBBX_DEFAULT_CHAT_CHANNELS; + chat->message_max = HYBBX_CHAT_MESSAGE_MAX; + + for (i = 0; i < HYBBX_CHAT_CHANNEL_MAX; i++) { + snprintf(chat->names[i], sizeof(chat->names[i]), "Channel%u", i + 1); + } +} + +void hybbx_chat_config_apply(hybbx_chat_config_t *chat, + const hybbx_config_t *config) +{ + unsigned count; + unsigned i; + char key[16]; + const char *value; + + if (chat == NULL || config == NULL) { + return; + } + + hybbx_chat_config_defaults(chat); + + count = hybbx_config_get_uint(config, "chat", "channels", + HYBBX_DEFAULT_CHAT_CHANNELS, 1u, + HYBBX_CHAT_CHANNEL_MAX); + chat->channel_count = count; + + chat->message_max = hybbx_config_get_uint(config, "chat", "message_max", + HYBBX_CHAT_MESSAGE_MAX, 1u, + HYBBX_LINE_MAX - 1u); + + for (i = 1; i <= chat->channel_count; i++) { + snprintf(key, sizeof(key), "channel%u", i); + value = hybbx_config_get(config, "chat", key, NULL); + if (value != NULL && value[0] != '\0' && channel_name_valid(value)) { + hybbx_strlcpy(chat->names[i - 1], value, + sizeof(chat->names[i - 1])); + } + } + + hybbx_log_info("[chat] channels=%u message_max=%u", + chat->channel_count, chat->message_max); +} + +const char *hybbx_chat_channel_name(const hybbx_chat_config_t *chat, + unsigned channel_index) +{ + if (chat == NULL || channel_index == 0 || + channel_index > chat->channel_count) { + return NULL; + } + + return chat->names[channel_index - 1]; +} + +hybbx_result_t hybbx_chat_resolve_channel(const hybbx_chat_config_t *chat, + const char *spec, + unsigned *out_index) +{ + char *end; + unsigned long parsed; + unsigned i; + + if (chat == NULL || spec == NULL || out_index == NULL || spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + parsed = strtoul(spec, &end, 10); + if (end != spec && *end == '\0' && parsed >= 1 && + parsed <= chat->channel_count) { + *out_index = (unsigned)parsed; + return HYBBX_OK; + } + + for (i = 1; i <= chat->channel_count; i++) { + if (str_ieq(chat->names[i - 1], spec)) { + *out_index = i; + return HYBBX_OK; + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +void hybbx_chat_list_channels(hybbx_session_t *session, + const hybbx_chat_config_t *chat) +{ + unsigned i; + char line[HYBBX_CHAT_CHANNEL_NAME_MAX + 32]; + unsigned current; + + if (session == NULL || chat == NULL) { + return; + } + + current = hybbx_session_chat_channel(session); + + hybbx_session_write_line(session, "Channels:"); + for (i = 1; i <= chat->channel_count; i++) { + snprintf(line, sizeof(line), " %u %s", i, chat->names[i - 1]); + hybbx_session_write_line(session, line); + } + + if (current > 0) { + snprintf(line, sizeof(line), "Current: %u %s", + current, hybbx_chat_channel_name(chat, current)); + hybbx_session_write_line(session, line); + } else { + hybbx_session_write_line(session, "Use /chat <n> or <name>."); + } +} + +typedef struct chat_broadcast_ctx { + hybbx_session_t *from; + const char *message; + const char *from_user; + unsigned channel; +} chat_broadcast_ctx_t; + +static void chat_broadcast_visitor(hybbx_session_t *session, void *userdata) +{ + chat_broadcast_ctx_t *ctx = (chat_broadcast_ctx_t *)userdata; + char line[HYBBX_USER_NAME_MAX + HYBBX_LINE_MAX + 16]; + + if (session == NULL || ctx == NULL || ctx->message == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_chat_channel(session) != ctx->channel) { + return; + } + + if (session == ctx->from) { + snprintf(line, sizeof(line), "ME: %s", ctx->message); + } else { + snprintf(line, sizeof(line), "%s: %s", ctx->from_user, ctx->message); + } + + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_chat_post(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + chat_broadcast_ctx_t ctx; + const hybbx_chat_config_t *chat; + unsigned channel; + + if (service == NULL || from == NULL || message == NULL) { + return HYBBX_ERR_INVALID; + } + + if (message[0] == '\0') { + return HYBBX_OK; + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(message) > chat->message_max) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_session_area(from) != HYBBX_AREA_CHAT) { + return HYBBX_ERR_INVALID; + } + + channel = hybbx_session_chat_channel(from); + if (channel == 0) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.message = message; + ctx.from_user = hybbx_session_display_name(from); + ctx.channel = channel; + + hybbx_service_visit_sessions(service, chat_broadcast_visitor, &ctx); + return HYBBX_OK; +} + +#define CHAT_SHOWALL_MAX 128u + +typedef struct chat_show_entry { + unsigned channel; + char label[HYBBX_USER_NAME_MAX + HYBBX_CHAT_CHANNEL_NAME_MAX + 4]; +} chat_show_entry_t; + +typedef struct chat_show_ctx { + hybbx_session_t *requester; + unsigned channel; + unsigned count; + chat_show_entry_t entries[CHAT_SHOWALL_MAX]; + const hybbx_chat_config_t *chat; +} chat_show_ctx_t; + +static void chat_show_channel_visitor(hybbx_session_t *session, void *userdata) +{ + chat_show_ctx_t *ctx = (chat_show_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || session == ctx->requester) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_chat_channel(session) != ctx->channel) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + hybbx_session_write_line(ctx->requester, hybbx_session_display_name(session)); +} + +void hybbx_chat_show_channel(hybbx_service_t *service, hybbx_session_t *session) +{ + chat_show_ctx_t ctx; + unsigned channel; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + hybbx_session_write_line(session, "Not in a chat channel."); + return; + } + + channel = hybbx_session_chat_channel(session); + if (channel == 0) { + hybbx_session_write_line(session, "Not in a chat channel."); + return; + } + + ctx.requester = session; + ctx.channel = channel; + ctx.count = 0; + ctx.chat = NULL; + + hybbx_service_visit_sessions(service, chat_show_channel_visitor, &ctx); +} + +static void chat_show_all_collect(hybbx_session_t *session, void *userdata) +{ + chat_show_ctx_t *ctx = (chat_show_ctx_t *)userdata; + unsigned channel; + const char *ch_name; + chat_show_entry_t *entry; + + if (session == NULL || ctx == NULL || ctx->chat == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + channel = hybbx_session_chat_channel(session); + if (channel == 0 || channel > ctx->chat->channel_count) { + return; + } + + if (ctx->count >= CHAT_SHOWALL_MAX) { + return; + } + + ch_name = hybbx_chat_channel_name(ctx->chat, channel); + if (ch_name == NULL) { + return; + } + + entry = &ctx->entries[ctx->count++]; + entry->channel = channel; + snprintf(entry->label, sizeof(entry->label), "%s@%s", + hybbx_session_display_name(session), ch_name); +} + +static int chat_show_entry_cmp(const void *a, const void *b) +{ + const chat_show_entry_t *ea = (const chat_show_entry_t *)a; + const chat_show_entry_t *eb = (const chat_show_entry_t *)b; + + if (ea->channel != eb->channel) { + return (ea->channel < eb->channel) ? -1 : 1; + } + + return strcmp(ea->label, eb->label); +} + +static void chat_showall_flush(hybbx_session_t *session, char *line, size_t *pos) +{ + if (session == NULL || line == NULL || pos == NULL || *pos == 0) { + return; + } + + line[*pos] = '\0'; + hybbx_session_write_line(session, line); + line[0] = '\0'; + *pos = 0; +} + +static void chat_showall_append(hybbx_session_t *session, char *line, + size_t line_size, size_t *pos, + const char *entry) +{ + size_t entry_len; + size_t need; + + if (session == NULL || line == NULL || pos == NULL || entry == NULL) { + return; + } + + entry_len = strlen(entry); + need = (*pos > 0) ? (*pos + 1 + entry_len) : entry_len; + + if (*pos > 0 && need > HYBBX_LINE_WIDTH) { + chat_showall_flush(session, line, pos); + need = entry_len; + } + + if (entry_len >= line_size) { + return; + } + + if (*pos > 0) { + line[(*pos)++] = ' '; + } + + memcpy(line + *pos, entry, entry_len); + *pos += entry_len; + line[*pos] = '\0'; +} + +void hybbx_chat_show_all(hybbx_service_t *service, hybbx_session_t *session) +{ + chat_show_ctx_t ctx; + char line[HYBBX_LINE_WIDTH + 1]; + size_t pos = 0; + unsigned i; + + if (service == NULL || session == NULL) { + return; + } + + ctx.requester = session; + ctx.channel = 0; + ctx.count = 0; + ctx.chat = hybbx_service_get_chat(service); + if (ctx.chat == NULL) { + return; + } + + hybbx_service_visit_sessions(service, chat_show_all_collect, &ctx); + + if (ctx.count == 0) { + return; + } + + qsort(ctx.entries, ctx.count, sizeof(ctx.entries[0]), chat_show_entry_cmp); + + line[0] = '\0'; + for (i = 0; i < ctx.count; i++) { + chat_showall_append(session, line, sizeof(line), &pos, ctx.entries[i].label); + } + + chat_showall_flush(session, line, &pos); +} |
