mirror of
https://github.com/asterisk/asterisk.git
synced 2025-10-22 12:52:33 +00:00
This patch adds two new menu features to app_confbridge, admin_toggle_menu_ participants and participant_count. The admin action will globally mute / unmute all non-admin participants on a converence, while the participant count simply exposes the existing participant count function to the conference bridge menu. This also adds configuration options to change the sound played when the conference is globally muted / unmuted, as well as the necessary config hooks to place these functions in the DTMF menus. (closes issue ASTERISK-18204) Reported by: Kevin Reeves Tested by: Matt Jordan Patches: app_confbridge.c.patch.txt, conf_config_parser.c.patch.txt, confbridge.h.patch.txt uploaded by Kevin Reeves (license 6281) Review: https://reviewboard.asterisk.org/r/1518/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@345560 65c4cc65-6c06-0410-ace0-fbb531ad65f3
1506 lines
48 KiB
C
1506 lines
48 KiB
C
/*
|
|
* Asterisk -- An open source telephony toolkit.
|
|
*
|
|
* Copyright (C) 2011, Digium, Inc.
|
|
*
|
|
* David Vossel <dvossel@digium.com>
|
|
*
|
|
* See http://www.asterisk.org for more information about
|
|
* the Asterisk project. Please do not directly contact
|
|
* any of the maintainers of this project for assistance;
|
|
* the project provides a web site, mailing lists and IRC
|
|
* channels for your use.
|
|
*
|
|
* 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 ConfBridge config parser
|
|
*
|
|
* \author David Vossel <dvossel@digium.com>
|
|
*/
|
|
|
|
#include "asterisk.h"
|
|
|
|
ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
|
|
#include "asterisk/logger.h"
|
|
#include "asterisk/config.h"
|
|
#include "include/confbridge.h"
|
|
#include "asterisk/astobj2.h"
|
|
#include "asterisk/cli.h"
|
|
#include "asterisk/bridging_features.h"
|
|
#include "asterisk/stringfields.h"
|
|
#include "asterisk/pbx.h"
|
|
|
|
#define CONFBRIDGE_CONFIG "confbridge.conf"
|
|
|
|
static struct ao2_container *user_profiles;
|
|
static struct ao2_container *bridge_profiles;
|
|
static struct ao2_container *menus;
|
|
|
|
/*! bridge profile container functions */
|
|
static int bridge_cmp_cb(void *obj, void *arg, int flags)
|
|
{
|
|
const struct bridge_profile *entry1 = obj;
|
|
const struct bridge_profile *entry2 = arg;
|
|
return (!strcasecmp(entry1->name, entry2->name)) ? CMP_MATCH | CMP_STOP : 0;
|
|
}
|
|
static int bridge_hash_cb(const void *obj, const int flags)
|
|
{
|
|
const struct bridge_profile *b_profile = obj;
|
|
return ast_str_case_hash(b_profile->name);
|
|
}
|
|
static int bridge_mark_delme_cb(void *obj, void *arg, int flag)
|
|
{
|
|
struct bridge_profile *entry = obj;
|
|
entry->delme = 1;
|
|
return 0;
|
|
}
|
|
static int match_bridge_delme_cb(void *obj, void *arg, int flag)
|
|
{
|
|
const struct bridge_profile *entry = obj;
|
|
return entry->delme ? CMP_MATCH : 0;
|
|
}
|
|
|
|
/*! menu container functions */
|
|
static int menu_cmp_cb(void *obj, void *arg, int flags)
|
|
{
|
|
const struct conf_menu *entry1 = obj;
|
|
const struct conf_menu *entry2 = arg;
|
|
return (!strcasecmp(entry1->name, entry2->name)) ? CMP_MATCH | CMP_STOP : 0;
|
|
}
|
|
static int menu_hash_cb(const void *obj, const int flags)
|
|
{
|
|
const struct conf_menu *menu = obj;
|
|
return ast_str_case_hash(menu->name);
|
|
}
|
|
static int menu_mark_delme_cb(void *obj, void *arg, int flag)
|
|
{
|
|
struct conf_menu *entry = obj;
|
|
entry->delme = 1;
|
|
return 0;
|
|
}
|
|
static int match_menu_delme_cb(void *obj, void *arg, int flag)
|
|
{
|
|
const struct conf_menu *entry = obj;
|
|
return entry->delme ? CMP_MATCH : 0;
|
|
}
|
|
static void menu_destructor(void *obj)
|
|
{
|
|
struct conf_menu *menu = obj;
|
|
struct conf_menu_entry *entry = NULL;
|
|
|
|
while ((entry = AST_LIST_REMOVE_HEAD(&menu->entries, entry))) {
|
|
conf_menu_entry_destroy(entry);
|
|
ast_free(entry);
|
|
}
|
|
}
|
|
|
|
/*! User profile container functions */
|
|
static int user_cmp_cb(void *obj, void *arg, int flags)
|
|
{
|
|
const struct user_profile *entry1 = obj;
|
|
const struct user_profile *entry2 = arg;
|
|
return (!strcasecmp(entry1->name, entry2->name)) ? CMP_MATCH | CMP_STOP : 0;
|
|
}
|
|
static int user_hash_cb(const void *obj, const int flags)
|
|
{
|
|
const struct user_profile *u_profile = obj;
|
|
return ast_str_case_hash(u_profile->name);
|
|
}
|
|
static int user_mark_delme_cb(void *obj, void *arg, int flag)
|
|
{
|
|
struct user_profile *entry = obj;
|
|
entry->delme = 1;
|
|
return 0;
|
|
}
|
|
static int match_user_delme_cb(void *obj, void *arg, int flag)
|
|
{
|
|
const struct user_profile *entry = obj;
|
|
return entry->delme ? CMP_MATCH : 0;
|
|
}
|
|
|
|
/*! Bridge Profile Sounds functions */
|
|
static void bridge_profile_sounds_destroy_cb(void *obj)
|
|
{
|
|
struct bridge_profile_sounds *sounds = obj;
|
|
ast_string_field_free_memory(sounds);
|
|
}
|
|
|
|
static struct bridge_profile_sounds *bridge_profile_sounds_alloc(void)
|
|
{
|
|
struct bridge_profile_sounds *sounds = ao2_alloc(sizeof(*sounds), bridge_profile_sounds_destroy_cb);
|
|
|
|
if (!sounds) {
|
|
return NULL;
|
|
}
|
|
if (ast_string_field_init(sounds, 512)) {
|
|
ao2_ref(sounds, -1);
|
|
return NULL;
|
|
}
|
|
|
|
return sounds;
|
|
}
|
|
|
|
static int set_user_option(const char *name, const char *value, struct user_profile *u_profile)
|
|
{
|
|
if (!strcasecmp(name, "admin")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_ADMIN);
|
|
} else if (!strcasecmp(name, "marked")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_MARKEDUSER);
|
|
} else if (!strcasecmp(name, "startmuted")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_STARTMUTED);
|
|
} else if (!strcasecmp(name, "music_on_hold_when_empty")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_MUSICONHOLD);
|
|
} else if (!strcasecmp(name, "quiet")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_QUIET);
|
|
} else if (!strcasecmp(name, "announce_user_count_all")) {
|
|
if (ast_true(value)) {
|
|
u_profile->flags = u_profile->flags | USER_OPT_ANNOUNCEUSERCOUNTALL;
|
|
} else if (ast_false(value)) {
|
|
u_profile->flags = u_profile->flags & ~USER_OPT_ANNOUNCEUSERCOUNTALL;
|
|
} else if (sscanf(value, "%30u", &u_profile->announce_user_count_all_after) == 1) {
|
|
u_profile->flags = u_profile->flags | USER_OPT_ANNOUNCEUSERCOUNTALL;
|
|
} else {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "announce_user_count")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_ANNOUNCEUSERCOUNT);
|
|
} else if (!strcasecmp(name, "announce_only_user")) {
|
|
u_profile->flags = ast_true(value) ?
|
|
u_profile->flags & ~USER_OPT_NOONLYPERSON :
|
|
u_profile->flags | USER_OPT_NOONLYPERSON;
|
|
} else if (!strcasecmp(name, "wait_marked")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_WAITMARKED);
|
|
} else if (!strcasecmp(name, "end_marked")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_ENDMARKED);
|
|
} else if (!strcasecmp(name, "talk_detection_events")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_TALKER_DETECT);
|
|
} else if (!strcasecmp(name, "dtmf_passthrough")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_DTMF_PASS);
|
|
} else if (!strcasecmp(name, "announce_join_leave")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_ANNOUNCE_JOIN_LEAVE);
|
|
} else if (!strcasecmp(name, "pin")) {
|
|
ast_copy_string(u_profile->pin, value, sizeof(u_profile->pin));
|
|
} else if (!strcasecmp(name, "music_on_hold_class")) {
|
|
ast_copy_string(u_profile->moh_class, value, sizeof(u_profile->moh_class));
|
|
} else if (!strcasecmp(name, "denoise")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_DENOISE);
|
|
} else if (!strcasecmp(name, "dsp_talking_threshold")) {
|
|
if (sscanf(value, "%30u", &u_profile->talking_threshold) != 1) {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "dsp_silence_threshold")) {
|
|
if (sscanf(value, "%30u", &u_profile->silence_threshold) != 1) {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "dsp_drop_silence")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_DROP_SILENCE);
|
|
} else if (!strcasecmp(name, "template")) {
|
|
if (!(conf_find_user_profile(NULL, value, u_profile))) {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "jitterbuffer")) {
|
|
ast_set2_flag(u_profile, ast_true(value), USER_OPT_JITTERBUFFER);
|
|
} else {
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int set_sound(const char *sound_name, const char *sound_file, struct bridge_profile_sounds *sounds)
|
|
{
|
|
if (ast_strlen_zero(sound_file)) {
|
|
return -1;
|
|
}
|
|
|
|
if (!strcasecmp(sound_name, "sound_only_person")) {
|
|
ast_string_field_set(sounds, onlyperson, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_has_joined")) {
|
|
ast_string_field_set(sounds, hasjoin, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_has_left")) {
|
|
ast_string_field_set(sounds, hasleft, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_kicked")) {
|
|
ast_string_field_set(sounds, kicked, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_muted")) {
|
|
ast_string_field_set(sounds, muted, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_unmuted")) {
|
|
ast_string_field_set(sounds, unmuted, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_there_are")) {
|
|
ast_string_field_set(sounds, thereare, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_other_in_party")) {
|
|
ast_string_field_set(sounds, otherinparty, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_place_into_conference")) {
|
|
ast_string_field_set(sounds, placeintoconf, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_wait_for_leader")) {
|
|
ast_string_field_set(sounds, waitforleader, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_get_pin")) {
|
|
ast_string_field_set(sounds, getpin, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_invalid_pin")) {
|
|
ast_string_field_set(sounds, invalidpin, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_locked")) {
|
|
ast_string_field_set(sounds, locked, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_unlocked_now")) {
|
|
ast_string_field_set(sounds, unlockednow, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_locked_now")) {
|
|
ast_string_field_set(sounds, lockednow, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_error_menu")) {
|
|
ast_string_field_set(sounds, errormenu, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_join")) {
|
|
ast_string_field_set(sounds, join, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_leave")) {
|
|
ast_string_field_set(sounds, leave, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_participants_muted")) {
|
|
ast_string_field_set(sounds, participantsmuted, sound_file);
|
|
} else if (!strcasecmp(sound_name, "sound_participants_unmuted")) {
|
|
ast_string_field_set(sounds, participantsunmuted, sound_file);
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
static int set_bridge_option(const char *name, const char *value, struct bridge_profile *b_profile)
|
|
{
|
|
if (!strcasecmp(name, "internal_sample_rate")) {
|
|
if (!strcasecmp(value, "auto")) {
|
|
b_profile->internal_sample_rate = 0;
|
|
} else if (sscanf(value, "%30u", &b_profile->internal_sample_rate) != 1) {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "mixing_interval")) {
|
|
if (sscanf(value, "%30u", &b_profile->mix_interval) != 1) {
|
|
return -1;
|
|
}
|
|
switch (b_profile->mix_interval) {
|
|
case 10:
|
|
case 20:
|
|
case 40:
|
|
case 80:
|
|
break;
|
|
default:
|
|
ast_log(LOG_WARNING, "invalid mixing interval %u\n", b_profile->mix_interval);
|
|
b_profile->mix_interval = 0;
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "record_conference")) {
|
|
ast_set2_flag(b_profile, ast_true(value), BRIDGE_OPT_RECORD_CONFERENCE);
|
|
} else if (!strcasecmp(name, "video_mode")) {
|
|
if (!strcasecmp(value, "first_marked")) {
|
|
ast_set_flag(b_profile, BRIDGE_OPT_VIDEO_SRC_FIRST_MARKED);
|
|
} else if (!strcasecmp(value, "last_marked")) {
|
|
ast_set_flag(b_profile, BRIDGE_OPT_VIDEO_SRC_LAST_MARKED);
|
|
} else if (!strcasecmp(value, "follow_talker")) {
|
|
ast_set_flag(b_profile, BRIDGE_OPT_VIDEO_SRC_FOLLOW_TALKER);
|
|
}
|
|
} else if (!strcasecmp(name, "max_members")) {
|
|
if (sscanf(value, "%30u", &b_profile->max_members) != 1) {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "record_file")) {
|
|
ast_copy_string(b_profile->rec_file, value, sizeof(b_profile->rec_file));
|
|
} else if (strlen(name) >= 5 && !strncasecmp(name, "sound", 5)) {
|
|
if (set_sound(name, value, b_profile->sounds)) {
|
|
return -1;
|
|
}
|
|
} else if (!strcasecmp(name, "template")) { /* Only documented for use in CONFBRIDGE dialplan function */
|
|
struct bridge_profile *tmp = b_profile;
|
|
struct bridge_profile_sounds *sounds = bridge_profile_sounds_alloc();
|
|
struct bridge_profile_sounds *oldsounds = b_profile->sounds;
|
|
if (!sounds) {
|
|
return -1;
|
|
}
|
|
if (!(conf_find_bridge_profile(NULL, value, tmp))) {
|
|
ao2_ref(sounds, -1);
|
|
return -1;
|
|
}
|
|
/* Using a bridge profile as a template is a little complicated due to the sounds. Since the sounds
|
|
* structure of a dynamic profile will need to be altered, a completely new sounds structure must be
|
|
* created instead of simply holding a reference to the one built by the config file. */
|
|
ast_string_field_set(sounds, onlyperson, tmp->sounds->onlyperson);
|
|
ast_string_field_set(sounds, hasjoin, tmp->sounds->hasjoin);
|
|
ast_string_field_set(sounds, hasleft, tmp->sounds->hasleft);
|
|
ast_string_field_set(sounds, kicked, tmp->sounds->kicked);
|
|
ast_string_field_set(sounds, muted, tmp->sounds->muted);
|
|
ast_string_field_set(sounds, unmuted, tmp->sounds->unmuted);
|
|
ast_string_field_set(sounds, thereare, tmp->sounds->thereare);
|
|
ast_string_field_set(sounds, otherinparty, tmp->sounds->otherinparty);
|
|
ast_string_field_set(sounds, placeintoconf, tmp->sounds->placeintoconf);
|
|
ast_string_field_set(sounds, waitforleader, tmp->sounds->waitforleader);
|
|
ast_string_field_set(sounds, getpin, tmp->sounds->getpin);
|
|
ast_string_field_set(sounds, invalidpin, tmp->sounds->invalidpin);
|
|
ast_string_field_set(sounds, locked, tmp->sounds->locked);
|
|
ast_string_field_set(sounds, unlockednow, tmp->sounds->unlockednow);
|
|
ast_string_field_set(sounds, lockednow, tmp->sounds->lockednow);
|
|
ast_string_field_set(sounds, errormenu, tmp->sounds->errormenu);
|
|
ast_string_field_set(sounds, participantsmuted, tmp->sounds->participantsmuted);
|
|
ast_string_field_set(sounds, participantsunmuted, tmp->sounds->participantsunmuted);
|
|
|
|
ao2_ref(tmp->sounds, -1); /* sounds struct copied over to it from the template by reference only. */
|
|
ao2_ref(oldsounds,-1); /* original sounds struct we don't need anymore */
|
|
tmp->sounds = sounds; /* the new sounds struct that is a deep copy of the one from the template. */
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*! CONFBRIDGE dialplan function functions and channel datastore. */
|
|
struct func_confbridge_data {
|
|
struct bridge_profile b_profile;
|
|
struct user_profile u_profile;
|
|
unsigned int b_usable:1; /*!< Tells if bridge profile is usable or not */
|
|
unsigned int u_usable:1; /*!< Tells if user profile is usable or not */
|
|
};
|
|
static void func_confbridge_destroy_cb(void *data)
|
|
{
|
|
struct func_confbridge_data *b_data = data;
|
|
conf_bridge_profile_destroy(&b_data->b_profile);
|
|
ast_free(b_data);
|
|
};
|
|
static const struct ast_datastore_info confbridge_datastore = {
|
|
.type = "confbridge",
|
|
.destroy = func_confbridge_destroy_cb
|
|
};
|
|
int func_confbridge_helper(struct ast_channel *chan, const char *cmd, char *data, const char *value)
|
|
{
|
|
struct ast_datastore *datastore = NULL;
|
|
struct func_confbridge_data *b_data = NULL;
|
|
char *parse = NULL;
|
|
int new = 0;
|
|
AST_DECLARE_APP_ARGS(args,
|
|
AST_APP_ARG(type);
|
|
AST_APP_ARG(option);
|
|
);
|
|
|
|
/* parse all the required arguments and make sure they exist. */
|
|
if (ast_strlen_zero(data) || ast_strlen_zero(value)) {
|
|
return -1;
|
|
}
|
|
parse = ast_strdupa(data);
|
|
AST_STANDARD_APP_ARGS(args, parse);
|
|
if (ast_strlen_zero(args.type) || ast_strlen_zero(args.option)) {
|
|
return -1;
|
|
}
|
|
|
|
ast_channel_lock(chan);
|
|
if (!(datastore = ast_channel_datastore_find(chan, &confbridge_datastore, NULL))) {
|
|
ast_channel_unlock(chan);
|
|
|
|
if (!(datastore = ast_datastore_alloc(&confbridge_datastore, NULL))) {
|
|
return 0;
|
|
}
|
|
if (!(b_data = ast_calloc(1, sizeof(*b_data)))) {
|
|
ast_datastore_free(datastore);
|
|
return 0;
|
|
}
|
|
if (!(b_data->b_profile.sounds = bridge_profile_sounds_alloc())) {
|
|
ast_datastore_free(datastore);
|
|
ast_free(b_data);
|
|
return 0;
|
|
}
|
|
datastore->data = b_data;
|
|
new = 1;
|
|
} else {
|
|
ast_channel_unlock(chan);
|
|
b_data = datastore->data;
|
|
}
|
|
|
|
/* SET(CONFBRIDGE(type,option)=value) */
|
|
if (!strcasecmp(args.type, "bridge") && !set_bridge_option(args.option, value, &b_data->b_profile)) {
|
|
b_data->b_usable = 1;
|
|
} else if (!strcasecmp(args.type, "user") && !set_user_option(args.option, value, &b_data->u_profile)) {
|
|
b_data->u_usable = 1;
|
|
} else {
|
|
ast_log(LOG_WARNING, "Profile type \"%s\" can not be set in CONFBRIDGE function with option \"%s\" and value \"%s\"\n",
|
|
args.type, args.option, value);
|
|
goto cleanup_error;
|
|
}
|
|
if (new) {
|
|
ast_channel_lock(chan);
|
|
ast_channel_datastore_add(chan, datastore);
|
|
ast_channel_unlock(chan);
|
|
}
|
|
return 0;
|
|
|
|
cleanup_error:
|
|
ast_log(LOG_ERROR, "Invalid argument provided to the %s function\n", cmd);
|
|
if (new) {
|
|
ast_datastore_free(datastore);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*!
|
|
* \brief Parse the bridge profile options
|
|
*/
|
|
static int parse_bridge(const char *cat, struct ast_config *cfg)
|
|
{
|
|
struct ast_variable *var;
|
|
struct bridge_profile tmp;
|
|
struct bridge_profile *b_profile;
|
|
|
|
ast_copy_string(tmp.name, cat, sizeof(tmp.name));
|
|
if ((b_profile = ao2_find(bridge_profiles, &tmp, OBJ_POINTER))) {
|
|
b_profile->delme = 0;
|
|
} else if ((b_profile = ao2_alloc(sizeof(*b_profile), NULL))) {
|
|
ast_copy_string(b_profile->name, cat, sizeof(b_profile->name));
|
|
ao2_link(bridge_profiles, b_profile);
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
ao2_lock(b_profile);
|
|
/* set defaults */
|
|
b_profile->internal_sample_rate = 0;
|
|
b_profile->flags = 0;
|
|
b_profile->max_members = 0;
|
|
b_profile->mix_interval = 0;
|
|
memset(b_profile->rec_file, 0, sizeof(b_profile->rec_file));
|
|
if (b_profile->sounds) {
|
|
ao2_ref(b_profile->sounds, -1); /* sounds is read only. Once it has been created
|
|
* it can never be altered. This prevents having to
|
|
* do any locking after it is built from the config. */
|
|
b_profile->sounds = NULL;
|
|
}
|
|
|
|
if (!(b_profile->sounds = bridge_profile_sounds_alloc())) {
|
|
ao2_unlock(b_profile);
|
|
ao2_ref(b_profile, -1);
|
|
ao2_unlink(bridge_profiles, b_profile);
|
|
return -1;
|
|
}
|
|
|
|
for (var = ast_variable_browse(cfg, cat); var; var = var->next) {
|
|
if (!strcasecmp(var->name, "type")) {
|
|
continue;
|
|
} else if (set_bridge_option(var->name, var->value, b_profile)) {
|
|
ast_log(LOG_WARNING, "Invalid: '%s' at line %d of %s is not supported.\n",
|
|
var->name, var->lineno, CONFBRIDGE_CONFIG);
|
|
}
|
|
}
|
|
ao2_unlock(b_profile);
|
|
|
|
ao2_ref(b_profile, -1);
|
|
return 0;
|
|
}
|
|
|
|
static int parse_user(const char *cat, struct ast_config *cfg)
|
|
{
|
|
struct ast_variable *var;
|
|
struct user_profile tmp;
|
|
struct user_profile *u_profile;
|
|
|
|
ast_copy_string(tmp.name, cat, sizeof(tmp.name));
|
|
if ((u_profile = ao2_find(user_profiles, &tmp, OBJ_POINTER))) {
|
|
u_profile->delme = 0;
|
|
} else if ((u_profile = ao2_alloc(sizeof(*u_profile), NULL))) {
|
|
ast_copy_string(u_profile->name, cat, sizeof(u_profile->name));
|
|
ao2_link(user_profiles, u_profile);
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
ao2_lock(u_profile);
|
|
/* set defaults */
|
|
u_profile->flags = 0;
|
|
u_profile->announce_user_count_all_after = 0;
|
|
u_profile->silence_threshold = DEFAULT_SILENCE_THRESHOLD;
|
|
u_profile->talking_threshold = DEFAULT_TALKING_THRESHOLD;
|
|
memset(u_profile->pin, 0, sizeof(u_profile->pin));
|
|
memset(u_profile->moh_class, 0, sizeof(u_profile->moh_class));
|
|
for (var = ast_variable_browse(cfg, cat); var; var = var->next) {
|
|
if (!strcasecmp(var->name, "type")) {
|
|
continue;
|
|
} else if (set_user_option(var->name, var->value, u_profile)) {
|
|
ast_log(LOG_WARNING, "Invalid option '%s' at line %d of %s is not supported.\n",
|
|
var->name, var->lineno, CONFBRIDGE_CONFIG);
|
|
}
|
|
}
|
|
ao2_unlock(u_profile);
|
|
|
|
ao2_ref(u_profile, -1);
|
|
return 0;
|
|
}
|
|
|
|
static int add_action_to_menu_entry(struct conf_menu_entry *menu_entry, enum conf_menu_action_id id, char *databuf)
|
|
{
|
|
struct conf_menu_action *menu_action = ast_calloc(1, sizeof(*menu_action));
|
|
|
|
if (!menu_action) {
|
|
return -1;
|
|
}
|
|
menu_action->id = id;
|
|
|
|
switch (id) {
|
|
case MENU_ACTION_NOOP:
|
|
case MENU_ACTION_TOGGLE_MUTE:
|
|
case MENU_ACTION_INCREASE_LISTENING:
|
|
case MENU_ACTION_DECREASE_LISTENING:
|
|
case MENU_ACTION_INCREASE_TALKING:
|
|
case MENU_ACTION_DECREASE_TALKING:
|
|
case MENU_ACTION_RESET_LISTENING:
|
|
case MENU_ACTION_RESET_TALKING:
|
|
case MENU_ACTION_ADMIN_TOGGLE_LOCK:
|
|
case MENU_ACTION_ADMIN_TOGGLE_MUTE_PARTICIPANTS:
|
|
case MENU_ACTION_PARTICIPANT_COUNT:
|
|
case MENU_ACTION_ADMIN_KICK_LAST:
|
|
case MENU_ACTION_LEAVE:
|
|
case MENU_ACTION_SET_SINGLE_VIDEO_SRC:
|
|
case MENU_ACTION_RELEASE_SINGLE_VIDEO_SRC:
|
|
break;
|
|
case MENU_ACTION_PLAYBACK:
|
|
case MENU_ACTION_PLAYBACK_AND_CONTINUE:
|
|
if (!(ast_strlen_zero(databuf))) {
|
|
ast_copy_string(menu_action->data.playback_file, databuf, sizeof(menu_action->data.playback_file));
|
|
} else {
|
|
ast_free(menu_action);
|
|
return -1;
|
|
}
|
|
break;
|
|
case MENU_ACTION_DIALPLAN_EXEC:
|
|
if (!(ast_strlen_zero(databuf))) {
|
|
AST_DECLARE_APP_ARGS(args,
|
|
AST_APP_ARG(context);
|
|
AST_APP_ARG(exten);
|
|
AST_APP_ARG(priority);
|
|
);
|
|
AST_STANDARD_APP_ARGS(args, databuf);
|
|
if (!ast_strlen_zero(args.context)) {
|
|
ast_copy_string(menu_action->data.dialplan_args.context,
|
|
args.context,
|
|
sizeof(menu_action->data.dialplan_args.context));
|
|
}
|
|
if (!ast_strlen_zero(args.exten)) {
|
|
ast_copy_string(menu_action->data.dialplan_args.exten,
|
|
args.exten,
|
|
sizeof(menu_action->data.dialplan_args.exten));
|
|
}
|
|
menu_action->data.dialplan_args.priority = 1; /* 1 by default */
|
|
if (!ast_strlen_zero(args.priority) &&
|
|
(sscanf(args.priority, "%30u", &menu_action->data.dialplan_args.priority) != 1)) {
|
|
/* invalid priority */
|
|
ast_free(menu_action);
|
|
return -1;
|
|
}
|
|
} else {
|
|
ast_free(menu_action);
|
|
return -1;
|
|
}
|
|
};
|
|
|
|
AST_LIST_INSERT_TAIL(&menu_entry->actions, menu_action, action);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int add_menu_entry(struct conf_menu *menu, const char *dtmf, const char *action_names)
|
|
{
|
|
struct conf_menu_entry *menu_entry = NULL, *cur = NULL;
|
|
int res = 0;
|
|
char *tmp_action_names = ast_strdupa(action_names);
|
|
char *action = NULL;
|
|
char *action_args;
|
|
char *tmp;
|
|
char buf[PATH_MAX];
|
|
char *delimiter = ",";
|
|
|
|
if (!(menu_entry = ast_calloc(1, sizeof(*menu_entry)))) {
|
|
return -1;
|
|
}
|
|
|
|
for (;;) {
|
|
char *comma;
|
|
char *startbrace;
|
|
char *endbrace;
|
|
unsigned int action_len;
|
|
|
|
if (ast_strlen_zero(tmp_action_names)) {
|
|
break;
|
|
}
|
|
startbrace = strchr(tmp_action_names, '(');
|
|
endbrace = strchr(tmp_action_names, ')');
|
|
comma = strchr(tmp_action_names, ',');
|
|
|
|
/* If the next action has brackets with comma delimited arguments in it,
|
|
* make the delimeter ')' instead of a comma to preserve the argments */
|
|
if (startbrace && endbrace && comma && (comma > startbrace && comma < endbrace)) {
|
|
delimiter = ")";
|
|
} else {
|
|
delimiter = ",";
|
|
}
|
|
|
|
if (!(action = strsep(&tmp_action_names, delimiter))) {
|
|
break;
|
|
}
|
|
|
|
action = ast_strip(action);
|
|
if (ast_strlen_zero(action)) {
|
|
continue;
|
|
}
|
|
|
|
action_len = strlen(action);
|
|
ast_copy_string(menu_entry->dtmf, dtmf, sizeof(menu_entry->dtmf));
|
|
if (!strcasecmp(action, "toggle_mute")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_TOGGLE_MUTE, NULL);
|
|
} else if (!strcasecmp(action, "no_op")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_NOOP, NULL);
|
|
} else if (!strcasecmp(action, "increase_listening_volume")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_INCREASE_LISTENING, NULL);
|
|
} else if (!strcasecmp(action, "decrease_listening_volume")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_DECREASE_LISTENING, NULL);
|
|
} else if (!strcasecmp(action, "increase_talking_volume")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_INCREASE_TALKING, NULL);
|
|
} else if (!strcasecmp(action, "reset_listening_volume")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_RESET_LISTENING, NULL);
|
|
} else if (!strcasecmp(action, "reset_talking_volume")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_RESET_TALKING, NULL);
|
|
} else if (!strcasecmp(action, "decrease_talking_volume")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_DECREASE_TALKING, NULL);
|
|
} else if (!strcasecmp(action, "admin_toggle_conference_lock")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_ADMIN_TOGGLE_LOCK, NULL);
|
|
} else if (!strcasecmp(action, "admin_toggle_mute_participants")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_ADMIN_TOGGLE_MUTE_PARTICIPANTS, NULL);
|
|
} else if (!strcasecmp(action, "participant_count")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_PARTICIPANT_COUNT, NULL);
|
|
} else if (!strcasecmp(action, "admin_kick_last")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_ADMIN_KICK_LAST, NULL);
|
|
} else if (!strcasecmp(action, "leave_conference")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_LEAVE, NULL);
|
|
} else if (!strcasecmp(action, "set_as_single_video_src")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_SET_SINGLE_VIDEO_SRC, NULL);
|
|
} else if (!strcasecmp(action, "release_as_single_video_src")) {
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_RELEASE_SINGLE_VIDEO_SRC, NULL);
|
|
} else if (!strncasecmp(action, "dialplan_exec(", 14)) {
|
|
ast_copy_string(buf, action, sizeof(buf));
|
|
action_args = buf;
|
|
if ((action_args = strchr(action, '('))) {
|
|
action_args++;
|
|
}
|
|
/* it is possible that this argument may or may not
|
|
* have a closing brace at this point, it all depends on if
|
|
* comma delimited arguments were provided */
|
|
if ((tmp = strchr(action, ')'))) {
|
|
*tmp = '\0';
|
|
}
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_DIALPLAN_EXEC, action_args);
|
|
} else if (action_len >= 21 && !strncasecmp(action, "playback_and_continue(", 22)) {
|
|
ast_copy_string(buf, action, sizeof(buf));
|
|
action_args = buf;
|
|
if ((action_args = strchr(action, '(')) && (tmp = strrchr(action_args, ')'))) {
|
|
*tmp = '\0';
|
|
action_args++;
|
|
}
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_PLAYBACK_AND_CONTINUE, action_args);
|
|
} else if (action_len >= 8 && !strncasecmp(action, "playback(", 9)) {
|
|
ast_copy_string(buf, action, sizeof(buf));
|
|
action_args = buf;
|
|
if ((action_args = strchr(action, '(')) && (tmp = strrchr(action_args, ')'))) {
|
|
*tmp = '\0';
|
|
action_args++;
|
|
}
|
|
res |= add_action_to_menu_entry(menu_entry, MENU_ACTION_PLAYBACK, action_args);
|
|
}
|
|
}
|
|
|
|
/* if adding any of the actions failed, bail */
|
|
if (res) {
|
|
struct conf_menu_action *action;
|
|
while ((action = AST_LIST_REMOVE_HEAD(&menu_entry->actions, action))) {
|
|
ast_free(action);
|
|
}
|
|
ast_free(menu_entry);
|
|
return -1;
|
|
}
|
|
|
|
/* remove any list entry with an identical DTMF sequence for overrides */
|
|
AST_LIST_TRAVERSE_SAFE_BEGIN(&menu->entries, cur, entry) {
|
|
if (!strcasecmp(cur->dtmf, menu_entry->dtmf)) {
|
|
AST_LIST_REMOVE_CURRENT(entry);
|
|
ast_free(cur);
|
|
break;
|
|
}
|
|
}
|
|
AST_LIST_TRAVERSE_SAFE_END;
|
|
|
|
AST_LIST_INSERT_TAIL(&menu->entries, menu_entry, entry);
|
|
|
|
return 0;
|
|
}
|
|
static int parse_menu(const char *cat, struct ast_config *cfg)
|
|
{
|
|
struct ast_variable *var;
|
|
struct conf_menu tmp;
|
|
struct conf_menu *menu;
|
|
|
|
ast_copy_string(tmp.name, cat, sizeof(tmp.name));
|
|
if ((menu = ao2_find(menus, &tmp, OBJ_POINTER))) {
|
|
menu->delme = 0;
|
|
} else if ((menu = ao2_alloc(sizeof(*menu), menu_destructor))) {
|
|
ast_copy_string(menu->name, cat, sizeof(menu->name));
|
|
ao2_link(menus, menu);
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
ao2_lock(menu);
|
|
/* this isn't freeing the menu, just destroying the menu list so it can be rebuilt.*/
|
|
menu_destructor(menu);
|
|
for (var = ast_variable_browse(cfg, cat); var; var = var->next) {
|
|
if (!strcasecmp(var->name, "type")) {
|
|
continue;
|
|
} else if (add_menu_entry(menu, var->name, var->value)) {
|
|
ast_log(LOG_WARNING, "Unknown option '%s' at line %d of %s is not supported.\n",
|
|
var->name, var->lineno, CONFBRIDGE_CONFIG);
|
|
}
|
|
}
|
|
ao2_unlock(menu);
|
|
|
|
ao2_ref(menu, -1);
|
|
return 0;
|
|
}
|
|
|
|
static char *complete_user_profile_name(const char *line, const char *word, int pos, int state)
|
|
{
|
|
int which = 0;
|
|
char *res = NULL;
|
|
int wordlen = strlen(word);
|
|
struct ao2_iterator i;
|
|
struct user_profile *u_profile = NULL;
|
|
|
|
i = ao2_iterator_init(user_profiles, 0);
|
|
while ((u_profile = ao2_iterator_next(&i))) {
|
|
if (!strncasecmp(u_profile->name, word, wordlen) && ++which > state) {
|
|
res = ast_strdup(u_profile->name);
|
|
ao2_ref(u_profile, -1);
|
|
break;
|
|
}
|
|
ao2_ref(u_profile, -1);
|
|
}
|
|
ao2_iterator_destroy(&i);
|
|
|
|
return res;
|
|
}
|
|
|
|
static char *handle_cli_confbridge_show_user_profiles(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
struct ao2_iterator it;
|
|
struct user_profile *u_profile;
|
|
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "confbridge show profile users";
|
|
e->usage =
|
|
"Usage confbridge show profile users\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
return NULL;
|
|
}
|
|
|
|
ast_cli(a->fd,"--------- User Profiles -----------\n");
|
|
ao2_lock(user_profiles);
|
|
it = ao2_iterator_init(user_profiles, 0);
|
|
while ((u_profile = ao2_iterator_next(&it))) {
|
|
ast_cli(a->fd,"%s\n", u_profile->name);
|
|
ao2_ref(u_profile, -1);
|
|
}
|
|
ao2_iterator_destroy(&it);
|
|
ao2_unlock(user_profiles);
|
|
|
|
return CLI_SUCCESS;
|
|
}
|
|
static char *handle_cli_confbridge_show_user_profile(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
struct user_profile u_profile;
|
|
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "confbridge show profile user";
|
|
e->usage =
|
|
"Usage confbridge show profile user [<profile name>]\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
if (a->pos == 4) {
|
|
return complete_user_profile_name(a->line, a->word, a->pos, a->n);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
if (a->argc != 5) {
|
|
return CLI_SHOWUSAGE;
|
|
}
|
|
|
|
if (!(conf_find_user_profile(NULL, a->argv[4], &u_profile))) {
|
|
ast_cli(a->fd, "No conference user profile named '%s' found!\n", a->argv[4]);
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
ast_cli(a->fd,"--------------------------------------------\n");
|
|
ast_cli(a->fd,"Name: %s\n",
|
|
u_profile.name);
|
|
ast_cli(a->fd,"Admin: %s\n",
|
|
u_profile.flags & USER_OPT_ADMIN ?
|
|
"true" : "false");
|
|
ast_cli(a->fd,"Marked User: %s\n",
|
|
u_profile.flags & USER_OPT_MARKEDUSER ?
|
|
"true" : "false");
|
|
ast_cli(a->fd,"Start Muted: %s\n",
|
|
u_profile.flags & USER_OPT_STARTMUTED?
|
|
"true" : "false");
|
|
ast_cli(a->fd,"MOH When Empty: %s\n",
|
|
u_profile.flags & USER_OPT_MUSICONHOLD ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"MOH Class: %s\n",
|
|
ast_strlen_zero(u_profile.moh_class) ?
|
|
"default" : u_profile.moh_class);
|
|
ast_cli(a->fd,"Quiet: %s\n",
|
|
u_profile.flags & USER_OPT_QUIET ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Wait Marked: %s\n",
|
|
u_profile.flags & USER_OPT_WAITMARKED ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"END Marked: %s\n",
|
|
u_profile.flags & USER_OPT_ENDMARKED ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Drop_silence: %s\n",
|
|
u_profile.flags & USER_OPT_DROP_SILENCE ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Silence Threshold: %dms\n",
|
|
u_profile.silence_threshold);
|
|
ast_cli(a->fd,"Talking Threshold: %dms\n",
|
|
u_profile.talking_threshold);
|
|
ast_cli(a->fd,"Denoise: %s\n",
|
|
u_profile.flags & USER_OPT_DENOISE ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Jitterbuffer: %s\n",
|
|
u_profile.flags & USER_OPT_JITTERBUFFER ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Talk Detect Events: %s\n",
|
|
u_profile.flags & USER_OPT_TALKER_DETECT ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"DTMF Pass Through: %s\n",
|
|
u_profile.flags & USER_OPT_DTMF_PASS ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"PIN: %s\n",
|
|
ast_strlen_zero(u_profile.pin) ?
|
|
"None" : u_profile.pin);
|
|
ast_cli(a->fd,"Announce User Count: %s\n",
|
|
u_profile.flags & USER_OPT_ANNOUNCEUSERCOUNT ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Announce join/leave: %s\n",
|
|
u_profile.flags & USER_OPT_ANNOUNCE_JOIN_LEAVE ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"Announce User Count all: %s\n",
|
|
u_profile.flags & USER_OPT_ANNOUNCEUSERCOUNTALL ?
|
|
"enabled" : "disabled");
|
|
ast_cli(a->fd,"\n");
|
|
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
static char *complete_bridge_profile_name(const char *line, const char *word, int pos, int state)
|
|
{
|
|
int which = 0;
|
|
char *res = NULL;
|
|
int wordlen = strlen(word);
|
|
struct ao2_iterator i;
|
|
struct bridge_profile *b_profile = NULL;
|
|
|
|
i = ao2_iterator_init(bridge_profiles, 0);
|
|
while ((b_profile = ao2_iterator_next(&i))) {
|
|
if (!strncasecmp(b_profile->name, word, wordlen) && ++which > state) {
|
|
res = ast_strdup(b_profile->name);
|
|
ao2_ref(b_profile, -1);
|
|
break;
|
|
}
|
|
ao2_ref(b_profile, -1);
|
|
}
|
|
ao2_iterator_destroy(&i);
|
|
|
|
return res;
|
|
}
|
|
|
|
static char *handle_cli_confbridge_show_bridge_profiles(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
struct ao2_iterator it;
|
|
struct bridge_profile *b_profile;
|
|
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "confbridge show profile bridges";
|
|
e->usage =
|
|
"Usage confbridge show profile bridges\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
return NULL;
|
|
}
|
|
|
|
ast_cli(a->fd,"--------- Bridge Profiles -----------\n");
|
|
ao2_lock(bridge_profiles);
|
|
it = ao2_iterator_init(bridge_profiles, 0);
|
|
while ((b_profile = ao2_iterator_next(&it))) {
|
|
ast_cli(a->fd,"%s\n", b_profile->name);
|
|
ao2_ref(b_profile, -1);
|
|
}
|
|
ao2_iterator_destroy(&it);
|
|
ao2_unlock(bridge_profiles);
|
|
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
static char *handle_cli_confbridge_show_bridge_profile(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
struct bridge_profile b_profile;
|
|
char tmp[64];
|
|
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "confbridge show profile bridge";
|
|
e->usage =
|
|
"Usage confbridge show profile bridge <profile name>\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
if (a->pos == 4) {
|
|
return complete_bridge_profile_name(a->line, a->word, a->pos, a->n);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
if (a->argc != 5) {
|
|
return CLI_SHOWUSAGE;
|
|
}
|
|
|
|
if (!(conf_find_bridge_profile(NULL, a->argv[4], &b_profile))) {
|
|
ast_cli(a->fd, "No conference bridge profile named '%s' found!\n", a->argv[4]);
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
ast_cli(a->fd,"--------------------------------------------\n");
|
|
ast_cli(a->fd,"Name: %s\n", b_profile.name);
|
|
|
|
if (b_profile.internal_sample_rate) {
|
|
snprintf(tmp, sizeof(tmp), "%d", b_profile.internal_sample_rate);
|
|
} else {
|
|
ast_copy_string(tmp, "auto", sizeof(tmp));
|
|
}
|
|
ast_cli(a->fd,"Internal Sample Rate: %s\n", tmp);
|
|
|
|
if (b_profile.mix_interval) {
|
|
ast_cli(a->fd,"Mixing Interval: %d\n", b_profile.mix_interval);
|
|
} else {
|
|
ast_cli(a->fd,"Mixing Interval: Default 20ms\n");
|
|
}
|
|
|
|
ast_cli(a->fd,"Record Conference: %s\n",
|
|
b_profile.flags & BRIDGE_OPT_RECORD_CONFERENCE ?
|
|
"yes" : "no");
|
|
|
|
ast_cli(a->fd,"Record File: %s\n",
|
|
ast_strlen_zero(b_profile.rec_file) ? "Auto Generated" :
|
|
b_profile.rec_file);
|
|
|
|
if (b_profile.max_members) {
|
|
ast_cli(a->fd,"Max Members: %d\n", b_profile.max_members);
|
|
} else {
|
|
ast_cli(a->fd,"Max Members: No Limit\n");
|
|
}
|
|
|
|
if (b_profile.flags & BRIDGE_OPT_VIDEO_SRC_LAST_MARKED) {
|
|
ast_cli(a->fd, "Video Mode: last_marked\n");
|
|
} else if (b_profile.flags & BRIDGE_OPT_VIDEO_SRC_FIRST_MARKED) {
|
|
ast_cli(a->fd, "Video Mode: first_marked\n");
|
|
} else if (b_profile.flags & BRIDGE_OPT_VIDEO_SRC_FOLLOW_TALKER) {
|
|
ast_cli(a->fd, "Video Mode: follow_talker\n");
|
|
} else {
|
|
ast_cli(a->fd, "Video Mode: no video\n");
|
|
}
|
|
|
|
ast_cli(a->fd,"sound_join: %s\n", conf_get_sound(CONF_SOUND_JOIN, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_leave: %s\n", conf_get_sound(CONF_SOUND_LEAVE, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_only_person: %s\n", conf_get_sound(CONF_SOUND_ONLY_PERSON, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_has_joined: %s\n", conf_get_sound(CONF_SOUND_HAS_JOINED, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_has_left: %s\n", conf_get_sound(CONF_SOUND_HAS_LEFT, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_kicked: %s\n", conf_get_sound(CONF_SOUND_KICKED, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_muted: %s\n", conf_get_sound(CONF_SOUND_MUTED, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_unmuted: %s\n", conf_get_sound(CONF_SOUND_UNMUTED, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_there_are: %s\n", conf_get_sound(CONF_SOUND_THERE_ARE, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_other_in_party: %s\n", conf_get_sound(CONF_SOUND_OTHER_IN_PARTY, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_place_into_conference: %s\n", conf_get_sound(CONF_SOUND_PLACE_IN_CONF, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_wait_for_leader: %s\n", conf_get_sound(CONF_SOUND_WAIT_FOR_LEADER, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_get_pin: %s\n", conf_get_sound(CONF_SOUND_GET_PIN, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_invalid_pin: %s\n", conf_get_sound(CONF_SOUND_INVALID_PIN, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_locked: %s\n", conf_get_sound(CONF_SOUND_LOCKED, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_unlocked_now: %s\n", conf_get_sound(CONF_SOUND_UNLOCKED_NOW, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_lockednow: %s\n", conf_get_sound(CONF_SOUND_LOCKED_NOW, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_error_menu: %s\n", conf_get_sound(CONF_SOUND_ERROR_MENU, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_participants_muted: %s\n", conf_get_sound(CONF_SOUND_PARTICIPANTS_MUTED, b_profile.sounds));
|
|
ast_cli(a->fd,"sound_participants_unmuted: %s\n", conf_get_sound(CONF_SOUND_PARTICIPANTS_UNMUTED, b_profile.sounds));
|
|
ast_cli(a->fd,"\n");
|
|
|
|
conf_bridge_profile_destroy(&b_profile);
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
static char *complete_menu_name(const char *line, const char *word, int pos, int state)
|
|
{
|
|
int which = 0;
|
|
char *res = NULL;
|
|
int wordlen = strlen(word);
|
|
struct ao2_iterator i;
|
|
struct conf_menu *menu = NULL;
|
|
|
|
i = ao2_iterator_init(menus, 0);
|
|
while ((menu = ao2_iterator_next(&i))) {
|
|
if (!strncasecmp(menu->name, word, wordlen) && ++which > state) {
|
|
res = ast_strdup(menu->name);
|
|
ao2_ref(menu, -1);
|
|
break;
|
|
}
|
|
ao2_ref(menu, -1);
|
|
}
|
|
ao2_iterator_destroy(&i);
|
|
|
|
return res;
|
|
}
|
|
|
|
static char *handle_cli_confbridge_show_menus(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
struct ao2_iterator it;
|
|
struct conf_menu *menu;
|
|
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "confbridge show menus";
|
|
e->usage =
|
|
"Usage confbridge show profile menus\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
return NULL;
|
|
}
|
|
|
|
ast_cli(a->fd,"--------- Menus -----------\n");
|
|
ao2_lock(menus);
|
|
it = ao2_iterator_init(menus, 0);
|
|
while ((menu = ao2_iterator_next(&it))) {
|
|
ast_cli(a->fd,"%s\n", menu->name);
|
|
ao2_ref(menu, -1);
|
|
}
|
|
ao2_iterator_destroy(&it);
|
|
ao2_unlock(menus);
|
|
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
static char *handle_cli_confbridge_show_menu(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
|
|
{
|
|
struct conf_menu tmp;
|
|
struct conf_menu *menu;
|
|
struct conf_menu_entry *menu_entry = NULL;
|
|
struct conf_menu_action *menu_action = NULL;
|
|
|
|
switch (cmd) {
|
|
case CLI_INIT:
|
|
e->command = "confbridge show menu";
|
|
e->usage =
|
|
"Usage confbridge show menu [<menu name>]\n";
|
|
return NULL;
|
|
case CLI_GENERATE:
|
|
if (a->pos == 3) {
|
|
return complete_menu_name(a->line, a->word, a->pos, a->n);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
if (a->argc != 4) {
|
|
return CLI_SHOWUSAGE;
|
|
}
|
|
|
|
ast_copy_string(tmp.name, a->argv[3], sizeof(tmp.name));
|
|
if (!(menu = ao2_find(menus, &tmp, OBJ_POINTER))) {
|
|
ast_cli(a->fd, "No conference menu named '%s' found!\n", a->argv[3]);
|
|
return CLI_SUCCESS;
|
|
}
|
|
ao2_lock(menu);
|
|
|
|
ast_cli(a->fd,"Name: %s\n", menu->name);
|
|
AST_LIST_TRAVERSE(&menu->entries, menu_entry, entry) {
|
|
int action_num = 0;
|
|
ast_cli(a->fd, "%s=", menu_entry->dtmf);
|
|
AST_LIST_TRAVERSE(&menu_entry->actions, menu_action, action) {
|
|
if (action_num) {
|
|
ast_cli(a->fd, ", ");
|
|
}
|
|
switch (menu_action->id) {
|
|
case MENU_ACTION_TOGGLE_MUTE:
|
|
ast_cli(a->fd, "toggle_mute");
|
|
break;
|
|
case MENU_ACTION_NOOP:
|
|
ast_cli(a->fd, "no_op");
|
|
break;
|
|
case MENU_ACTION_INCREASE_LISTENING:
|
|
ast_cli(a->fd, "increase_listening_volume");
|
|
break;
|
|
case MENU_ACTION_DECREASE_LISTENING:
|
|
ast_cli(a->fd, "decrease_listening_volume");
|
|
break;
|
|
case MENU_ACTION_RESET_LISTENING:
|
|
ast_cli(a->fd, "reset_listening_volume");
|
|
break;
|
|
case MENU_ACTION_RESET_TALKING:
|
|
ast_cli(a->fd, "reset_talking_volume");
|
|
break;
|
|
case MENU_ACTION_INCREASE_TALKING:
|
|
ast_cli(a->fd, "increase_talking_volume");
|
|
break;
|
|
case MENU_ACTION_DECREASE_TALKING:
|
|
ast_cli(a->fd, "decrease_talking_volume");
|
|
break;
|
|
case MENU_ACTION_PLAYBACK:
|
|
ast_cli(a->fd, "playback(%s)", menu_action->data.playback_file);
|
|
break;
|
|
case MENU_ACTION_PLAYBACK_AND_CONTINUE:
|
|
ast_cli(a->fd, "playback_and_continue(%s)", menu_action->data.playback_file);
|
|
break;
|
|
case MENU_ACTION_DIALPLAN_EXEC:
|
|
ast_cli(a->fd, "dialplan_exec(%s,%s,%d)",
|
|
menu_action->data.dialplan_args.context,
|
|
menu_action->data.dialplan_args.exten,
|
|
menu_action->data.dialplan_args.priority);
|
|
break;
|
|
case MENU_ACTION_ADMIN_TOGGLE_LOCK:
|
|
ast_cli(a->fd, "admin_toggle_conference_lock");
|
|
break;
|
|
case MENU_ACTION_ADMIN_TOGGLE_MUTE_PARTICIPANTS:
|
|
ast_cli(a->fd, "admin_toggle_mute_participants");
|
|
break;
|
|
case MENU_ACTION_PARTICIPANT_COUNT:
|
|
ast_cli(a->fd, "participant_count");
|
|
break;
|
|
case MENU_ACTION_ADMIN_KICK_LAST:
|
|
ast_cli(a->fd, "admin_kick_last");
|
|
break;
|
|
case MENU_ACTION_LEAVE:
|
|
ast_cli(a->fd, "leave_conference");
|
|
break;
|
|
case MENU_ACTION_SET_SINGLE_VIDEO_SRC:
|
|
ast_cli(a->fd, "set_as_single_video_src");
|
|
break;
|
|
case MENU_ACTION_RELEASE_SINGLE_VIDEO_SRC:
|
|
ast_cli(a->fd, "release_as_single_video_src");
|
|
break;
|
|
}
|
|
action_num++;
|
|
}
|
|
ast_cli(a->fd,"\n");
|
|
}
|
|
|
|
|
|
ao2_unlock(menu);
|
|
ao2_ref(menu, -1);
|
|
return CLI_SUCCESS;
|
|
}
|
|
|
|
static struct ast_cli_entry cli_confbridge_parser[] = {
|
|
AST_CLI_DEFINE(handle_cli_confbridge_show_user_profile, "Show a conference user profile."),
|
|
AST_CLI_DEFINE(handle_cli_confbridge_show_bridge_profile, "Show a conference bridge profile."),
|
|
AST_CLI_DEFINE(handle_cli_confbridge_show_menu, "Show a conference menu"),
|
|
AST_CLI_DEFINE(handle_cli_confbridge_show_user_profiles, "Show a list of conference user profiles."),
|
|
AST_CLI_DEFINE(handle_cli_confbridge_show_bridge_profiles, "Show a list of conference bridge profiles."),
|
|
AST_CLI_DEFINE(handle_cli_confbridge_show_menus, "Show a list of conference menus"),
|
|
|
|
};
|
|
|
|
static int conf_parse_init(void)
|
|
{
|
|
if (!(user_profiles = ao2_container_alloc(283, user_hash_cb, user_cmp_cb))) {
|
|
conf_destroy_config();
|
|
return -1;
|
|
}
|
|
|
|
if (!(bridge_profiles = ao2_container_alloc(283, bridge_hash_cb, bridge_cmp_cb))) {
|
|
conf_destroy_config();
|
|
return -1;
|
|
}
|
|
|
|
if (!(menus = ao2_container_alloc(283, menu_hash_cb, menu_cmp_cb))) {
|
|
conf_destroy_config();
|
|
return -1;
|
|
}
|
|
|
|
ast_cli_register_multiple(cli_confbridge_parser, ARRAY_LEN(cli_confbridge_parser));
|
|
|
|
return 0;
|
|
}
|
|
|
|
void conf_destroy_config()
|
|
{
|
|
if (user_profiles) {
|
|
ao2_ref(user_profiles, -1);
|
|
user_profiles = NULL;
|
|
}
|
|
if (bridge_profiles) {
|
|
ao2_ref(bridge_profiles, -1);
|
|
bridge_profiles = NULL;
|
|
}
|
|
|
|
if (menus) {
|
|
ao2_ref(menus, -1);
|
|
menus = NULL;
|
|
}
|
|
ast_cli_unregister_multiple(cli_confbridge_parser, sizeof(cli_confbridge_parser) / sizeof(struct ast_cli_entry));
|
|
}
|
|
|
|
static void remove_all_delme(void)
|
|
{
|
|
ao2_callback(user_profiles, OBJ_NODATA | OBJ_MULTIPLE | OBJ_UNLINK, match_user_delme_cb, NULL);
|
|
ao2_callback(bridge_profiles, OBJ_NODATA | OBJ_MULTIPLE | OBJ_UNLINK, match_bridge_delme_cb, NULL);
|
|
ao2_callback(menus, OBJ_NODATA | OBJ_MULTIPLE | OBJ_UNLINK, match_menu_delme_cb, NULL);
|
|
}
|
|
|
|
static void mark_all_delme(void)
|
|
{
|
|
ao2_callback(user_profiles, OBJ_NODATA | OBJ_MULTIPLE, user_mark_delme_cb, NULL);
|
|
ao2_callback(bridge_profiles, OBJ_NODATA | OBJ_MULTIPLE, bridge_mark_delme_cb, NULL);
|
|
ao2_callback(menus, OBJ_NODATA | OBJ_MULTIPLE, menu_mark_delme_cb, NULL);
|
|
}
|
|
|
|
int conf_load_config(int reload)
|
|
{
|
|
struct ast_flags config_flags = { 0, };
|
|
struct ast_config *cfg = ast_config_load(CONFBRIDGE_CONFIG, config_flags);
|
|
const char *type = NULL;
|
|
char *cat = NULL;
|
|
|
|
if (!reload) {
|
|
conf_parse_init();
|
|
}
|
|
|
|
if (!cfg || cfg == CONFIG_STATUS_FILEMISSING || cfg == CONFIG_STATUS_FILEINVALID) {
|
|
return 0;
|
|
}
|
|
|
|
mark_all_delme();
|
|
|
|
while ((cat = ast_category_browse(cfg, cat))) {
|
|
if (!(type = (ast_variable_retrieve(cfg, cat, "type")))) {
|
|
if (strcasecmp(cat, "general")) {
|
|
ast_log(LOG_WARNING, "Section '%s' lacks type\n", cat);
|
|
}
|
|
continue;
|
|
}
|
|
if (!strcasecmp(type, "bridge")) {
|
|
parse_bridge(cat, cfg);
|
|
} else if (!strcasecmp(type, "user")) {
|
|
parse_user(cat, cfg);
|
|
} else if (!strcasecmp(type, "menu")) {
|
|
parse_menu(cat, cfg);
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
remove_all_delme();
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void conf_user_profile_copy(struct user_profile *dst, struct user_profile *src)
|
|
{
|
|
memcpy(dst, src, sizeof(*dst));
|
|
}
|
|
|
|
const struct user_profile *conf_find_user_profile(struct ast_channel *chan, const char *user_profile_name, struct user_profile *result)
|
|
{
|
|
struct user_profile tmp;
|
|
struct user_profile *tmp2;
|
|
struct ast_datastore *datastore = NULL;
|
|
struct func_confbridge_data *b_data = NULL;
|
|
ast_copy_string(tmp.name, user_profile_name, sizeof(tmp.name));
|
|
|
|
if (chan) {
|
|
ast_channel_lock(chan);
|
|
if ((datastore = ast_channel_datastore_find(chan, &confbridge_datastore, NULL))) {
|
|
ast_channel_unlock(chan);
|
|
b_data = datastore->data;
|
|
if (b_data->u_usable) {
|
|
conf_user_profile_copy(result, &b_data->u_profile);
|
|
return result;
|
|
}
|
|
}
|
|
ast_channel_unlock(chan);
|
|
}
|
|
|
|
if (ast_strlen_zero(user_profile_name)) {
|
|
user_profile_name = DEFAULT_USER_PROFILE;
|
|
}
|
|
if (!(tmp2 = ao2_find(user_profiles, &tmp, OBJ_POINTER))) {
|
|
return NULL;
|
|
}
|
|
ao2_lock(tmp2);
|
|
conf_user_profile_copy(result, tmp2);
|
|
ao2_unlock(tmp2);
|
|
ao2_ref(tmp2, -1);
|
|
|
|
return result;
|
|
}
|
|
|
|
void conf_bridge_profile_copy(struct bridge_profile *dst, struct bridge_profile *src)
|
|
{
|
|
memcpy(dst, src, sizeof(*dst));
|
|
if (src->sounds) {
|
|
ao2_ref(src->sounds, +1);
|
|
}
|
|
}
|
|
|
|
void conf_bridge_profile_destroy(struct bridge_profile *b_profile)
|
|
{
|
|
if (b_profile->sounds) {
|
|
ao2_ref(b_profile->sounds, -1);
|
|
b_profile->sounds = NULL;
|
|
}
|
|
}
|
|
|
|
const struct bridge_profile *conf_find_bridge_profile(struct ast_channel *chan, const char *bridge_profile_name, struct bridge_profile *result)
|
|
{
|
|
struct bridge_profile tmp;
|
|
struct bridge_profile *tmp2;
|
|
struct ast_datastore *datastore = NULL;
|
|
struct func_confbridge_data *b_data = NULL;
|
|
|
|
if (chan) {
|
|
ast_channel_lock(chan);
|
|
if ((datastore = ast_channel_datastore_find(chan, &confbridge_datastore, NULL))) {
|
|
ast_channel_unlock(chan);
|
|
b_data = datastore->data;
|
|
if (b_data->b_usable) {
|
|
conf_bridge_profile_copy(result, &b_data->b_profile);
|
|
return result;
|
|
}
|
|
} else {
|
|
ast_channel_unlock(chan);
|
|
}
|
|
}
|
|
if (ast_strlen_zero(bridge_profile_name)) {
|
|
bridge_profile_name = DEFAULT_BRIDGE_PROFILE;
|
|
}
|
|
ast_copy_string(tmp.name, bridge_profile_name, sizeof(tmp.name));
|
|
if (!(tmp2 = ao2_find(bridge_profiles, &tmp, OBJ_POINTER))) {
|
|
return NULL;
|
|
}
|
|
ao2_lock(tmp2);
|
|
conf_bridge_profile_copy(result, tmp2);
|
|
ao2_unlock(tmp2);
|
|
ao2_ref(tmp2, -1);
|
|
|
|
return result;
|
|
}
|
|
|
|
struct dtmf_menu_hook_pvt {
|
|
struct conference_bridge_user *conference_bridge_user;
|
|
struct conf_menu_entry menu_entry;
|
|
struct conf_menu *menu;
|
|
};
|
|
|
|
static void menu_hook_destroy(void *hook_pvt)
|
|
{
|
|
struct dtmf_menu_hook_pvt *pvt = hook_pvt;
|
|
struct conf_menu_action *action = NULL;
|
|
|
|
ao2_ref(pvt->menu, -1);
|
|
|
|
while ((action = AST_LIST_REMOVE_HEAD(&pvt->menu_entry.actions, action))) {
|
|
ast_free(action);
|
|
}
|
|
ast_free(pvt);
|
|
}
|
|
|
|
static int menu_hook_callback(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt)
|
|
{
|
|
struct dtmf_menu_hook_pvt *pvt = hook_pvt;
|
|
return conf_handle_dtmf(bridge_channel, pvt->conference_bridge_user, &pvt->menu_entry, pvt->menu);
|
|
}
|
|
|
|
static int copy_menu_entry(struct conf_menu_entry *dst, struct conf_menu_entry *src)
|
|
{
|
|
struct conf_menu_action *menu_action = NULL;
|
|
struct conf_menu_action *new_menu_action = NULL;
|
|
|
|
memcpy(dst, src, sizeof(*dst));
|
|
AST_LIST_HEAD_INIT_NOLOCK(&dst->actions);
|
|
|
|
AST_LIST_TRAVERSE(&src->actions, menu_action, action) {
|
|
if (!(new_menu_action = ast_calloc(1, sizeof(*new_menu_action)))) {
|
|
return -1;
|
|
}
|
|
memcpy(new_menu_action, menu_action, sizeof(*new_menu_action));
|
|
AST_LIST_INSERT_HEAD(&dst->actions, new_menu_action, action);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void conf_menu_entry_destroy(struct conf_menu_entry *menu_entry)
|
|
{
|
|
struct conf_menu_action *menu_action = NULL;
|
|
while ((menu_action = AST_LIST_REMOVE_HEAD(&menu_entry->actions, action))) {
|
|
ast_free(menu_action);
|
|
}
|
|
}
|
|
|
|
int conf_find_menu_entry_by_sequence(const char *dtmf_sequence, struct conf_menu *menu, struct conf_menu_entry *result)
|
|
{
|
|
struct conf_menu_entry *menu_entry = NULL;
|
|
|
|
ao2_lock(menu);
|
|
AST_LIST_TRAVERSE(&menu->entries, menu_entry, entry) {
|
|
if (!strcasecmp(menu_entry->dtmf, dtmf_sequence)) {
|
|
copy_menu_entry(result, menu_entry);
|
|
ao2_unlock(menu);
|
|
return 1;
|
|
}
|
|
}
|
|
ao2_unlock(menu);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int conf_set_menu_to_user(const char *menu_name, struct conference_bridge_user *conference_bridge_user)
|
|
{
|
|
struct conf_menu tmp;
|
|
struct conf_menu *menu;
|
|
struct conf_menu_entry *menu_entry = NULL;
|
|
ast_copy_string(tmp.name, menu_name, sizeof(tmp.name));
|
|
|
|
if (!(menu = ao2_find(menus, &tmp, OBJ_POINTER))) {
|
|
return -1;
|
|
}
|
|
ao2_lock(menu);
|
|
AST_LIST_TRAVERSE(&menu->entries, menu_entry, entry) {
|
|
struct dtmf_menu_hook_pvt *pvt;
|
|
if (!(pvt = ast_calloc(1, sizeof(*pvt)))) {
|
|
ao2_unlock(menu);
|
|
ao2_ref(menu, -1);
|
|
return -1;
|
|
}
|
|
if (copy_menu_entry(&pvt->menu_entry, menu_entry)) {
|
|
ast_free(pvt);
|
|
ao2_unlock(menu);
|
|
ao2_ref(menu, -1);
|
|
return -1;
|
|
}
|
|
pvt->conference_bridge_user = conference_bridge_user;
|
|
ao2_ref(menu, +1);
|
|
pvt->menu = menu;
|
|
|
|
ast_bridge_features_hook(&conference_bridge_user->features, pvt->menu_entry.dtmf, menu_hook_callback, pvt, menu_hook_destroy);
|
|
}
|
|
|
|
ao2_unlock(menu);
|
|
ao2_ref(menu, -1);
|
|
|
|
return 0;
|
|
}
|