From 18e401f663f16c1214717f06e66b8139c11a5fbf Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Sat, 18 Nov 2023 19:50:50 -0500 Subject: [PATCH] mod_asterisk_queues: Add Asterisk queue agent module. This adds several modules related to allowing Asterisk queue agents to interact with queues using a BBS module: * mod_asterisk_ami: Asterisk Manager Interface (AMI) integration using CAMI library callbacks. Other modules can then receive AMI events, such as mod_asterisk_queues. * mod_asterisk_queues: Generic queue management system. Specific queue functionality needs to be implemented in custom user modules that define queue handlers. This module handles all the general queue management functionality so that handlers only need to define queue-specific business logic. * mod_ncurses: Provides graphical ncurses interface that works abstractly within the BBS. Since ncurses is generally not safe to use in multithreaded programs, this interface forks and run ncurses on a node in a separate process and returns the chosen value to the calling BBS function. This also abstracts the complexity of ncurses away to make simple menus easy to create. This commit does include specific queue handlers. As part of this change: * Variables can now be defined per user in variables.conf and can be used by modules to restrict or customize functionality (such as by mod_asterisk_queues) --- README.rst | 2 + bbs/alertpipe.c | 4 +- bbs/auth.c | 17 +- bbs/bbs.c | 2 +- bbs/module.c | 5 + bbs/node.c | 1 + bbs/pty.c | 2 +- bbs/variables.c | 31 +- configs/mod_asterisk_ami.conf | 6 + configs/mod_asterisk_queues.conf | 19 + configs/variables.conf | 8 + include/alertpipe.h | 3 +- include/mod_asterisk_ami.h | 34 + include/mod_asterisk_queues.h | 47 ++ include/mod_ncurses.h | 104 +++ include/node.h | 21 + include/variables.h | 7 + modules/Makefile | 12 + modules/mod_asterisk_ami.c | 266 +++++++ modules/mod_asterisk_queues.c | 1056 ++++++++++++++++++++++++++ modules/mod_ncurses.c | 409 ++++++++++ nets/net_imap/imap_client_parallel.c | 2 +- nets/net_irc.c | 2 +- scripts/install_prereq.sh | 16 +- scripts/libcami.sh | 14 + 25 files changed, 2065 insertions(+), 25 deletions(-) create mode 100644 configs/mod_asterisk_ami.conf create mode 100644 configs/mod_asterisk_queues.conf create mode 100644 include/mod_asterisk_ami.h create mode 100644 include/mod_asterisk_queues.h create mode 100644 include/mod_ncurses.h create mode 100644 modules/mod_asterisk_ami.c create mode 100644 modules/mod_asterisk_queues.c create mode 100644 modules/mod_ncurses.c create mode 100755 scripts/libcami.sh diff --git a/README.rst b/README.rst index e2e65da8..06734fde 100644 --- a/README.rst +++ b/README.rst @@ -64,6 +64,8 @@ Key features and capabilities include: * Internet Relay Chat client and server (including ChanServ), with native IRC, Slack, and Discord relays +* Queue agent position system for Asterisk + * Emulated slow baud rate support * TDD/TTY (telecommunications device for the deaf) support diff --git a/bbs/alertpipe.c b/bbs/alertpipe.c index c780fc48..b8286fd0 100644 --- a/bbs/alertpipe.c +++ b/bbs/alertpipe.c @@ -85,12 +85,12 @@ int bbs_alertpipe_close(int alert_pipe[2]) return 0; } -int bbs_alertpipe_poll(int alert_pipe[2]) +int bbs_alertpipe_poll(int alert_pipe[2], int ms) { int res; for (;;) { struct pollfd pfd = { alert_pipe[0], POLLIN, 0 }; - res = poll(&pfd, 1, -1); + res = poll(&pfd, 1, ms); if (res < 0) { if (errno != EINTR) { bbs_warning("poll returned error: %s\n", strerror(errno)); diff --git a/bbs/auth.c b/bbs/auth.c index d1474cdd..1429a1d4 100644 --- a/bbs/auth.c +++ b/bbs/auth.c @@ -597,6 +597,13 @@ int bbs_user_authenticate(struct bbs_user *user, const char *username, const cha return 0; } +static int post_auth(struct bbs_node *node, struct bbs_user *user) +{ + bbs_auth("Node %d now logged in as %s (via %s)\n", node->id, bbs_username(user), node->protname); + bbs_event_dispatch(node, EVENT_USER_LOGIN); + return 0; +} + int bbs_node_attach_user(struct bbs_node *node, struct bbs_user *user) { bbs_assert_exists(node); @@ -609,7 +616,7 @@ int bbs_node_attach_user(struct bbs_node *node, struct bbs_user *user) return -1; } node->user = user; - bbs_auth("Node %d now logged in as %s (via %s)\n", node->id, bbs_username(user), node->protname); + post_auth(node, user); /* Manually emit event since bbs_authenticate wasn't used */ return 0; } @@ -668,13 +675,7 @@ int bbs_authenticate(struct bbs_node *node, const char *username, const char *pa bbs_warning("Username '%s' contains space (may not be compatible with all services)\n", username); /* e.g. IRC */ } - /* Do not run any callbacks for user login here, since this function isn't always - * called on authentication (SSH for example could call bbs_user_authenticate - * and then bbs_node_attach_user). - * Any such stuff should be done in node.c after user login */ - - bbs_auth("Node %d now logged in as %s\n", node->id, bbs_username(node->user)); - bbs_event_dispatch(node, EVENT_USER_LOGIN); /* XXX If bbs_user_authenticate is called directly, this event isn't emitted */ + post_auth(node, node->user); return 0; } diff --git a/bbs/bbs.c b/bbs/bbs.c index 84d09eec..1bc7499e 100644 --- a/bbs/bbs.c +++ b/bbs/bbs.c @@ -781,7 +781,7 @@ static void *monitor_sig_flags(void *unused) UNUSED(unused); for (;;) { - if (bbs_alertpipe_poll(sig_alert_pipe) <= 0) { + if (bbs_alertpipe_poll(sig_alert_pipe, -1) <= 0) { break; } pthread_mutex_lock(&sig_lock); diff --git a/bbs/module.c b/bbs/module.c index 3a975e3f..2c4160a6 100644 --- a/bbs/module.c +++ b/bbs/module.c @@ -761,6 +761,11 @@ static struct bbs_module *unload_resource_nolock(struct bbs_module *mod, int for if (force > 1) { bbs_warning("Warning: Forcing removal of module '%s' with use count %d\n", mod->name, mod->usecount); } else { + if (RWLIST_EMPTY(&mod->refs)) { + /* The integer count is positive, but our list is empty? + * Critical lack of synchronization! (Probably a bug) */ + bbs_error("Module '%s' supposedly has use count %d, but refcount list is empty?\n", mod->name, mod->usecount); + } bbs_warning("Soft unload failed, '%s' has use count %d\n", mod->name, mod->usecount); return NULL; } diff --git a/bbs/node.c b/bbs/node.c index b20749e3..cff8b1ed 100644 --- a/bbs/node.c +++ b/bbs/node.c @@ -1239,6 +1239,7 @@ static int node_intro(struct bbs_node *node) bbs_node_var_set_fmt(node, "BBS_USERID", "%d", node->user->id); bbs_node_var_set_fmt(node, "BBS_USERPRIV", "%d", node->user->priv); bbs_node_var_set(node, "BBS_USERNAME", bbs_username(node->user)); + bbs_user_init_vars(node); /* Set any custom variables for this user */ /*! \todo Notify user's friends that s/he's logged on now */ /*! \todo Notify the sysop (sysop console), via BELL, that a new user has logged in, if and only if the sysop console is idle */ diff --git a/bbs/pty.c b/bbs/pty.c index b904b483..b98fe694 100644 --- a/bbs/pty.c +++ b/bbs/pty.c @@ -309,7 +309,7 @@ int bbs_node_spy(int fdin, int fdout, unsigned int nodenum) * Except, we only get signals from ^C on the foreground console. */ if (fgconsole) { - if (bbs_alertpipe_poll(spy_alert_pipe) > 0) { + if (bbs_alertpipe_poll(spy_alert_pipe, -1) > 0) { bbs_alertpipe_read(spy_alert_pipe); } /* Restore the original handler for ^C */ diff --git a/bbs/variables.c b/bbs/variables.c index a38e6eed..de98d290 100644 --- a/bbs/variables.c +++ b/bbs/variables.c @@ -26,6 +26,7 @@ #include "include/linkedlists.h" #include "include/variables.h" #include "include/node.h" +#include "include/user.h" #include "include/config.h" #include "include/utils.h" #include "include/cli.h" @@ -120,8 +121,8 @@ static int load_config(void) while ((section = bbs_config_walk(cfg, section))) { if (strcmp(bbs_config_section_name(section), "variables")) { - bbs_warning("Invalid section '%s', ignoring\n", bbs_config_section_name(section)); - /* [variables] is the only valid section */ + /* [variables] contains global variables + * Don't load anything else into memory directly. */ continue; } while ((keyval = bbs_config_section_walk(section, keyval))) { @@ -129,7 +130,7 @@ static int load_config(void) bbs_var_set_user(key, value); } } - bbs_config_free(cfg); + /* Don't free the config, since we'll reference it whenever users log in. */ return 0; } @@ -192,6 +193,30 @@ int bbs_vars_init(void) return load_config() || bbs_cli_register_multiple(cli_commands_variables); } +/*! \note Could use a callback for this (allowing NULL modules in event.c), + * but just make this callable directly from node.c */ +int bbs_user_init_vars(struct bbs_node *node) +{ + struct bbs_config_section *section = NULL; + struct bbs_keyval *keyval = NULL; + struct bbs_config *cfg = bbs_config_load("variables.conf", 1); + + if (!cfg) { + return 0; + } + + while ((section = bbs_config_walk(cfg, section))) { + if (strcasecmp(bbs_config_section_name(section), bbs_username(node->user))) { + continue; + } + while ((keyval = bbs_config_section_walk(section, keyval))) { + const char *key = bbs_keyval_key(keyval), *value = bbs_keyval_val(keyval); + bbs_node_var_set(node, key, value); + } + } + return 0; +} + int bbs_varlist_last_var_append(struct bbs_vars *vars, const char *s) { struct bbs_var *v; diff --git a/configs/mod_asterisk_ami.conf b/configs/mod_asterisk_ami.conf new file mode 100644 index 00000000..73a1a9ff --- /dev/null +++ b/configs/mod_asterisk_ami.conf @@ -0,0 +1,6 @@ +; mod_asterisk_ami.conf + +;[ami] ; Connection information for Asterisk Manager Interface +;hostname=127.0.0.1 +;username=amiuser +;password=amipass diff --git a/configs/mod_asterisk_queues.conf b/configs/mod_asterisk_queues.conf new file mode 100644 index 00000000..57014a06 --- /dev/null +++ b/configs/mod_asterisk_queues.conf @@ -0,0 +1,19 @@ +; mod_asterisk_queues.conf + +[general] +title = OPERATOR SERVICE POSITION SYSTEM +callmenutitle = OPERATOR SERVICE POSITION SYSTEM (CHOOSE CALL) +queueidvar = queueuniq ; Name of Asterisk channel variable on queue calls that will contain a unique, numeric ID for the call + +; Agents must have the variable ASTERISK_AGENT_ID defined. Only these users may use the module. +; You can define these variables statically in variables.conf. + +;[sales] +;title = SALES ; Queue name that will display to the agent. +;handler = sales ; The name of a queue call handler that will handle this call. + ; Currently, these handlers are implemented in custom C handlers that must be written for each module. + ; These are generally proprietary business logic - so you will likely have to implement your own. + +;[engineering] +;title = ENGINEERING +;handler = engineering diff --git a/configs/variables.conf b/configs/variables.conf index 0be3ab92..4861ebcb 100644 --- a/configs/variables.conf +++ b/configs/variables.conf @@ -4,3 +4,11 @@ ; Variable names without lowercase letters should be avoided, since reserved and predefined BBS variables will always be all caps. ; Some variables are defined automatically, run /variables from the sysop console to see available variables. ;foo=bar + +; Per-user variables that are automatically defined when a user logs in. +;[sysop] ; Username for which these variables are defined. +;SUPERADMIN=1 +;abc=def + +;[pat] +;ASTERISK_AGENT_ID=111 diff --git a/include/alertpipe.h b/include/alertpipe.h index fcc3aad0..f6a153ad 100644 --- a/include/alertpipe.h +++ b/include/alertpipe.h @@ -38,6 +38,7 @@ int bbs_alertpipe_close(int alert_pipe[2]); /*! * \brief Wait indefinitely for traffic on an alertpipe + * \param ms Same as poll() * \retval Same as poll() */ -int bbs_alertpipe_poll(int alert_pipe[2]); +int bbs_alertpipe_poll(int alert_pipe[2], int ms); diff --git a/include/mod_asterisk_ami.h b/include/mod_asterisk_ami.h new file mode 100644 index 00000000..2a2543e3 --- /dev/null +++ b/include/mod_asterisk_ami.h @@ -0,0 +1,34 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Asterisk Manager Interface + * + * \author Naveen Albert + */ + +int __bbs_ami_callback_register(int (*callback)(struct ami_event *event, const char *eventname), void *mod); + +/*! + * \brief Register an AMI callback + * \param callback Callback function to execute on AMI events + * \retval 0 on success, -1 on failure + */ +#define bbs_ami_callback_register(callback) __bbs_ami_callback_register(callback, BBS_MODULE_SELF) + +/*! + * \brief Unregister an AMI callback previously registered using bbs_ami_callback_register + * \param callback + * \retval 0 on success, -1 on failure + */ +int bbs_ami_callback_unregister(int (*callback)(struct ami_event *event, const char *eventname)); diff --git a/include/mod_asterisk_queues.h b/include/mod_asterisk_queues.h new file mode 100644 index 00000000..263b4cc4 --- /dev/null +++ b/include/mod_asterisk_queues.h @@ -0,0 +1,47 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Asterisk Queue Position System + * + * \author Naveen Albert + */ + +struct queue_call_handle { + /* Agent info */ + struct bbs_node *node; /*!< Node of agent handling call */ + int agentid; /*!< ID of agent handling call */ + /* Call info */ + int id; /*!< Queue call ID */ + const char *channel; /*!< Channel name */ + int ani2; /*!< ANI II */ + unsigned long ani; /*!< ANI */ + unsigned long dnis; /*!< DNIS */ + const char *cnam; /*!< CNAM */ +}; + +int __bbs_queue_call_handler_register(const char *name, int (*handler)(struct queue_call_handle *qch), void *mod); + +/*! + * \brief Register a queue call handler + * \param name Name of handler to register + * \retval 0 on success, -1 on failure + */ +#define bbs_queue_call_handler_register(name, handler) __bbs_queue_call_handler_register(name, handler, BBS_MODULE_SELF) + +/*! + * \brief Unregister a queue call handler + * \param name Name of registered handler + * \retval 0 on success, -1 on failure + */ +int bbs_queue_call_handler_unregister(const char *name); diff --git a/include/mod_ncurses.h b/include/mod_ncurses.h new file mode 100644 index 00000000..16413dc2 --- /dev/null +++ b/include/mod_ncurses.h @@ -0,0 +1,104 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Graphical text menus + * + * \author Naveen Albert + */ + +#define MAX_NCURSES_MENU_OPTIONS 64 + +struct bbs_ncurses_menu { + const char *title; + const char *subtitle; + const char *keybindings; + char keybind[MAX_NCURSES_MENU_OPTIONS]; + char *options[MAX_NCURSES_MENU_OPTIONS]; + char *optvals[MAX_NCURSES_MENU_OPTIONS]; + int num_options; +}; + +/*! + * \brief Initialize a menu. You must call this before using it in any way. + */ +void bbs_ncurses_menu_init(struct bbs_ncurses_menu *menu); + +/*! \brief Destroy a menu when done with it */ +void bbs_ncurses_menu_destroy(struct bbs_ncurses_menu *menu); + +/*! + * \brief Set the title of a menu + * \param menu + * \param title Menu title. Must remain valid throughout the lifetime of menu. + */ +void bbs_ncurses_menu_set_title(struct bbs_ncurses_menu *menu, const char *title); + +/*! + * \brief Set the subtitle of a menu + * \param menu + * \param subtitle Menu subtitle. Must remain valid throughout the lifetime of menu. + */ +void bbs_ncurses_menu_set_subtitle(struct bbs_ncurses_menu *menu, const char *subtitle); + +/*! + * \brief Disable custom key bindings. A default 'q' option will still be added to quit. + * \param menu + */ +void bbs_ncurses_menu_disable_keybindings(struct bbs_ncurses_menu *menu); + +/*! + * \brief Add an option to a menu + * \param menu + * \param key Key binding. Specify 0 for no key binding. + * \param opt Will be duplicated. + * \param value Will be duplicated. + * \retval -1 on failure, 0 on success + */ +int bbs_ncurses_menu_addopt(struct bbs_ncurses_menu *menu, char key, const char *opt, const char *value) __nonnull ((1, 3)); + +/*! + * \brief Get the option value of a menu item at a particular index + * \param menu + * \param index + * \return NULL on failure or invalid index + * \return Option value + */ +const char *bbs_ncurses_menu_getopt_name(struct bbs_ncurses_menu *menu, int index); + +/*! + * \brief Get the keybinding of a menu item at a particular index + * \param menu + * \param index + * \return 0 on failure or invalid index + * \return Key binding + */ +char bbs_ncurses_menu_getopt_key(struct bbs_ncurses_menu *menu, int index); + +/*! + * \brief Get an option using a menu (pass to bbs_ncurses_menu_getopt_name for the text value) + * \param node + * \param menu + * \retval -1 on failure, option index otherwise + */ +int bbs_ncurses_menu_getopt(struct bbs_node *node, struct bbs_ncurses_menu *menu); + +/*! + * \brief Run a menu and return the keybinding for the chosen option. + * This is a convenience wrapper that calls bbs_ncurses_menu_getopt + * and then calls bbs_ncurses_menu_getopt_key on the result, if there is one. + * \param node + * \param menu + * \return Same as bbs_ncurses_menu_getopt_key + */ +char bbs_ncurses_menu_getopt_selection(struct bbs_node *node, struct bbs_ncurses_menu *menu); diff --git a/include/node.h b/include/node.h index 03074bdc..5ee347ba 100644 --- a/include/node.h +++ b/include/node.h @@ -578,6 +578,27 @@ ssize_t bbs_node_any_fd_write(struct bbs_node *node, int fd, const char *buf, si */ ssize_t __attribute__ ((format (gnu_printf, 3, 4))) bbs_node_any_fd_writef(struct bbs_node *node, int fd, const char *fmt, ...) __attribute__ ((nonnull (1))); +/*! + * \brief Write data to an arbitrary node + * \param node + * \param buf Data to write + * \param len Length of bfu + * \retval Same as write() + * \note Unlike bbs_write, this does not guarantee the buffer will be fully written before returning. + * Applications SHOULD use bbs_node_write instead of this function when writing from the node thread. + */ +#define bbs_node_any_write(node, fmt, ...) bbs_node_any_fd_writef(node, node->slavefd, fmt, ## __VA_ARGS__) + +/*! + * \brief Write formatted data to an arbitrary node + * \param node + * \param fmt printf-style format string + * \retval Same as write() + * \note Unlike bbs_write, this does not guarantee the buffer will be fully written before returning. + * Applications SHOULD use bbs_node_writef instead of this function when writing from the node thread. + */ +#define bbs_node_any_writef(node, fmt, ...) bbs_node_any_fd_writef(node, node->slavefd, fmt, ## __VA_ARGS__) + /*! * \brief printf-style wrapper for bbs_node_write. * \param node diff --git a/include/variables.h b/include/variables.h index 034eb4fb..7e7b5abc 100644 --- a/include/variables.h +++ b/include/variables.h @@ -51,6 +51,13 @@ void bbs_vars_cleanup(void); /*! \brief Load variables on startup from variables.conf */ int bbs_vars_init(void); +/*! + * \brief Initialize custom user-specific node variables upon user login + * \param node Authenticated node + * \retval 0 on success, -1 on failure + */ +int bbs_user_init_vars(struct bbs_node *node); + /*! * \brief Append to the variable added most recently to a list * \param vars diff --git a/modules/Makefile b/modules/Makefile index ae80c9fe..8bb1efd3 100644 --- a/modules/Makefile +++ b/modules/Makefile @@ -33,6 +33,14 @@ mod_mimeparse.o : mod_mimeparse.c mod_mimeparse.d @echo "== Linking $@" $(CC) -shared -fPIC -o $(basename $^).so $^ +mod_asterisk_ami.so : mod_asterisk_ami.o + @echo "== Linking $@" + $(CC) -shared -fPIC -o $(basename $^).so $^ -lcami + +mod_asterisk_queues.so : mod_asterisk_queues.o + @echo "== Linking $@" + $(CC) -shared -fPIC -o $(basename $^).so $^ -lcami + mod_discord.so : mod_discord.o @echo "== Linking $@" $(CC) -shared -fPIC -o $(basename $^).so $^ -ldiscord @@ -57,6 +65,10 @@ mod_mysql.so : mod_mysql.o @echo "== Linking $@" $(CC) -shared -fPIC -o $(basename $^).so $^ $(MYSQL_LIBS) +mod_ncurses.so : mod_ncurses.o + @echo "== Linking $@" + $(CC) -shared -fPIC -o $(basename $^).so $^ -lmenu -lncurses + mod_oauth.so : mod_oauth.o @echo "== Linking $@" $(CC) -shared -fPIC -o $(basename $^).so $^ -ljansson diff --git a/modules/mod_asterisk_ami.c b/modules/mod_asterisk_ami.c new file mode 100644 index 00000000..3e2797c4 --- /dev/null +++ b/modules/mod_asterisk_ami.c @@ -0,0 +1,266 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Asterisk Manager Interface + * + * \author Naveen Albert + */ + +#include "include/bbs.h" + +#include +#include +#include +#include + +#include + +#include "include/module.h" +#include "include/config.h" +#include "include/alertpipe.h" +#include "include/linkedlists.h" +#include "include/cli.h" + +#include "include/mod_asterisk_ami.h" + +static int asterisk_up = 0; + +struct ami_callback { + int (*callback)(struct ami_event *event, const char *eventname); + void *mod; + RWLIST_ENTRY(ami_callback) entry; +}; + +static RWLIST_HEAD_STATIC(callbacks, ami_callback); + +static void set_ami_status(int up) +{ + asterisk_up = up; + bbs_debug(3, "Asterisk Manager Interface is now %s\n", up ? "UP" : "DOWN"); +} + +/*! \brief Callback function executing asynchronously when new events are available */ +static void ami_callback(struct ami_event *event) +{ + struct ami_callback *cb; + const char *eventname; + + eventname = ami_keyvalue(event, "Event"); + bbs_assert_exists(eventname); + + if (unlikely(!strcmp(eventname, "FullyBooted"))) { + set_ami_status(1); /* We get this when Asterisk starts, but also when we connect, so if Asterisk is already running, we're still good. */ + goto cleanup; /* No need to forward this event to listeners. */ + } + + RWLIST_RDLOCK(&callbacks); + RWLIST_TRAVERSE(&callbacks, cb, entry) { + int res; + /* Dispatch AMI event to each subscribed callback function */ + bbs_module_ref(cb->mod, 1); + res = cb->callback(event, eventname); + bbs_module_unref(cb->mod, 1); + /* If callback returns 0, that means it handled it non-exclusively. + * If callback returns -1, that means it's not handling it. + * If callback returns 1, abort callback handling. */ + if (res == 1) { + break; + } + } + RWLIST_UNLOCK(&callbacks); + +cleanup: + ami_event_free(event); /* Free event when done with it */ +} + +int __bbs_ami_callback_register(int (*callback)(struct ami_event *event, const char *eventname), void *mod) +{ + struct ami_callback *cb; + + cb = calloc(1, sizeof(*cb)); + if (ALLOC_FAILURE(cb)) { + return -1; + } + + cb->callback = callback; + cb->mod = mod; + + RWLIST_WRLOCK(&callbacks); + RWLIST_INSERT_HEAD(&callbacks, cb, entry); + RWLIST_UNLOCK(&callbacks); + + return 0; +} + +int bbs_ami_callback_unregister(int (*callback)(struct ami_event *event, const char *eventname)) +{ + struct ami_callback *cb; + + RWLIST_WRLOCK(&callbacks); + cb = RWLIST_REMOVE_BY_FIELD(&callbacks, callback, callback, entry); + RWLIST_UNLOCK(&callbacks); + + if (!cb) { + bbs_error("Tried to unregister unregistered callback %p\n", callback); + return -1; + } else { + free(cb); + } + return 0; +} + +static int ami_log_fd = -1; + +static int cli_ami_loglevel(struct bbs_cli_args *a) +{ + int newlevel = atoi(a->argv[2]); + if (newlevel < 0 || newlevel > 10) { + bbs_dprintf(a->fdout, "Invalid log level: %d\n", newlevel); + return -1; + } + ami_set_debug_level(newlevel); + return 0; +} + +static int cli_ami_status(struct bbs_cli_args *a) +{ + bbs_dprintf(a->fdout, "Asterisk Manager Interface status: %s\n", asterisk_up ? "UP" : "DOWN"); + return 0; +} + +static struct bbs_cli_entry cli_commands_ami[] = { + BBS_CLI_COMMAND(cli_ami_loglevel, "ami loglevel", 3, "Set CAMI (AMI library) log level", "ami loglevel "), + BBS_CLI_COMMAND(cli_ami_status, "ami status", 2, "View Asterisk Manager Interface connection status", NULL), +}; + +static int load_config(int open_logfile); + +static int ami_alert_pipe[2] = { -1, -1 }; +static int unloading = 0; + +static void ami_disconnect_callback(void) +{ + int sleep_ms = 500; + + set_ami_status(0); + bbs_error("Asterisk Manager Interface connection lost\n"); + + if (unloading) { + return; /* If we're unloading, don't care */ + } + + RWLIST_RDLOCK(&callbacks); + /* Perhaps Asterisk restarted (or crashed). + * Try to reconnect if it comes back up. */ + for (;;) { + int res = load_config(0); + if (!res) { + bbs_verb(4, "Asterisk Manager Interface connection re-established\n"); + set_ami_status(1); + break; + } + if (bbs_alertpipe_poll(ami_alert_pipe, sleep_ms)) { + bbs_alertpipe_read(ami_alert_pipe); + bbs_debug(3, "AMI reconnect interrupted\n"); + break; + } + if (sleep_ms < 64000) { /* Exponential backoff, up to 64 seconds */ + sleep_ms *= 2; + } + } + RWLIST_UNLOCK(&callbacks); +} + +static int load_config(int open_logfile) +{ + int res = 0; + struct bbs_config *cfg; + char hostname[256]; + char username[64]; + char password[92]; + char logfile[512]; + + cfg = bbs_config_load("mod_asterisk_ami.conf", 1); + if (!cfg) { + return -1; + } + + res |= bbs_config_val_set_str(cfg, "ami", "hostname", hostname, sizeof(hostname)); + res |= bbs_config_val_set_str(cfg, "ami", "username", username, sizeof(username)); + res |= bbs_config_val_set_str(cfg, "ami", "password", password, sizeof(password)); + + if (open_logfile && !bbs_config_val_set_str(cfg, "logging", "logfile", logfile, sizeof(logfile))) { + ami_log_fd = open(logfile, O_CREAT | O_APPEND); + if (ami_log_fd != -1) { + unsigned int loglevel; + bbs_config_val_set_uint(cfg, "logging", "loglevel", &loglevel); + if (loglevel > 10) { + bbs_warning("Maximum AMI debug level is 10\n"); + } + ami_set_debug_level((int) loglevel); + } else { + bbs_error("Failed to open %s for AMI logging: %s\n", logfile, strerror(errno)); + } + } + + if (!res) { + if (ami_connect(hostname, 0, ami_callback, ami_disconnect_callback)) { + bbs_error("AMI connection failed to %s\n", hostname); + res = -1; + } else if (ami_action_login(username, password)) { + bbs_error("AMI login failed for user %s@%s\n", username, hostname); + res = -1; + } + } + + /* Fully purge the password from memory */ + bbs_memzero(password, strlen(password)); + bbs_config_free(cfg); + return res; +} + +static int load_module(void) +{ + if (bbs_alertpipe_create(ami_alert_pipe)) { + return -1; + } + if (load_config(1)) { + close_if(ami_log_fd); + bbs_alertpipe_close(ami_alert_pipe); + return -1; + } + bbs_cli_register_multiple(cli_commands_ami); + return 0; +} + +static int unload_module(void) +{ + /* If ami_disconnect_callback is currently being executed by some thread, + * get rid of it. */ + unloading = 1; + bbs_alertpipe_write(ami_alert_pipe); /* Wake up anything waiting in ami_disconnect_callback */ + + /* If anything was in the body of ami_disconnect_callback, + * it had the list locked. If we can lock the list, that means they're gone. */ + RWLIST_WRLOCK(&callbacks); + RWLIST_UNLOCK(&callbacks); + + bbs_cli_unregister_multiple(cli_commands_ami); + ami_disconnect(); + close_if(ami_log_fd); + bbs_alertpipe_close(ami_alert_pipe); + return 0; +} + +BBS_MODULE_INFO_FLAGS("Asterisk Manager Interface", MODFLAG_GLOBAL_SYMBOLS); diff --git a/modules/mod_asterisk_queues.c b/modules/mod_asterisk_queues.c new file mode 100644 index 00000000..b4a14c98 --- /dev/null +++ b/modules/mod_asterisk_queues.c @@ -0,0 +1,1056 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Asterisk Queue Position System + * + * \author Naveen Albert + */ + +#include "include/bbs.h" + +#include +#include + +#include + +#include "include/module.h" +#include "include/config.h" +#include "include/linkedlists.h" +#include "include/node.h" +#include "include/term.h" +#include "include/user.h" +#include "include/door.h" +#include "include/variables.h" +#include "include/cli.h" + +#include "include/mod_asterisk_ami.h" +#include "include/mod_asterisk_queues.h" +#include "include/mod_ncurses.h" + +struct queue; + +struct queue_call { + int id; /*!< Queue call ID */ + const char *channel; /*!< Channel name */ + struct queue *queue; /*!< Parent queue to which this call belongs */ + int ani2; /*!< ANI II */ + unsigned long ani; /*!< ANI */ + unsigned long dnis; /*!< DNIS, which unlike ConnectedLine, doesn't come for free (need to use Getvar) */ + const char *cnam; /*!< CNAM */ + time_t added; /*!< Time added to queue */ + int refcount; /*!< Reference Count */ + unsigned int dead:1; /*!< Dead? */ + RWLIST_ENTRY(queue_call) entry; + char data[]; +}; + +static RWLIST_HEAD_STATIC(calls, queue_call); + +struct agent { + int id; /*!< Agent ID */ + struct bbs_node *node; /*!< Agent's node */ + unsigned int idle:1; /*!< Currently idle? */ + unsigned int gotwritten:1; /*!< Another thread wrote onto our terminal while we were idle */ + RWLIST_ENTRY(agent) entry; +}; + +static RWLIST_HEAD_STATIC(agents, agent); + +/*! \brief Information about an agent for a specific queue */ +struct member { + struct queue *queue; + struct agent *agent; + int calls_taken; + RWLIST_ENTRY(member) entry; +}; + +RWLIST_HEAD(members, member); + +struct queue { + const char *name; + const char *title; + const char *handler; + /* By storing membership by queue, rather than agent, + * we make queue operations linear time but + * agent operations can by polynomial in the worst case. + * We choose this tradeoff since agent operations are limited in number, + * but queue operations can be more common. + * They could both be linear if we kept a list per agent of queues of which + * that agent is a member, but the linked list API we use does not make + * that feasible. */ + struct members members; /*!< Queue members */ + int ringing; /*!< # of calls currently ringing in the system */ + int calls; /*!< Total # calls */ + int completed; /*!< # completed calls */ + int abandoned; /*!< # abandoned calls */ + RWLIST_ENTRY(queue) entry; + char data[]; +}; + +static RWLIST_HEAD_STATIC(queues, queue); + +struct queue_call_handler { + const char *name; + int (*handler)(struct queue_call_handle *qch); + void *mod; + RWLIST_ENTRY(queue_call_handler) entry; + char data[]; +}; + +static RWLIST_HEAD_STATIC(handlers, queue_call_handler); + +int __bbs_queue_call_handler_register(const char *name, int (*handler)(struct queue_call_handle *qch), void *mod) +{ + struct queue_call_handler *qch; + + RWLIST_WRLOCK(&handlers); + RWLIST_TRAVERSE(&handlers, qch, entry) { + if (!strcmp(qch->name, name)) { + RWLIST_UNLOCK(&handlers); + bbs_error("Queue call handler with name '%s' already registered\n", name); + return -1; + } + }; + + qch = calloc(1, sizeof(*qch) + strlen(name) + 1); + if (ALLOC_FAILURE(qch)) { + RWLIST_UNLOCK(&handlers); + return -1; + } + + strcpy(qch->data, name); /* Safe */ + qch->name = qch->data; + qch->mod = mod; + qch->handler = handler; + RWLIST_INSERT_HEAD(&handlers, qch, entry); + RWLIST_UNLOCK(&handlers); + return 0; +} + +int bbs_queue_call_handler_unregister(const char *name) +{ + struct queue_call_handler *qch; + + RWLIST_WRLOCK(&handlers); + qch = RWLIST_REMOVE_BY_FIELD(&handlers, name, name, entry); + RWLIST_UNLOCK(&handlers); + + if (!qch) { + bbs_error("Queue call handler '%s' was not registered\n", name); + return -1; + } + + free(qch); + return 0; +} + +//static pthread_mutex_t queue_lock = PTHREAD_MUTEX_INITIALIZER; + +static char system_title[42]; +static char call_menu_title[48]; +static char queue_id_var[64]; + +static struct agent *new_agent(struct bbs_node *node, int agentid) +{ + struct agent *agent; + + agent = calloc(1, sizeof(*agent)); + if (ALLOC_FAILURE(agent)) { + return NULL; + } + agent->idle = 0; + agent->node = node; + agent->id = agentid; + + RWLIST_WRLOCK(&agents); + RWLIST_INSERT_HEAD(&agents, agent, entry); + RWLIST_UNLOCK(&agents); + return agent; +} + +static void del_agent(struct agent *agent) +{ + struct queue *queue; + struct member *member; + RWLIST_RDLOCK(&queues); + RWLIST_TRAVERSE(&queues, queue, entry) { + RWLIST_WRLOCK(&queue->members); + RWLIST_TRAVERSE_SAFE_BEGIN(&queue->members, member, entry) { + if (member->agent == agent) { + RWLIST_REMOVE_CURRENT(entry); + free(member); + break; + } + } + RWLIST_TRAVERSE_SAFE_END; + RWLIST_UNLOCK(&queue->members); + } + RWLIST_UNLOCK(&queues); + /* Finally, remove the agent itself, now that there are no longer any references to it, + * besides ours. */ + free(agent); +} + +/*! \brief queues must be locked when calling */ +/*! \note This returns member unlocked, but this is fine if the calling thread "owns" the member. + * If not, then do not use this function. */ +static struct member *queue_member(struct queue *queue, struct agent *agent) +{ + struct member *member; + RWLIST_RDLOCK(&queue->members); + RWLIST_TRAVERSE(&queue->members, member, entry) { + if (member->agent == agent) { + RWLIST_UNLOCK(&queue->members); + return member; + } + } + RWLIST_UNLOCK(&queue->members); + return NULL; +} + +/*! \brief Must be called with queues locked */ +static struct queue *find_queue(const char *name) +{ + struct queue *queue; + RWLIST_TRAVERSE(&queues, queue, entry) { + if (!strcmp(queue->name, name)) { + return queue; + } + } + return NULL; +} + +static int queues_init(void) +{ + int i; + /* Initially, get all stats for all queues. */ + struct ami_response *resp = ami_action("QueueStatus", ""); + if (!resp || !resp->success) { + bbs_error("Failed to get queue stats\n"); + return -1; + } + RWLIST_RDLOCK(&queues); + for (i = 1; i < resp->size - 1; i++) { + struct ami_event *e = resp->events[i]; + const char *event = ami_keyvalue(e, "Event"); + if (!strcmp(event, "QueueParams")) { + const char *numcalls, *completed, *abandoned; + const char *queue_name = ami_keyvalue(e, "Queue"); + struct queue *queue = find_queue(queue_name); + if (!queue) { + bbs_debug(5, "Skipping irrelevant queue '%s'\n", queue_name); + continue; /* Not one of our queues that we care about */ + } + numcalls = ami_keyvalue(e, "Calls"); + completed = ami_keyvalue(e, "Completed"); + abandoned = ami_keyvalue(e, "Abandoned"); + if (strlen_zero(numcalls) || strlen_zero(completed) || strlen_zero(abandoned)) { + bbs_error("Empty mandatory fields?\n"); + continue; + } + /* Store stats for the queue, semipermanently, so we can reference them until the next update. */ + queue->calls = atoi(numcalls); + queue->completed = atoi(completed); + queue->abandoned = atoi(abandoned); + bbs_verb(5, "Added queue %s\n", queue->name); + } + } + RWLIST_UNLOCK(&queues); + ami_resp_free(resp); /* Free response when done with it */ + bbs_verb(4, "All queues are initialized\n"); + return 0; +} + +static int cli_asterisk_queues(struct bbs_cli_args *a) +{ + struct queue *queue; + bbs_dprintf(a->fdout, "%-30s %-15s %5s %9s %9s\n", "Name", "Handler", "Calls", "Completed", "Abandoned"); + RWLIST_RDLOCK(&queues); + RWLIST_TRAVERSE(&queues, queue, entry) { + struct queue_call_handler *qch; + bbs_dprintf(a->fdout, "%-30s %-15s %5d %9d %9d\n", queue->name, queue->handler, queue->calls, queue->completed, queue->abandoned); + /* Check that a handler actually exists. + * We can't do this when the module loads, + * because the handler modules are dependent on us, + * so they can't even load until we finish loading. + * However, during runtime, those modules should be loaded, + * and we should be able to go ahead and check. */ + RWLIST_RDLOCK(&handlers); + RWLIST_TRAVERSE(&handlers, qch, entry) { + if (!strcmp(qch->name, queue->handler)) { + break; + } + } + RWLIST_UNLOCK(&handlers); + if (!qch) { + bbs_warning("No queue call handler named '%s' appears to be registered currently\n", queue->handler); + } + } + RWLIST_UNLOCK(&queues); + return 0; +} + +static int cli_asterisk_agents(struct bbs_cli_args *a) +{ + struct agent *agent; + + bbs_dprintf(a->fdout, "%4s %8s\n", "Node", "Agent ID"); + RWLIST_RDLOCK(&agents); + RWLIST_TRAVERSE(&agents, agent, entry) { + bbs_dprintf(a->fdout, "%4d %8d\n", agent->node->id, agent->id); + } + RWLIST_UNLOCK(&agents); + return 0; +} + +static void mark_dead(const char *channel) +{ + struct queue_call *call; + + RWLIST_RDLOCK(&calls); + RWLIST_TRAVERSE(&calls, call, entry) { + if (call->dead) { + continue; + } + if (!strcmp(call->channel, channel)) { + bbs_debug(3, "Marking channel as now dead: %s\n", channel); + call->dead = 1; + break; + } + } + RWLIST_UNLOCK(&calls); +} + +static void prune_dead_calls(int querydead) +{ + struct queue_call *call; + + if (querydead) { + /* Only hold a RDLOCK while we're making AMI calls. */ + RWLIST_RDLOCK(&calls); + RWLIST_TRAVERSE(&calls, call, entry) { + char *val; + if (call->dead) { + continue; + } + val = ami_action_getvar("queueuniq", call->channel); + if (!val) { + call->dead = 1; + bbs_debug(3, "Queue call %d is now dead\n", call->id); + continue; + } + if (strlen_zero(val)) { + call->dead = 1; + bbs_debug(3, "Queue call %d is now dead\n", call->id); + free(val); + continue; + } + free(val); + } + RWLIST_UNLOCK(&calls); + } + + /* Now purge any dead calls */ + RWLIST_WRLOCK(&calls); + RWLIST_TRAVERSE_SAFE_BEGIN(&calls, call, entry) { + /* If a call is dead but has a positive refcount, + * an agent is handling it, so don't remove it yet. */ + if (call->dead && !call->refcount) { + RWLIST_REMOVE_CURRENT(entry); + bbs_debug(3, "Pruning dead queue call %d\n", call->id); + free(call); + } + } + RWLIST_TRAVERSE_SAFE_END; + RWLIST_UNLOCK(&calls); +} + +static int cli_asterisk_calls(struct bbs_cli_args *a) +{ + struct queue_call *call; + + prune_dead_calls(1); + + bbs_dprintf(a->fdout, "%4s %-25s %4s %4s %2s %15s %s\n", "ID", "Queue", "Dead", "Refs", "II", "ANI", "CNAM"); + RWLIST_RDLOCK(&calls); + RWLIST_TRAVERSE(&calls, call, entry) { + bbs_dprintf(a->fdout, "%4d %-25s %4s %4d %02d %15lu %s\n", call->id, call->queue->name, BBS_YN(call->dead), call->refcount, call->ani2, call->ani, call->cnam); + } + RWLIST_UNLOCK(&calls); + return 0; +} + +/*! \note 4 and 6 should also be nonnull, but SET_FSM_STRING_VAR does a strlen_zero check, so we can't include them in the attribute */ +static __nonnull ((1, 5)) struct queue_call *new_call(struct queue *queue, int queueid, int ani2, const char *channel, const char *ani, const char *cnam, const char *dnis) +{ + struct queue_call *call; + char *data; + size_t chanlen, cnamlen; + + chanlen = strlen(channel) + 1; + cnamlen = strlen(cnam) + 1; + + call = calloc(1, sizeof(*call) + chanlen + cnamlen); + if (ALLOC_FAILURE(call)) { + return NULL; + } + + while (*ani && !isalnum(*ani)) { + ani++; + } + + call->added = time(NULL); + call->id = queueid; + call->queue = queue; + call->ani2 = ani2; + call->ani = (unsigned long) atol(ani); + if (!strlen_zero(dnis)) { + call->dnis = (unsigned long) atol(dnis); + } + data = call->data; + SET_FSM_STRING_VAR(call, data, channel, channel, chanlen); + SET_FSM_STRING_VAR(call, data, cnam, cnam, cnamlen); + + RWLIST_WRLOCK(&calls); + RWLIST_INSERT_TAIL(&calls, call, entry); + RWLIST_UNLOCK(&calls); + + bbs_debug(4, "Added call from '%s' to queue '%s' as call %d\n", S_IF(ani), queue->name, queueid); + return call; +} + +static void __attribute__ ((format (gnu_printf, 3, 4))) agent_printf(struct queue *queue, const char *member_name, const char *fmt, ...) +{ + char *buf; + int len; + va_list ap; + struct agent *agent; + int agent_id = !strlen_zero(member_name) ? atoi(member_name) : -1; + + va_start(ap, fmt); + len = vasprintf(&buf, fmt, ap); + va_end(ap); + + if (len < 0) { + return; + } + + RWLIST_RDLOCK(&agents); + RWLIST_TRAVERSE(&agents, agent, entry) { + /* The chance that it's not idle because it's updating the time is unlikely: + * literally just 1 in 1000 */ + if (!agent->idle) { + continue; /* Terminal is busy, don't interfere */ + } + if (agent_id == -1) { + /* Applies to all relevant members */ + if (!queue_member(queue, agent)) { + continue; /* Not a member of this queue, doesn't pertain */ + } + } else { + if (agent_id != agent->id) { + continue; /* Not the right agent */ + } + } + agent->gotwritten = 1; /* Let the poor TTY know we just did something to it, so it can plan accordingly the next time it writes */ + bbs_node_any_write(agent->node, buf, len); + } + RWLIST_UNLOCK(&agents); + free(buf); +} + +static int ami_callback(struct ami_event *e, const char *eventname) +{ + const char *queue_name; + struct queue *queue; + + if (strncmp(eventname, "Queue", STRLEN("Queue")) && strncmp(eventname, "Agent", STRLEN("Agent"))) { + return -1; /* If it doesn't start with Queue or Agent, not relevant to us. */ + } + + queue_name = ami_keyvalue(e, "Queue"); + if (strlen_zero(queue_name)) { + return -1; + } + + /* Is it one of our queues that we care about? */ + queue = find_queue(queue_name); + if (!queue) { + return -1; /* Nope, it's not. */ + } + + /* Events that print to agents' terminals use \r instead of \n + * to overwrite the current timestamp */ + + if (!strcmp(eventname, "QueueCallerJoin")) { + char *queueid, *ani2, *dnis; + const char *callerid, *channel, *callername; + callerid = ami_keyvalue(e, "CallerIDNum"); + channel = ami_keyvalue(e, "Channel"); + callername = ami_keyvalue(e, "CallerIDName"); + if (strlen_zero(callerid) || strlen_zero(channel) || strlen_zero(callername)) { + bbs_error("Missing mandatory fields\n"); + return -1; + } + queueid = ami_action_getvar(queue_id_var, channel); + if (strlen_zero(queueid)) { + return -1; + } + ani2 = ami_action_getvar("CALLERID(ani2)", channel); + dnis = ami_action_getvar("CALLERID(DNID)", channel); + new_call(queue, atoi(queueid), atoi(S_IF(ani2)), channel, callerid, callername, dnis); + free_if(queueid); + free_if(ani2); + free_if(dnis); + } else if (!strcmp(eventname, "QueueMemberStatus") || !strcmp(eventname, "AgentComplete")) { + return -1; /* Don't care */ + } else if (!strcmp(eventname, "AgentCalled")) { + const char *callerid = ami_keyvalue(e, "CallerIDNum"); + const char *member_name = ami_keyvalue(e, "MemberName"); + if (strlen_zero(member_name)) { + return -1; + } + agent_printf(queue, member_name, "%s\r%s%-15s %-20s %15s\n", COLOR_RESET, TERM_BELL, "ACD RING", queue->title, S_OR(callerid, "")); + } else if (!strcmp(eventname, "QueueCallerAbandon")) { + const char *originalpos, *pos, *holdtime, *channel, *callerid; + originalpos = ami_keyvalue(e, "OriginalPosition"); + pos = ami_keyvalue(e, "Position"); + holdtime = ami_keyvalue(e, "HoldTime"); + channel = ami_keyvalue(e, "Channel"); + callerid = ami_keyvalue(e, "CallerIDNum"); + /* This is the actual "caller hung up before agent answered" event */ + agent_printf(queue, NULL, "%s\r%s%-15s %-20s %15s %s>%s [%s]\n", COLOR_RESET, TERM_BELL, "ACD DC", queue->title, S_OR(callerid, ""), S_OR(originalpos, ""), S_OR(pos, ""), S_OR(holdtime, "")); + mark_dead(channel); /* Probably safe to unregister directly if we wanted to, but just mark as dead for now */ + } else if (!strcmp(eventname, "QueueCallerLeave")) { + const char *count, *pos, *callerid; + pos = ami_keyvalue(e, "Position"); + count = ami_keyvalue(e, "Count"); + callerid = ami_keyvalue(e, "CallerIDNum"); + /* This happens when an agent answers a call as well as when a caller hangs up. */ + agent_printf(queue, NULL, "%s\r%-15s %-20s %15s P%s %s@\n", COLOR_RESET, "ACD DD", queue->title, S_OR(callerid, ""), S_OR(pos, ""), S_OR(count, "")); +#if 0 + /* Actually, because this happens when an agent answers a call as well, we can't assume the call is dead here */ + /* Don't unregister the call now, since an agent might be handling it. + * However, we can go ahead and mark it as dead, to save an AMI request later. */ + mark_dead(channel); +#endif + } else if (!strcmp(eventname, "AgentConnect")) { + const char *holdtime, *ringtime, *member_name, *callerid; + member_name = ami_keyvalue(e, "MemberName"); + holdtime = ami_keyvalue(e, "HoldTime"); + ringtime = ami_keyvalue(e, "RingTime"); + callerid = ami_keyvalue(e, "CallerIDNum"); + agent_printf(queue, member_name, "%s\r%-15s %-20s %15s [%s/%s]\n", COLOR_RESET, "ACD ANS", queue->title, S_OR(callerid, ""), S_OR(holdtime, ""), S_OR(ringtime, "")); + } else { + bbs_debug(6, "Ignoring queue event: %s\n", eventname); /* We know it's queue related since it contains a Queue key */ + return -1; + } + + return 0; +} + +/* XXX CallsTaken is only set when door execution begins, and isn't updated afterwards during execution */ +static int membership_init(struct agent *agent) +{ + int i; + struct ami_response *resp = ami_action("QueueStatus", "Member:%d", agent->id); + + /* We still need to initialize the agent-specific stats for all queues. + * This response will be smaller (maybe much smaller) than asking for everything. */ + if (!resp || !resp->success) { + bbs_error("Failed to get queue status for agent %d\n", agent->id); + return -1; + } + + /* Loop through each of the queue events and populate our structures if they apply. */ + RWLIST_RDLOCK(&queues); + for (i = 1; i < resp->size - 1; i++) { + struct queue *queue; + struct member *member; + const char *event, *queue_name, *name, *calls_taken; + struct ami_event *e = resp->events[i]; + /* Slightly faster to loop through each field in the response. + * Faster than requesting each of the event fields individually (O(n) as opposed to O(n^2)), but, meh... */ + event = ami_keyvalue(e, "Event"); + if (strcmp(event, "QueueMember")) { + continue; /* Skip QueueParams event */ + } + queue_name = ami_keyvalue(e, "Queue"); + name = ami_keyvalue(e, "Name"); + calls_taken = ami_keyvalue(e, "CallsTaken"); + /* It's a QueueMember event */ + if (strlen_zero(event)) { + bbs_error("Missing event name?\n"); /* Shouldn't ever happen */ + continue; + } + if (strlen_zero(queue_name)) { + bbs_error("Missing queue name?\n"); + continue; + } + if (strlen_zero(calls_taken)) { + bbs_error("Missing calls taken?\n"); + continue; + } + queue = find_queue(queue_name); + if (!queue) { + bbs_debug(5, "Agent '%s' not a member of queue '%s'\n", name, queue_name); + continue; /* Not a queue that concerns us */ + } + bbs_debug(4, "Agent '%s' member of queue '%s'\n", name, queue_name); + member = calloc(1, sizeof(*member)); + if (ALLOC_FAILURE(member)) { + continue; + } + member->agent = agent; + member->queue = queue; + if (calls_taken && name && agent->id == atoi(name)) { + member->calls_taken = atoi(calls_taken); + } + RWLIST_WRLOCK(&queue->members); + RWLIST_INSERT_HEAD(&queue->members, member, entry); + RWLIST_UNLOCK(&queue->members); + } + RWLIST_UNLOCK(&queues); + return 0; +} + +/*! \brief Draw the agent "home screen" showing all queues and their details */ +static int queues_status(struct agent *agent) +{ + struct queue *queue; + + bbs_node_writef(agent->node, "%-22s %6s\t%6s\t%6s\t%6s\n", "===== ACD QUEUE =====", "!!", "+", "-", "@"); + RWLIST_RDLOCK(&queues); + RWLIST_TRAVERSE(&queues, queue, entry) { + struct member *member = queue_member(queue, agent); + if (!member) { + continue; + } + bbs_node_writef(agent->node, "%-22s %6d\t%6d\t%6d\t%6d\n", queue->title, queue->ringing, queue->completed, queue->abandoned, member->calls_taken); + } + RWLIST_UNLOCK(&queues); + return 0; +} + +static int handle_call(struct agent *agent, struct queue_call *call) +{ + int res; + struct queue_call_handler *qch; + struct queue_call_handle qch_info; + + /* While this function is executing, call cannot disappear, + * since we've bumped its refcount. */ + + RWLIST_RDLOCK(&handlers); + RWLIST_TRAVERSE(&handlers, qch, entry) { + if (!strcmp(qch->name, call->queue->handler)) { + /* Increment refcount before breaking loop, + * to ensure it sticks around until we unref it. */ + bbs_module_ref(qch->mod, 1); + break; + } + } + RWLIST_UNLOCK(&handlers); + + if (!qch) { + bbs_warning("No queue call handler exists for queue '%s'\n", call->queue->name); + return -1; + } + + /* Pass the call off to handling to a queue call handler + * for this particular queue. + * This is architected this way to keep the general queue handling logic + * in this module and anything queue-specific in its own module, + * which end users can focus on writing and customizing themselves as needed. */ + + memset(&qch_info, 0, sizeof(qch_info)); + qch_info.node = agent->node; + qch_info.agentid = agent->id; + qch_info.id = call->id; + qch_info.channel = call->channel; + qch_info.ani = call->ani; + qch_info.ani2 = call->ani2; + qch_info.dnis = call->dnis; + qch_info.cnam = call->cnam; + + res = qch->handler(&qch_info); + bbs_module_unref(qch->mod, 1); + + return res; +} + +static int select_call(struct agent *agent) +{ + struct queue_call *call; + int callid; + int res; + struct bbs_ncurses_menu menu; + int call_count = 0; + const char *optval; + char subtitle[116]; + + bbs_ncurses_menu_init(&menu); + + /* We're notified about calls when they arrive, + * but some channels may no longer exist or may be in the queue system. + * We can be notified about channel hangups but not necessarily if the channel + * exists and is no longer in the queue system. + * Manually check all channels in our list and see if any have gone bad. */ + prune_dead_calls(1); + + RWLIST_RDLOCK(&calls); + RWLIST_TRAVERSE(&calls, call, entry) { + char optkey[5]; + char optvalbuf[128]; + snprintf(optkey, sizeof(optkey), "%4d", call->id); + snprintf(optvalbuf, sizeof(optvalbuf), "%4d %-20s %02d %15lu %-15s\n", call->id, call->queue->title, call->ani2, call->ani, call->cnam); + bbs_ncurses_menu_addopt(&menu, 0, optkey, optvalbuf); + call_count++; + } + RWLIST_UNLOCK(&calls); + + bbs_debug(4, "Currently %d call%s in all queues\n", call_count, ESS(call_count)); + if (!call_count) { + /* No calls currently active. Abort. */ + bbs_node_ring_bell(agent->node); + return 0; + } + + bbs_ncurses_menu_set_title(&menu, call_menu_title); + snprintf(subtitle, sizeof(subtitle), "%4s %-20s %2s %15s %-15s", "ID #", "ACD QUEUE", "II", "ANI", "NAME"); + bbs_ncurses_menu_set_subtitle(&menu, subtitle); + + res = bbs_ncurses_menu_getopt(agent->node, &menu); + if (res < 0) { + bbs_ncurses_menu_destroy(&menu); + return 0; + } + + optval = bbs_ncurses_menu_getopt_name(&menu, res); + if (!optval) { + bbs_ncurses_menu_destroy(&menu); + return 0; + } + + /* The queue call ID is at the beginning of the string. atoi will exactly extract it for us. */ + callid = atoi(optval); + bbs_ncurses_menu_destroy(&menu); /* No longer needed at this point */ + + /* Look for this call in the list. */ + RWLIST_WRLOCK(&calls); + RWLIST_TRAVERSE(&calls, call, entry) { + if (call->id == callid) { + /* Prevent call from disappearing, while an agent is handling it. */ + call->refcount++; + break; + } + } + RWLIST_UNLOCK(&calls); + + if (!call) { + bbs_debug(3, "Call %d disappeared before agent could handle it\n", callid); + return 0; + } + + res = handle_call(agent, call); + RWLIST_WRLOCK(&calls); + call->refcount--; + RWLIST_UNLOCK(&calls); + + prune_dead_calls(0); /* We might be able to remove the call we just handled, if it no longer exists. */ + + return res; +} + +static void print_full_time(struct agent *agent) +{ + char str_date[256]; + time_t now = time(NULL); + strftime(str_date, sizeof(str_date), "%Y/%m/%d %I:%M:%S %p", localtime(&now)); + bbs_node_writef(agent->node, "%s%s", COLOR(COLOR_MAGENTA), str_date); +} + +#define BACKSPACE_BUF "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b" + +static void print_full_time_update(struct agent *agent, int forcefull) +{ + time_t now; + char str_date[32]; + now = time(NULL); + strftime(str_date, sizeof(str_date), "%Y/%m/%d %I:%M:%S %p", localtime(&now)); + /* e.g. 2023/01/01 12:00:00 AM */ + if (forcefull) { + bbs_node_writef(agent->node, "\r%s", str_date); /* Fully rewrite entire line */ + } else if (str_date[18] != '0') { + /* If the last digit of the second is non-0, then nothing else could have changed from before. + * Only rewrite that. + * There are escape sequences to move to the exact column we want, + * but this is probably the simplest way to do it: back up a few and then overwrite the tail end. */ + bbs_node_writef(agent->node, "%.*s%s", 4, BACKSPACE_BUF, str_date + 18); + /* Trust me, when you're the one accessing this at 300 baud, + * you will thank me for the added responsiveness! */ + } else { + char last_date[32]; + const char *a, *b; + int diff; + now--; + strftime(last_date, sizeof(last_date), "%Y/%m/%d %I:%M:%S %p", localtime(&now)); + /* Whenever the seconds digit is 0, then at least one previous character must also have changed. + * To determine how many, print the time a second ago into a buffer, + * and then see how similar the two strings are. */ + a = last_date; + b = str_date; + while (*a == *b) { /* It's guaranteed they differ in at least the digit, so no need to check for NUL terminator */ + a++; + b++; + } + /* For example, say we transition thus: + * 2023/01/01 12:00:49 AM + * 2023/01/01 12:00:50 AM + * ^-- a/b after loop. + * diff = 17 after the loop. + * The entire string is 22 characters. + * We need to therefore print 5 backspaces, and then print str_date + 17. + * + * Now, if the change is drastic enough, say diff < X, for some X, + * then it'll be a smaller data transmission to either: + * - explicitly set the character using the "Set Cursor to Col" escape sequence. (X = length of the set column escape sequence) + * - print \r and rewrite the entire line. (X = ~width/2 = 22/2 = 11) + * + * Currently, we just fallback to the latter if needed. + */ + diff = (int) (a - last_date); + if (diff > 12) { + bbs_node_writef(agent->node, "%.*s%s", 22 - diff, BACKSPACE_BUF, str_date + diff); + } else { + bbs_node_writef(agent->node, "\r%s", str_date); /* Fully rewrite entire line */ + } + } +} + +static int agent_exec(struct bbs_node *node, const char *args) +{ + int agentid; + struct agent *agent; + const char *tmp; + + UNUSED(args); + + /* The agent's ID correlates into queues.conf in Asterisk. + * Right now these are set manually per user from variables.conf */ + + bbs_node_lock(node); + tmp = bbs_node_var_get(node, "ASTERISK_AGENT_ID"); + if (!tmp) { + bbs_warning("Rejecting unauthorized queue agent '%s'\n", bbs_username(node->user)); + bbs_node_unlock(node); + return 0; + } + agentid = atoi(tmp); + bbs_node_unlock(node); + + agent = new_agent(node, agentid); + if (!agent) { + return 0; + } + + if (membership_init(agent)) { + goto cleanup; + } + + /* Agent loop */ + for (;;) { +start: + bbs_node_clear_screen(node); + bbs_node_writef(node, "*** %s%-42s %s%d%s ***\n", COLOR(COLOR_MAGENTA), system_title, COLOR(COLOR_GREEN), agent->id, COLOR_RESET); + queues_status(agent); /* Display current status of all relevant queues */ + print_full_time(agent); + bbs_node_unbuffer(node); /* Uncook the terminal. */ + + /* Wait for something interesting to happen. */ + for (;;) { + ssize_t res; + char c; + agent->idle = 1; + res = bbs_node_poll(node, 1000); + agent->idle = 0; + if (res < 0) { + goto cleanup; + } + if (!res) { + /* Nothing happened. Update the time and repeat. */ + if (agent->gotwritten) { + /* Color was reset, so set it back */ + bbs_node_writef(node, "%s", COLOR(COLOR_MAGENTA)); + } + print_full_time_update(agent, agent->gotwritten); /* Update the current time */ + agent->gotwritten = 0; /* If we were, we handled it, and are no more */ + continue; + } + /* else, got input from the node */ + res = bbs_node_read(node, &c, 1); + if (res != 1) { + goto cleanup; + } + if (!isalnum(c)) { + bbs_debug(3, "Ignoring non-alphanumeric input: %d\n", c); + continue; + } + bbs_debug(3, "Handling agent input '%c'\n", c); + bbs_node_writef(node, "%s", COLOR_RESET); + switch (c) { + case 'l': /* Load (handle) call(s) */ + res = select_call(agent); + if (res < 0) { + bbs_debug(4, "Aborting queue system\n"); + goto cleanup; + } + goto start; + case 'r': /* Refresh table */ + case '\n': + goto start; + case 'x': + case 'q': + goto cleanup; + default: /* Ignore */ + bbs_node_writef(node, "%s", COLOR(COLOR_MAGENTA)); + } + } + } + +cleanup: + del_agent(agent); + return 0; +} + +static struct bbs_cli_entry cli_commands_queues[] = { + BBS_CLI_COMMAND(cli_asterisk_queues, "asterisk queues", 2, "List Asterisk queues", NULL), + BBS_CLI_COMMAND(cli_asterisk_agents, "asterisk agents", 2, "List Asterisk queue agents", NULL), + BBS_CLI_COMMAND(cli_asterisk_calls, "asterisk calls", 2, "List Asterisk queue calls", NULL), +}; + +static int load_config(void) +{ + int res = 0; + struct bbs_config *cfg; + struct bbs_config_section *section = NULL; + + cfg = bbs_config_load("mod_asterisk_queues.conf", 1); + if (!cfg) { + return -1; + } + + res |= bbs_config_val_set_str(cfg, "general", "title", system_title, sizeof(system_title)); + res |= bbs_config_val_set_str(cfg, "general", "callmenutitle", call_menu_title, sizeof(call_menu_title)); + res |= bbs_config_val_set_str(cfg, "general", "queueidvar", queue_id_var, sizeof(queue_id_var)); + + if (res) { + bbs_warning("Missing required settings in [general]\n"); + return -1; + } + + RWLIST_WRLOCK(&queues); + while ((section = bbs_config_walk(cfg, section))) { + struct queue *queue; + struct bbs_keyval *keyval = NULL; + const char *name = NULL, *title = NULL, *handler = NULL; /* Mandatory */ + size_t datalen, namelen, titlelen, handlerlen; /* Mandatory */ + char *data; + if (!strcmp(bbs_config_section_name(section), "general")) { + continue; /* Not a queue, skip */ + } + if (find_queue(bbs_config_section_name(section))) { + bbs_warning("Queue '%s' already exists\n", bbs_config_section_name(section)); + continue; + } + + while ((keyval = bbs_config_section_walk(section, keyval))) { + const char *key = bbs_keyval_key(keyval), *value = bbs_keyval_val(keyval); + if (!strcasecmp(key, "title")) { + title = value; + titlelen = strlen(value) + 1; + } else if (!strcasecmp(key, "handler")) { + handler = value; + handlerlen = strlen(value) + 1; + } else { + bbs_warning("Unknown directive: %s\n", key); + } + } + + name = bbs_config_section_name(section); + namelen = strlen(name) + 1; + + if (!title) { + bbs_warning("Missing mandatory field 'title' for '%s'\n", name); + continue; + } else if (!handler) { + bbs_warning("Missing mandatory field 'handler' for '%s'\n", name); + continue; + } + + datalen = namelen + titlelen + handlerlen; + queue = calloc(1, sizeof(*queue) + datalen); + if (ALLOC_FAILURE(queue)) { + continue; + } + data = queue->data; + SET_FSM_STRING_VAR(queue, data, name, name, namelen); + SET_FSM_STRING_VAR(queue, data, title, title, titlelen); + SET_FSM_STRING_VAR(queue, data, handler, handler, handlerlen); + RWLIST_INSERT_TAIL(&queues, queue, entry); + bbs_debug(4, "Added queue '%s'\n", name); + } + RWLIST_UNLOCK(&queues); + + return 0; +} + +static int unload_module(void) +{ + bbs_cli_unregister_multiple(cli_commands_queues); + bbs_ami_callback_unregister(ami_callback); + bbs_unregister_door("astqueue"); + /* Agents and queue members will all be gone if the module is being unloaded, only queues are persistent */ + RWLIST_REMOVE_ALL(&queues, entry, free); + RWLIST_REMOVE_ALL(&calls, entry, free); + return 0; +} + +static int load_module(void) +{ + if (load_config()) { + RWLIST_REMOVE_ALL(&queues, entry, free); + return -1; + } + /* Once we're ready to go, add the callback */ + if (bbs_ami_callback_register(ami_callback)) { + RWLIST_REMOVE_ALL(&queues, entry, free); + return -1; + } + if (queues_init()) { + RWLIST_REMOVE_ALL(&queues, entry, free); + RWLIST_REMOVE_ALL(&calls, entry, free); + return -1; + } + if (bbs_register_door("astqueue", agent_exec)) { + bbs_ami_callback_unregister(ami_callback); + RWLIST_REMOVE_ALL(&queues, entry, free); + RWLIST_REMOVE_ALL(&calls, entry, free); + return -1; + } + bbs_cli_register_multiple(cli_commands_queues); + return 0; +} + +BBS_MODULE_INFO_FLAGS_DEPENDENT("Asterisk Queues", MODFLAG_GLOBAL_SYMBOLS, "mod_asterisk_ami.so,mod_ncurses.so"); diff --git a/modules/mod_ncurses.c b/modules/mod_ncurses.c new file mode 100644 index 00000000..585517ff --- /dev/null +++ b/modules/mod_ncurses.c @@ -0,0 +1,409 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2023, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief Graphical text menus + * + * \author Naveen Albert + */ + +#include "include/bbs.h" + +#include +#include +#include +#include +#include +#include + +#include "include/module.h" +#include "include/node.h" +#include "include/term.h" + +#include "include/mod_ncurses.h" + +void bbs_ncurses_menu_init(struct bbs_ncurses_menu *menu) +{ + menu->title = NULL; + menu->subtitle = NULL; + menu->num_options = 0; + menu->keybindings = menu->keybind; + /* Don't bother initializing the options array */ +} + +void bbs_ncurses_menu_destroy(struct bbs_ncurses_menu *menu) +{ + int i; + for (i = 0; i < menu->num_options; i++) { + free(menu->options[i]); + free_if(menu->optvals[i]); + } + /* Don't free menu itself, it's stack allocated */ +} + +void bbs_ncurses_menu_set_title(struct bbs_ncurses_menu *menu, const char *title) +{ + menu->title = title; +} + +void bbs_ncurses_menu_set_subtitle(struct bbs_ncurses_menu *menu, const char *subtitle) +{ + menu->subtitle = subtitle; +} + +void bbs_ncurses_menu_disable_keybindings(struct bbs_ncurses_menu *menu) +{ + menu->keybindings = NULL; +} + +int bbs_ncurses_menu_addopt(struct bbs_ncurses_menu *menu, char key, const char *opt, const char *value) +{ + if (menu->num_options >= MAX_NCURSES_MENU_OPTIONS) { + bbs_warning("Maximum number of options (%d) reached for menu\n", menu->num_options); + return -1; + } + /* We pass in 0 for no key binding, but to avoid null terminating the buffer, + * we silently change that to a space, which is internally used to indicate no binding. */ + menu->keybind[menu->num_options] = key ? key : ' '; + menu->options[menu->num_options] = strdup(opt); + bbs_debug(6, "Added menu option %d with key binding '%c'\n", menu->num_options, key ? key : ' '); + menu->optvals[menu->num_options] = !strlen_zero(value) ? strdup(value) : NULL; + menu->num_options++; + return 0; +} + +const char *bbs_ncurses_menu_getopt_name(struct bbs_ncurses_menu *menu, int index) +{ + if (!IN_BOUNDS(index, 0, menu->num_options - 1)) { + bbs_warning("Index %d is out of bounds for this menu\n", index); + return NULL; + } + return menu->options[index]; +} + +char bbs_ncurses_menu_getopt_key(struct bbs_ncurses_menu *menu, int index) +{ + if (!IN_BOUNDS(index, 0, menu->num_options - 1)) { + bbs_warning("Index %d is out of bounds for this menu\n", index); + return 0; + } + if (!menu->keybindings) { + return 0; + } + return menu->keybind[index]; /* If keybindings isn't NULL, it's equivalent to keybind */ +} + +char bbs_ncurses_menu_getopt_selection(struct bbs_node *node, struct bbs_ncurses_menu *menu) +{ + int res; + + res = bbs_ncurses_menu_getopt(node, menu); + if (res < 0) { + return 0; + } + + return bbs_ncurses_menu_getopt_key(menu, res); +} + +#define MENU_WIDTH 76 +#define MENU_PAGE_NUM_OPTIONS 10 + +enum print_pos { + PRINT_MIDDLE = 0, + PRINT_LEFT, +}; + +#define print_in_middle(win, starty, startx, width, string, color) __print_pos(win, starty, startx, width, string, color, PRINT_MIDDLE) +#define print_left(win, starty, startx, width, string, color) __print_pos(win, starty, startx, width, string, color, PRINT_LEFT) + +static void __print_pos(WINDOW *win, int starty, int startx, int width, const char *string, chtype color, enum print_pos print_pos) +{ + int length, x, y; + double temp; + + if (!win) { + win = stdscr; + } + getyx(win, y, x); + if (startx != 0) { + x = startx; + } + if (starty != 0) { + y = starty; + } + if (width == 0) { + width = MENU_WIDTH; + } + + length = (int) strlen(string); + temp = (1.0 * width - length) / 2; + x = startx + (int) temp; + wattron(win, color); + switch (print_pos) { + case PRINT_MIDDLE: + mvwprintw(win, y, x, "%s", string); + break; + case PRINT_LEFT: + mvwprintw(win, y, 2, "%s", string); + break; + } + wattroff(win, color); + refresh(); +} + +static int run_menu(const char *title, const char *subtitle, int num_choices, ITEM **options, const char *optkeys) +{ + char *curpos; + int c, offset, selected_item; + ITEM *selection; + MENU *menu; + WINDOW *win; + int show_subtitle = !strlen_zero(subtitle) ? 1 : 0; + + /* Create menu */ + menu = new_menu(options); + + /* Create window for menu */ + win = newwin(MENU_PAGE_NUM_OPTIONS + 5, MENU_WIDTH, 2, 2); + keypad(win, TRUE); + + /* Set main window and sub window */ + set_menu_win(menu, win); + set_menu_sub(menu, derwin(win, MENU_PAGE_NUM_OPTIONS, MENU_WIDTH - 2, 3 + show_subtitle, 1)); + set_menu_format(menu, MENU_PAGE_NUM_OPTIONS, 1); + set_menu_mark(menu, " * "); /* Set "selected" item string */ + + /* Print a border around the main window and print a title */ + wborder(win, '|', '|', '-', '-', '/', '\\', '\\', '/'); /* relying on the default ACS macros (perhaps using box) doesn't always work properly */ + print_in_middle(win, 1, 0, MENU_WIDTH, title, COLOR_PAIR(1)); + + /* This will print left-aligned text on the same line as the title, which could be useful but not what we want here... */ + if (show_subtitle) { + print_left(win, 1 + show_subtitle, 0, MENU_WIDTH, subtitle, COLOR_PAIR(2)); + } + mvwaddch(win, 2 + show_subtitle, 0, '|'); + mvwhline(win, 2 + show_subtitle, 1, '-', MENU_WIDTH - 2); + mvwaddch(win, 2 + show_subtitle, MENU_WIDTH - 1, '|'); + mvprintw(LINES - 2, 0, "ESC to exit"); + + refresh(); + + /* Post the menu */ + post_menu(menu); + wrefresh(win); + + for (;;) { /* Loop until we get an ESCAPE */ + c = wgetch(win); + if (c == ERR) { + selected_item = -1; + goto quit; + } + switch (c) { + case 27: /* Break on ESCAPE */ + selected_item = -1; + goto quit; + case 10: /* Break on ENTER */ + case KEY_ENTER: /* Break on ENTER */ + goto postmenu; + case KEY_DOWN: + menu_driver(menu, REQ_DOWN_ITEM); + break; + case KEY_UP: + menu_driver(menu, REQ_UP_ITEM); + break; + case 'n': + case KEY_NPAGE: + menu_driver(menu, REQ_SCR_DPAGE); + break; + case 'k': + case KEY_PPAGE: + menu_driver(menu, REQ_SCR_UPAGE); + break; + case KEY_HOME: + menu_driver(menu, REQ_FIRST_ITEM); + break; + case KEY_END: + menu_driver(menu, REQ_LAST_ITEM); + break; + default: + if (optkeys) { + curpos = strchr(optkeys, c); + if (curpos && c != ' ') { /* Space indicates no key binding */ + offset = (int) (curpos - optkeys); /* Calculate index in string */ + if (offset < num_choices) { + /* Allow for optstrings like "abcdefg..." that might be longer than the available options. + * Prevent indexing out of bounds by just ignoring if it's not within bounds. */ + set_current_item(menu, options[offset]); + goto postmenu; + } + } else if (c == 'q' && !strchr(optkeys, 'q')) { + /* option string, but it doesn't contain 'q', so treat that as an exit */ + selected_item = -1; + goto quit; + } + } else { /* have a default 'q' binding for quit */ + if (c == 'q') { + selected_item = -1; + goto quit; + } + } + break; /* Do nothing */ + } + wrefresh(win); + } + +postmenu: + selection = current_item(menu); + selected_item = item_index(selection); + +quit: + /* Unpost and free all memory taken up */ + unpost_menu(menu); + free_menu(menu); + + return selected_item; +} + +static int ncurses_menu(struct bbs_ncurses_menu *menu) +{ + int i, selected_item; + ITEM **options; + + /* Initialize curses */ + initscr(); + start_color(); + cbreak(); /* Unlike raw, allow CTRL codes */ + noecho(); + keypad(stdscr, TRUE); + init_pair(1, COLOR_MAGENTA, COLOR_BLACK); + init_pair(2, COLOR_GREEN, COLOR_BLACK); + curs_set(0); /* Disable cursor */ + + /* Yes, you need to allocate N+1, or you get segfaults with 3 menu items... */ + options = calloc((size_t) menu->num_options + 1, sizeof(ITEM *)); + /* Create items */ + for (i = 0; i < menu->num_options; i++) { + /* Even though we can mutate menu independent of the parent, don't. + * This way, we can avoid having to do copy on write altogether. */ + options[i] = new_item(menu->options[i], menu->optvals[i] ? menu->optvals[i] : ""); + if (strlen_zero(menu->options[i])) { + bbs_warning("Option %d is empty\n", i); + } + } + + selected_item = run_menu(menu->title, menu->subtitle, menu->num_options, options, menu->keybind); + for (i = 0; i < menu->num_options; i++) { + free_item(options[i]); + } + free(options); + endwin(); + return selected_item; +} + +int bbs_ncurses_menu_getopt(struct bbs_node *node, struct bbs_ncurses_menu *menu) +{ + int pfd[2]; + int res; + ssize_t rres; + char c; + + /* Null terminate before running for strchr */ + menu->keybind[menu->num_options] = '\0'; + + if (menu->num_options <= 0) { + bbs_warning("Menu has %d options?\n", menu->num_options); + return -1; + } + + bbs_debug(3, "Executing menu with %d option%s\n", menu->num_options, ESS(menu->num_options)); + + /* ncurses is not threadsafe. + * There is a threadsafe version, but it's not very robust, + * not recommended, and it's almost certainly not the version + * of ncurses present on this system. + * Rather than requiring that this version be present, at the risk + * of messing up everything else on the system, + * work around this by forking and running ncurses in a separate process. + * To do this, we fork, and then construct the menu there, passing + * the return value back using a pipe. + */ + + if (pipe(pfd)) { + bbs_error("pipe failed: %s\n", strerror(errno)); + return -1; + } + + bbs_node_unbuffer(node); /* Make sure our PTY is unbuffered for ncurses */ + bbs_node_flush_input(node); + + res = fork(); + if (res < 0) { + bbs_error("fork failed: %s\n", strerror(errno)); + return -1; + } else if (!res) { + /* We now have our own copy of menu, to do with as we please. + * We don't even have to marshall the menu into argv and exec. */ + bbs_set_stdout_logging(0); /* Don't log to the child process's STDOUT. */ + close(pfd[0]); /* Close read end */ + + /* ncurses expects to use STDIN and STDOUT by default. + * Tie these to the node, just like in system.c. */ + dup2(node->slavefd, STDIN_FILENO); + dup2(node->slavefd, STDOUT_FILENO); + close(STDERR_FILENO); + + /* The environment (in particular, the TERM variable) isn't set properly + * at this point for ncurses (it may be, incidentally, if the BBS is running in the foreground, + * started from a terminal, but in general, it may be running daemonized.) + * We could therefore set the TERM variable here if we have it available on the node. */ + + res = ncurses_menu(menu); + c = (char) res; + if (write(pfd[1], &c, 1) != 1) { + _exit(errno); + } + close(pfd[1]); + _exit(0); + } + + /* Wait for completion. */ + node->childpid = res; + close(pfd[1]); /* Close write end */ + if (waitpid(res, NULL, 0) < 0) { + bbs_error("waitpid failed: %s\n", strerror(errno)); + close(pfd[0]); + return -1; + } + node->childpid = 0; + rres = read(pfd[0], &c, 1); + close(pfd[0]); + if (rres != 1) { + bbs_error("Failed to read result: %s\n", strerror(errno)); + return -1; + } + res = c; + bbs_debug(3, "Menu return value: %d\n", res); + return res; +} + +static int load_module(void) +{ + return 0; +} + +static int unload_module(void) +{ + return 0; +} + +BBS_MODULE_INFO_FLAGS("ncurses", MODFLAG_GLOBAL_SYMBOLS); diff --git a/nets/net_imap/imap_client_parallel.c b/nets/net_imap/imap_client_parallel.c index 54faa131..27a0b3d7 100644 --- a/nets/net_imap/imap_client_parallel.c +++ b/nets/net_imap/imap_client_parallel.c @@ -256,7 +256,7 @@ int imap_client_parallel_join(struct imap_parallel *p) for (;;) { /* We're interested in threads for tasks that have been started, but aren't yet completed. * If there aren't any, then everything has finished executing. */ - res = bbs_alertpipe_poll(p->alertpipe); + res = bbs_alertpipe_poll(p->alertpipe, -1); /* A thread exited. */ bbs_alertpipe_read(p->alertpipe); res = run_scheduler(p); diff --git a/nets/net_irc.c b/nets/net_irc.c index 4c13c1c0..c4596912 100644 --- a/nets/net_irc.c +++ b/nets/net_irc.c @@ -3272,7 +3272,7 @@ static void *ping_thread(void *unused) for (;;) { time_t now; int clients = 0; - if (bbs_poll(ping_alertpipe[0], PING_TIME)) { + if (bbs_alertpipe_poll(ping_alertpipe, PING_TIME)) { break; } diff --git a/scripts/install_prereq.sh b/scripts/install_prereq.sh index 4ee5d3cb..ec3bb166 100755 --- a/scripts/install_prereq.sh +++ b/scripts/install_prereq.sh @@ -6,7 +6,7 @@ # -- Core PACKAGES_DEBIAN="build-essential git" # make, git -PACKAGES_FEDORA="git gcc binutils-devel wget autoconf libtool" +PACKAGES_FEDORA="git gcc binutils-devel" # used by libopenarc, libetpan PACKAGES_DEBIAN="$PACKAGES_DEBIAN make automake pkg-config libtool m4" @@ -35,7 +35,7 @@ PACKAGES_FEDORA="$PACKAGES_FEDORA libuuid-devel" # PACKAGES_DEBIAN="$PACKAGES_DEBIAN libbsd-dev" -PACKAGES_FEDORA="$PACKAGES_FEDORA libbsd-devel" +PACKAGES_FEDORA="$PACKAGES_DEBIAN libbsd-devel" # , PACKAGES_DEBIAN="$PACKAGES_DEBIAN libedit-dev libreadline-dev" @@ -47,11 +47,13 @@ PACKAGES_DEBIAN="$PACKAGES_DEBIAN libssh-dev" PACKAGES_DEBIAN="$PACKAGES_DEBIAN binutils" # objdump PACKAGES_FEDORA="$PACKAGES_FEDORA libssh-devel" +# lirc (mod_irc_client) +scripts/lirc.sh + # MariaDB (MySQL) dev headers (mod_mysql, mod_mysql_auth) -# mariadb-server is also required to run a local DBMS, but this is not -# required for either compilation or operation. +# mariadb-server is also required to run a local DBMS, but this is not required for either compilation or operation. PACKAGES_DEBIAN="$PACKAGES_DEBIAN libmariadb-dev libmariadb-dev-compat" -PACKAGES_FEDORA="$PACKAGES_FEDORA mariadb-devel" +PACKAGES_FEDORA="$PACKAGES_FEDORA mariadb105-devel" # LMDB (mod_lmdb) PACKAGES_DEBIAN="$PACKAGES_DEBIAN liblmdb-dev" @@ -100,8 +102,8 @@ fi # == Source Install -# lirc (mod_irc_client) -scripts/lirc.sh +# libcami (mod_asterisk_ami) +scripts/libcami.sh # libdiscord (mod_discord) scripts/libdiscord.sh diff --git a/scripts/libcami.sh b/scripts/libcami.sh new file mode 100755 index 00000000..fbb875a4 --- /dev/null +++ b/scripts/libcami.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e +cd /usr/local/src +if [ ! -d cami ]; then + git clone https://github.com/InterLinked1/cami.git + cd cami +else + cd cami + git stash + git pull +fi +make +make install