summaryrefslogtreecommitdiff
path: root/src/core/conference.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /src/core/conference.c
#2
Diffstat (limited to 'src/core/conference.c')
-rw-r--r--src/core/conference.c720
1 files changed, 720 insertions, 0 deletions
diff --git a/src/core/conference.c b/src/core/conference.c
new file mode 100644
index 0000000..890348f
--- /dev/null
+++ b/src/core/conference.c
@@ -0,0 +1,720 @@
+#include "hybbx/conference.h"
+#include "hybbx/chat.h"
+#include "hybbx/messages.h"
+#include "hybbx/service.h"
+#include "hybbx/session.h"
+#include "hybbx/storage.h"
+#include "hybbx/auth.h"
+#include "hybbx/monitor.h"
+#include "hybbx/traffic.h"
+#include "hybbx/util.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <time.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 invite_reply_yes(const char *line)
+{
+ return line != NULL &&
+ (str_ieq(line, "y") || str_ieq(line, "yes"));
+}
+
+static int invite_reply_no(const char *line)
+{
+ return line != NULL &&
+ (str_ieq(line, "n") || str_ieq(line, "no"));
+}
+
+static int topic_char_ok(char ch)
+{
+ return (ch >= 'a' && ch <= 'z') ||
+ (ch >= 'A' && ch <= 'Z') ||
+ (ch >= '0' && ch <= '9') ||
+ ch == ' ' || ch == '-' || ch == '_';
+}
+
+static int conference_topic_valid(const char *topic)
+{
+ size_t len;
+ size_t i;
+
+ if (topic == NULL) {
+ return 0;
+ }
+
+ len = strlen(topic);
+ if (len < 2 || len >= HYBBX_CONFERENCE_TOPIC_MAX) {
+ return 0;
+ }
+
+ for (i = 0; i < len; i++) {
+ if (!topic_char_ok(topic[i])) {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+typedef struct find_user_session_ctx {
+ const char *username;
+ hybbx_session_t *found;
+} find_user_session_ctx_t;
+
+static void find_user_session_visitor(hybbx_session_t *session, void *userdata)
+{
+ find_user_session_ctx_t *ctx = (find_user_session_ctx_t *)userdata;
+
+ if (ctx == NULL || session == NULL || ctx->found != NULL) {
+ return;
+ }
+
+ if (!hybbx_session_logged_in(session) || hybbx_session_is_guest(session)) {
+ return;
+ }
+
+ if (str_ieq(hybbx_session_username(session), ctx->username)) {
+ ctx->found = session;
+ }
+}
+
+static hybbx_session_t *conference_find_online_user(hybbx_service_t *service,
+ const char *username)
+{
+ find_user_session_ctx_t ctx;
+
+ if (service == NULL || username == NULL || username[0] == '\0') {
+ return NULL;
+ }
+
+ ctx.username = username;
+ ctx.found = NULL;
+ hybbx_service_visit_sessions(service, find_user_session_visitor, &ctx);
+ return ctx.found;
+}
+
+static int conference_session_active(const hybbx_session_t *session)
+{
+ return session != NULL &&
+ hybbx_session_area(session) == HYBBX_AREA_CONFERENCE;
+}
+
+typedef struct invite_cancel_ctx {
+ const char *from_username;
+} invite_cancel_ctx_t;
+
+static void invite_cancel_visitor(hybbx_session_t *session, void *userdata)
+{
+ invite_cancel_ctx_t *ctx = (invite_cancel_ctx_t *)userdata;
+
+ if (session == NULL || ctx == NULL || ctx->from_username == NULL) {
+ return;
+ }
+
+ if (!hybbx_conference_invite_pending(session)) {
+ return;
+ }
+
+ if (!str_ieq(hybbx_session_conference_invite_from(session),
+ ctx->from_username)) {
+ return;
+ }
+
+ hybbx_session_clear_conference_invite(session);
+ hybbx_session_write_line(session, "*** Conference invite cancelled.");
+}
+
+static void conference_cancel_invites_from(hybbx_service_t *service,
+ const char *from_username)
+{
+ invite_cancel_ctx_t ctx;
+
+ if (service == NULL || from_username == NULL || from_username[0] == '\0') {
+ return;
+ }
+
+ ctx.from_username = from_username;
+ hybbx_service_visit_sessions(service, invite_cancel_visitor, &ctx);
+}
+
+static void conference_notify_partner_left(hybbx_session_t *session,
+ const char *partner_username)
+{
+ hybbx_service_t *service;
+ hybbx_session_t *partner;
+ char line[HYBBX_LINE_MAX];
+
+ if (session == NULL || partner_username == NULL ||
+ partner_username[0] == '\0') {
+ return;
+ }
+
+ service = hybbx_session_service(session);
+ if (service == NULL) {
+ return;
+ }
+
+ partner = conference_find_online_user(service, partner_username);
+ if (partner == NULL || !conference_session_active(partner)) {
+ return;
+ }
+
+ if (!str_ieq(hybbx_session_conference_partner(partner),
+ hybbx_session_username(session))) {
+ return;
+ }
+
+ hybbx_session_clear_conference(partner);
+ snprintf(line, sizeof(line), "*** %s left the conference.",
+ hybbx_session_display_name(session));
+ hybbx_session_write_line(partner, line);
+ if (hybbx_session_area(partner) == HYBBX_AREA_CONFERENCE) {
+ (void)hybbx_session_leave_area(partner);
+ }
+}
+
+static hybbx_result_t conference_activate(hybbx_service_t *service,
+ hybbx_session_t *initiator,
+ hybbx_session_t *partner,
+ const char *topic)
+{
+ char line[HYBBX_LINE_MAX];
+ hybbx_result_t rc;
+
+ if (service == NULL || initiator == NULL || partner == NULL ||
+ topic == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_session_join_conference(initiator, topic,
+ hybbx_session_username(partner));
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ rc = hybbx_session_join_conference(partner, topic,
+ hybbx_session_username(initiator));
+ if (rc != HYBBX_OK) {
+ hybbx_session_clear_conference(initiator);
+ (void)hybbx_session_leave_area(initiator);
+ return rc;
+ }
+
+ snprintf(line, sizeof(line), "Conference: %s", topic);
+ hybbx_session_write_line(initiator, line);
+ hybbx_session_write_line(initiator,
+ "Each line is a message; /leave or /main to exit.");
+
+ snprintf(line, sizeof(line), "Conference: %s", topic);
+ hybbx_session_write_line(partner, line);
+ hybbx_session_write_line(partner,
+ "Each line is a message; /leave or /main to exit.");
+
+ return HYBBX_OK;
+}
+
+hybbx_result_t hybbx_conference_start(hybbx_service_t *service,
+ hybbx_session_t *initiator,
+ const char *topic,
+ const char *partner_spec)
+{
+ hybbx_storage_t *storage;
+ hybbx_user_record_t partner_user;
+ hybbx_session_t *partner_session;
+ char line[HYBBX_LINE_MAX];
+ hybbx_result_t rc;
+
+ if (service == NULL || initiator == NULL || topic == NULL ||
+ partner_spec == NULL || topic[0] == '\0' || partner_spec[0] == '\0') {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (hybbx_session_is_guest(initiator)) {
+ hybbx_session_write_line(initiator, "Guests cannot use conference.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (!conference_topic_valid(topic)) {
+ hybbx_session_write_line(initiator, "Invalid conference topic.");
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (conference_session_active(initiator)) {
+ hybbx_session_write_line(initiator, "Already in a conference.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_conference_invite_pending(initiator)) {
+ hybbx_session_write_line(initiator,
+ "Answer the pending conference invite first (y/n).");
+ return HYBBX_OK;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_resolve_user(storage, partner_spec, &partner_user);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(initiator, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (!partner_user.active) {
+ hybbx_session_write_line(initiator, "User is not active.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_user_level_is_guest(partner_user.level)) {
+ hybbx_session_write_line(initiator, "Cannot conference with a guest.");
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(partner_user.username, hybbx_session_username(initiator))) {
+ hybbx_session_write_line(initiator, "Cannot conference with yourself.");
+ return HYBBX_OK;
+ }
+
+ if (!hybbx_session_conference_may_invite(initiator, partner_user.username)) {
+ hybbx_session_write_line(initiator,
+ "Invite limit: 2 per user per 30 minutes.");
+ return HYBBX_OK;
+ }
+
+ partner_session = conference_find_online_user(service, partner_user.username);
+ if (partner_session == NULL) {
+ hybbx_session_write_line(initiator, "User is not online.");
+ return HYBBX_OK;
+ }
+
+ if (conference_session_active(partner_session)) {
+ hybbx_session_write_line(initiator, "User is already in a conference.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_conference_invite_pending(partner_session)) {
+ hybbx_session_write_line(initiator,
+ "User has a pending conference invite.");
+ return HYBBX_OK;
+ }
+
+ hybbx_session_set_conference_invite(partner_session,
+ hybbx_session_username(initiator),
+ topic);
+ hybbx_session_conference_invite_sent(initiator, partner_user.username);
+
+ snprintf(line, sizeof(line), "Conference invite: %s", topic);
+ (void)hybbx_msg_send_private(partner_session,
+ hybbx_session_display_name(initiator), line);
+ hybbx_session_write_line(partner_session, "Accept? y/n or yes/no");
+
+ snprintf(line, sizeof(line), "Conference invite sent to %s.",
+ hybbx_user_display_name(&partner_user));
+ hybbx_session_write_line(initiator, line);
+
+ return HYBBX_OK;
+}
+
+hybbx_result_t hybbx_conference_reply_invite(hybbx_service_t *service,
+ hybbx_session_t *session,
+ const char *line)
+{
+ char from_username[HYBBX_USER_NAME_MAX];
+ char topic[HYBBX_CONFERENCE_TOPIC_MAX];
+ hybbx_session_t *initiator;
+ char msg[HYBBX_LINE_MAX];
+
+ if (service == NULL || session == NULL || line == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (!hybbx_conference_invite_pending(session)) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (!invite_reply_yes(line) && !invite_reply_no(line)) {
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ {
+ const char *from_ptr = hybbx_session_conference_invite_from(session);
+ const char *topic_ptr = hybbx_session_conference_invite_topic(session);
+
+ if (from_ptr == NULL || from_ptr[0] == '\0' ||
+ topic_ptr == NULL || topic_ptr[0] == '\0') {
+ hybbx_session_clear_conference_invite(session);
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_strlcpy(from_username, from_ptr, sizeof(from_username));
+ hybbx_strlcpy(topic, topic_ptr, sizeof(topic));
+ }
+
+ hybbx_session_clear_conference_invite(session);
+
+ if (invite_reply_no(line)) {
+ initiator = conference_find_online_user(service, from_username);
+ if (initiator != NULL) {
+ snprintf(msg, sizeof(msg),
+ "meet status=declined user=%s",
+ hybbx_session_display_name(session));
+ (void)hybbx_msg_send_system(initiator, msg);
+ }
+ hybbx_session_write_line(session, "*** Conference invite declined.");
+ return HYBBX_OK;
+ }
+
+ if (conference_session_active(session)) {
+ hybbx_session_write_line(session, "Already in a conference.");
+ return HYBBX_OK;
+ }
+
+ initiator = conference_find_online_user(service, from_username);
+ if (initiator == NULL) {
+ hybbx_session_write_line(session, "Inviter is no longer online.");
+ return HYBBX_OK;
+ }
+
+ if (conference_session_active(initiator)) {
+ hybbx_session_write_line(session, "Inviter is already in a conference.");
+ return HYBBX_OK;
+ }
+
+ if (conference_activate(service, initiator, session, topic) != HYBBX_OK) {
+ hybbx_session_write_line(session, "Conference could not be started.");
+ return HYBBX_OK;
+ }
+
+ snprintf(msg, sizeof(msg), "meet status=accepted user=%s",
+ hybbx_session_display_name(session));
+ hybbx_session_write_line(initiator, msg);
+ if (hybbx_monitor_is_active(initiator)) {
+ hybbx_monitor_broadcast(service, msg);
+ }
+
+ return HYBBX_OK;
+}
+
+void hybbx_conference_invite_tick(hybbx_service_t *service,
+ hybbx_session_t *session)
+{
+ time_t deadline;
+ time_t now;
+ char from_username[HYBBX_USER_NAME_MAX];
+ const char *from_ptr;
+ hybbx_session_t *initiator;
+ char msg[HYBBX_LINE_MAX];
+
+ if (service == NULL || session == NULL) {
+ return;
+ }
+
+ if (!hybbx_conference_invite_pending(session)) {
+ return;
+ }
+
+ deadline = hybbx_session_conference_invite_deadline(session);
+ if (deadline == 0) {
+ return;
+ }
+
+ now = time(NULL);
+ if (now < deadline) {
+ return;
+ }
+
+ from_ptr = hybbx_session_conference_invite_from(session);
+ if (from_ptr != NULL) {
+ hybbx_strlcpy(from_username, from_ptr, sizeof(from_username));
+ } else {
+ from_username[0] = '\0';
+ }
+
+ hybbx_session_clear_conference_invite(session);
+ hybbx_session_write_line(session, "*** Conference invite timed out.");
+
+ if (from_username[0] != '\0') {
+ initiator = conference_find_online_user(service, from_username);
+ if (initiator != NULL) {
+ snprintf(msg, sizeof(msg), "meet status=timeout user=%s",
+ hybbx_session_display_name(session));
+ hybbx_session_write_line(initiator, msg);
+ if (hybbx_monitor_is_active(initiator)) {
+ hybbx_monitor_broadcast(service, msg);
+ }
+ }
+ }
+}
+
+hybbx_result_t hybbx_conference_monitor_meet(hybbx_service_t *service,
+ hybbx_session_t *initiator,
+ const char *partner_spec,
+ int force)
+{
+ const hybbx_monitor_config_t *mcfg;
+ hybbx_storage_t *storage;
+ hybbx_user_record_t partner_user;
+ hybbx_session_t *partner_session;
+ char line[HYBBX_LINE_MAX];
+ hybbx_result_t rc;
+ const char *topic = "monitor";
+ unsigned timeout_sec = 20u;
+
+ if (service == NULL || initiator == NULL || partner_spec == NULL ||
+ partner_spec[0] == '\0') {
+ return HYBBX_ERR_INVALID;
+ }
+
+ mcfg = hybbx_monitor_config_get();
+ if (mcfg != NULL) {
+ timeout_sec = mcfg->invite_timeout_sec;
+ }
+
+ if (hybbx_session_is_guest(initiator)) {
+ hybbx_session_write_line(initiator, "Guests cannot use meet.");
+ return HYBBX_ERR_DENIED;
+ }
+
+ if (conference_session_active(initiator)) {
+ hybbx_session_write_line(initiator, "Already in a conference.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_conference_invite_pending(initiator)) {
+ hybbx_session_write_line(initiator,
+ "Answer the pending conference invite first (y/n).");
+ return HYBBX_OK;
+ }
+
+ storage = hybbx_service_get_storage(service);
+ if (storage == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ rc = hybbx_storage_resolve_user(storage, partner_spec, &partner_user);
+ if (rc == HYBBX_ERR_NOT_FOUND) {
+ hybbx_session_write_line(initiator, "Unknown user.");
+ return HYBBX_OK;
+ }
+ if (rc != HYBBX_OK) {
+ return rc;
+ }
+
+ if (!partner_user.active) {
+ hybbx_session_write_line(initiator, "User is not active.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_user_level_is_guest(partner_user.level)) {
+ hybbx_session_write_line(initiator, "Cannot conference with a guest.");
+ return HYBBX_OK;
+ }
+
+ if (str_ieq(partner_user.username, hybbx_session_username(initiator))) {
+ hybbx_session_write_line(initiator, "Cannot conference with yourself.");
+ return HYBBX_OK;
+ }
+
+ partner_session = conference_find_online_user(service, partner_user.username);
+ if (partner_session == NULL) {
+ hybbx_session_write_line(initiator, "User is not online.");
+ return HYBBX_OK;
+ }
+
+ if (conference_session_active(partner_session)) {
+ hybbx_session_write_line(initiator, "User is already in a conference.");
+ return HYBBX_OK;
+ }
+
+ if (force) {
+ if (conference_activate(service, initiator, partner_session,
+ topic) != HYBBX_OK) {
+ hybbx_session_write_line(initiator,
+ "Conference could not be started.");
+ return HYBBX_OK;
+ }
+ snprintf(line, sizeof(line), "meet status=forced user=%s",
+ hybbx_user_display_name(&partner_user));
+ hybbx_session_write_line(initiator, line);
+ hybbx_monitor_broadcast(service, line);
+ hybbx_session_write_line(partner_session,
+ "You were placed into a conference by Sysop.");
+ return HYBBX_OK;
+ }
+
+ if (hybbx_conference_invite_pending(partner_session)) {
+ hybbx_session_write_line(initiator,
+ "User has a pending conference invite.");
+ return HYBBX_OK;
+ }
+
+ hybbx_session_set_conference_invite(partner_session,
+ hybbx_session_username(initiator),
+ topic);
+ hybbx_session_set_conference_invite_deadline(
+ partner_session, time(NULL) + (time_t)timeout_sec);
+
+ snprintf(line, sizeof(line),
+ "*** Conference invite from %s: %s (reply in %u s)",
+ hybbx_session_display_name(initiator), topic, timeout_sec);
+ hybbx_session_write_line(partner_session, line);
+ hybbx_session_write_line(partner_session, "*** Accept? y/n or yes/no");
+
+ snprintf(line, sizeof(line),
+ "meet status=pending user=%s timeout=%us",
+ hybbx_user_display_name(&partner_user), timeout_sec);
+ hybbx_session_write_line(initiator, line);
+ hybbx_monitor_broadcast(service, line);
+
+ return HYBBX_OK;
+}
+
+typedef struct conference_post_ctx {
+ hybbx_session_t *from;
+ const char *message;
+ const char *from_user;
+ const char *from_login;
+} conference_post_ctx_t;
+
+static void conference_post_visitor(hybbx_session_t *session, void *userdata)
+{
+ conference_post_ctx_t *ctx = (conference_post_ctx_t *)userdata;
+ char line[HYBBX_USER_NAME_MAX + HYBBX_LINE_MAX + 16];
+ const char *session_login;
+
+ if (session == NULL || ctx == NULL || ctx->message == NULL ||
+ ctx->from == NULL || ctx->from_login == NULL) {
+ return;
+ }
+
+ if (!conference_session_active(session)) {
+ return;
+ }
+
+ if (session == ctx->from) {
+ snprintf(line, sizeof(line), "ME: %s", ctx->message);
+ hybbx_session_write_line(session, line);
+ return;
+ }
+
+ session_login = hybbx_session_username(session);
+ if (session_login == NULL ||
+ !str_ieq(hybbx_session_conference_partner(session), ctx->from_login) ||
+ !str_ieq(hybbx_session_conference_partner(ctx->from), session_login)) {
+ return;
+ }
+
+ snprintf(line, sizeof(line), "%s: %s", ctx->from_user, ctx->message);
+ hybbx_session_write_line(session, line);
+}
+
+hybbx_result_t hybbx_conference_post(hybbx_service_t *service,
+ hybbx_session_t *from,
+ const char *message)
+{
+ conference_post_ctx_t ctx;
+ const hybbx_chat_config_t *chat;
+
+ if (service == NULL || from == NULL || message == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (message[0] == '\0') {
+ return HYBBX_OK;
+ }
+
+ if (!conference_session_active(from)) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ chat = hybbx_service_get_chat(service);
+ if (chat == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ if (strlen(message) > chat->message_max) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ ctx.from = from;
+ ctx.message = message;
+ ctx.from_user = hybbx_session_display_name(from);
+ ctx.from_login = hybbx_session_username(from);
+ if (ctx.from_login == NULL || ctx.from_login[0] == '\0') {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_service_visit_sessions(service, conference_post_visitor, &ctx);
+ return HYBBX_OK;
+}
+
+void hybbx_conference_area_leaving(hybbx_session_t *session)
+{
+ const char *partner_name;
+
+ if (session == NULL) {
+ return;
+ }
+
+ partner_name = hybbx_session_conference_partner(session);
+ hybbx_session_clear_conference(session);
+
+ if (partner_name != NULL && partner_name[0] != '\0') {
+ conference_notify_partner_left(session, partner_name);
+ }
+}
+
+void hybbx_conference_session_closed(hybbx_session_t *session)
+{
+ hybbx_service_t *service;
+ const char *username;
+
+ if (session == NULL) {
+ return;
+ }
+
+ if (hybbx_conference_invite_pending(session)) {
+ hybbx_session_clear_conference_invite(session);
+ }
+
+ if (!conference_session_active(session)) {
+ service = hybbx_session_service(session);
+ username = hybbx_session_username(session);
+ if (service != NULL && username != NULL && username[0] != '\0') {
+ conference_cancel_invites_from(service, username);
+ }
+ return;
+ }
+
+ hybbx_conference_area_leaving(session);
+
+ service = hybbx_session_service(session);
+ username = hybbx_session_username(session);
+ if (service != NULL && username != NULL && username[0] != '\0') {
+ conference_cancel_invites_from(service, username);
+ }
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com