/*
 * Asterisk -- A telephony toolkit for Linux.
 *
 * Meet me conference bridge
 * 
 * Copyright (C) 1999, Mark Spencer
 *
 * Mark Spencer <markster@linux-support.net>
 *
 * This program is free software, distributed under the terms of
 * the GNU General Public License
 */

#include <asterisk/lock.h>
#include <asterisk/file.h>
#include <asterisk/logger.h>
#include <asterisk/channel.h>
#include <asterisk/pbx.h>
#include <asterisk/module.h>
#include <asterisk/config.h>
#include <asterisk/app.h>
#include <asterisk/musiconhold.h>
#include <asterisk/options.h>
#include <asterisk/cli.h>
#include <asterisk/say.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/ioctl.h>

#include <pthread.h>
#include <linux/zaptel.h>

static char *tdesc = "Simple MeetMe conference bridge";

static char *app = "MeetMe";
static char *app2 = "MeetMeCount";

static char *synopsis = "Simple MeetMe conference bridge";
static char *synopsis2 = "MeetMe participant count";

static char *descrip =
"  MeetMe(confno[|options]): Enters the user into a specified MeetMe conference.\n"
"If the conference number is omitted, the user will be prompted to enter\n"
"one.  This application always returns -1.  A ZAPTEL INTERFACE MUST BE INSTALLED\n"
"FOR CONFERENCING TO WORK!\n\n"

"The option string may contain zero or more of the following characters:\n"
"      'm' -- set monitor only mode (Listen only, no talking\n"
"      't' -- set talk only mode. (Talk only, no listening)\n"
"      'p' -- allow user to exit the conference by pressing '#'\n"
"      'd' -- dynamically add conference\n"
"      'v' -- video mode\n"
"      'q' -- quiet mode (don't play enter/leave sounds)\n"
"      'M' -- enable music on hold when the conference has a single caller\n"
"      'b' -- run AGI script specified in ${MEETME_AGI_BACKGROUND}\n"
"	      Default: conf-background.agi\n"
"             (Note: This does not work with non-Zap channels in the same conference)\n"
"      Not implemented yet:\n"
"      's' -- send user to admin/user menu if '*' is received\n"
"      'a' -- set admin mode\n";

static char *descrip2 =
"  MeetMeCount(confno[|var]): Plays back the number of users in the specifiedi\n"
"MeetMe conference. If var is specified, playback will be skipped and the value\n"
"will be returned in the variable. Returns 0 on success or -1 on a hangup.\n"
"A ZAPTEL INTERFACE MUST BE INSTALLED FOR CONFERENCING FUNCTIONALITY.\n";

STANDARD_LOCAL_USER;

LOCAL_USER_DECL;

static struct ast_conference {
	char confno[AST_MAX_EXTENSION];		/* Conference */
	int fd;				/* Announcements fd */
	int zapconf;			/* Zaptel Conf # */
	int users;			/* Number of active users */
	time_t start;			/* Start time (s) */
	int isdynamic;			/* Created on the fly? */
	char pin[AST_MAX_EXTENSION];			/* If protected by a PIN */
	struct ast_conference *next;
} *confs;

static ast_mutex_t conflock = AST_MUTEX_INITIALIZER;

#include "enter.h"
#include "leave.h"

#define ENTER	0
#define LEAVE	1

#define CONF_SIZE 160

#define CONFFLAG_ADMIN	(1 << 1)	/* If set the user has admin access on the conference */
#define CONFFLAG_MONITOR (1 << 2)	/* If set the user can only receive audio from the conference */
#define CONFFLAG_POUNDEXIT (1 << 3)	/* If set asterisk will exit conference when '#' is pressed */
#define CONFFLAG_STARMENU (1 << 4)	/* If set asterisk will provide a menu to the user what '*' is pressed */
#define CONFFLAG_TALKER (1 << 5)	/* If set the use can only send audio to the conference */
#define CONFFLAG_QUIET (1 << 6)		/* If set there will be no enter or leave sounds */
#define CONFFLAG_VIDEO (1 << 7)		/* Set to enable video mode */
#define CONFFLAG_AGI (1 << 8)		/* Set to run AGI Script in Background */
#define CONFFLAG_MOH (1 << 9)		/* Set to have music on hold when */


static int careful_write(int fd, unsigned char *data, int len)
{
	int res;
	while(len) {
		res = write(fd, data, len);
		if (res < 1) {
			if (errno != EAGAIN) {
				ast_log(LOG_WARNING, "Failed to write audio data to conference: %s\n", strerror(errno));
				return -1;
			} else
				return 0;
		}
		len -= res;
		data += res;
	}
	return 0;
}

static void conf_play(struct ast_conference *conf, int sound)
{
	unsigned char *data;
	int len;
	ast_mutex_lock(&conflock);
	switch(sound) {
	case ENTER:
		data = enter;
		len = sizeof(enter);
		break;
	case LEAVE:
		data = leave;
		len = sizeof(leave);
		break;
	default:
		data = NULL;
		len = 0;
	}
	if (data) 
		careful_write(conf->fd, data, len);
	ast_mutex_unlock(&conflock);
}

static struct ast_conference *build_conf(char *confno, char *pin, int make, int dynamic)
{
	struct ast_conference *cnf;
	struct zt_confinfo ztc;
	ast_mutex_lock(&conflock);
	cnf = confs;
	while(cnf) {
		if (!strcmp(confno, cnf->confno)) 
			break;
		cnf = cnf->next;
	}
	if (!cnf && (make || dynamic)) {
		cnf = malloc(sizeof(struct ast_conference));
		if (cnf) {
			/* Make a new one */
			memset(cnf, 0, sizeof(struct ast_conference));
			strncpy(cnf->confno, confno, sizeof(cnf->confno) - 1);
			strncpy(cnf->pin, pin, sizeof(cnf->pin) - 1);
			cnf->fd = open("/dev/zap/pseudo", O_RDWR);
			if (cnf->fd < 0) {
				ast_log(LOG_WARNING, "Unable to open pseudo channel\n");
				free(cnf);
				cnf = NULL;
				goto cnfout;
			}
			memset(&ztc, 0, sizeof(ztc));
			/* Setup a new zap conference */
			ztc.chan = 0;
			ztc.confno = -1;
			ztc.confmode = ZT_CONF_CONF | ZT_CONF_TALKER | ZT_CONF_LISTENER;
			if (ioctl(cnf->fd, ZT_SETCONF, &ztc)) {
				ast_log(LOG_WARNING, "Error setting conference\n");
				close(cnf->fd);
				free(cnf);
				cnf = NULL;
				goto cnfout;
			}
			cnf->start = time(NULL);
			cnf->zapconf = ztc.confno;
			cnf->isdynamic = dynamic;
			if (option_verbose > 2)
				ast_verbose(VERBOSE_PREFIX_3 "Created ZapTel conference %d for conference '%s'\n", cnf->zapconf, cnf->confno);
			cnf->next = confs;
			confs = cnf;
		} else	
			ast_log(LOG_WARNING, "Out of memory\n");
	}
cnfout:
	ast_mutex_unlock(&conflock);
	return cnf;
}

static int confs_show(int fd, int argc, char **argv)
{
	struct ast_conference *conf;
	int hr, min, sec;
	time_t now;
	char *header_format = "%14s %-14s %-8s  %-8s\n";
	char *data_format = "%-12.12s   %4.4d          %02d:%02d:%02d  %-8s\n";

	now = time(NULL);
	if (argc != 2)
		return RESULT_SHOWUSAGE;
	conf = confs;
	if (!conf) {
		ast_cli(fd, "No active conferences.\n");
		return RESULT_SUCCESS;
	}
	ast_cli(fd, header_format, "Conf Num", "Parties", "Activity", "Creation");
	while(conf) {
		hr = (now - conf->start) / 3600;
		min = ((now - conf->start) % 3600) / 60;
		sec = (now - conf->start) % 60;

		if (conf->isdynamic)
			ast_cli(fd, data_format, conf->confno, conf->users, hr, min, sec, "Dynamic");
		else
			ast_cli(fd, data_format, conf->confno, conf->users, hr, min, sec, "Static");

		conf = conf->next;
	}
	return RESULT_SUCCESS;
}

static char show_confs_usage[] = 
"Usage: show conferences\n"
"       Provides summary information on conferences with active\n"
"       participation.\n";

static struct ast_cli_entry cli_show_confs = {
	{ "show", "conferences", NULL }, confs_show, 
	"Show status of conferences", show_confs_usage, NULL };

static int conf_run(struct ast_channel *chan, struct ast_conference *conf, int confflags)
{
	struct ast_conference *prev=NULL, *cur;
	int fd;
	struct zt_confinfo ztc;
	struct ast_frame *f;
	struct ast_channel *c;
	struct ast_frame fr;
	int outfd;
	int ms;
	int nfds;
	int res;
	int flags;
	int retryzap;
	int origfd;
	int musiconhold = 0;
	int firstpass = 0;
	int ret = -1;
	int x;

	struct ast_app *app;
	char *agifile;
	char *agifiledefault = "conf-background.agi";

	ZT_BUFFERINFO bi;
	char __buf[CONF_SIZE + AST_FRIENDLY_OFFSET];
	char *buf = __buf + AST_FRIENDLY_OFFSET;
	
	conf->users++;
	if (!(confflags & CONFFLAG_QUIET) && conf->users == 1) {
		if (!ast_streamfile(chan, "conf-onlyperson", chan->language))
			ast_waitstream(chan, "");
	}

	if (confflags & CONFFLAG_VIDEO) {	
		/* Set it into linear mode (write) */
		if (ast_set_write_format(chan, AST_FORMAT_SLINEAR) < 0) {
			ast_log(LOG_WARNING, "Unable to set '%s' to write linear mode\n", chan->name);
			goto outrun;
		}

		/* Set it into linear mode (read) */
		if (ast_set_read_format(chan, AST_FORMAT_SLINEAR) < 0) {
			ast_log(LOG_WARNING, "Unable to set '%s' to read linear mode\n", chan->name);
			goto outrun;
		}
	} else {
		/* Set it into U-law mode (write) */
		if (ast_set_write_format(chan, AST_FORMAT_ULAW) < 0) {
			ast_log(LOG_WARNING, "Unable to set '%s' to write ulaw mode\n", chan->name);
			goto outrun;
		}

		/* Set it into U-law mode (read) */
		if (ast_set_read_format(chan, AST_FORMAT_ULAW) < 0) {
			ast_log(LOG_WARNING, "Unable to set '%s' to read ulaw mode\n", chan->name);
			goto outrun;
		}
	}
	ast_indicate(chan, -1);
	retryzap = strcasecmp(chan->type, "Zap");
zapretry:
	origfd = chan->fds[0];
	if (retryzap) {
		fd = open("/dev/zap/pseudo", O_RDWR);
		if (fd < 0) {
			ast_log(LOG_WARNING, "Unable to open pseudo channel: %s\n", strerror(errno));
			goto outrun;
		}
		/* Make non-blocking */
		flags = fcntl(fd, F_GETFL);
		if (flags < 0) {
			ast_log(LOG_WARNING, "Unable to get flags: %s\n", strerror(errno));
			close(fd);
			goto outrun;
		}
		if (fcntl(fd, F_SETFL, flags | O_NONBLOCK)) {
			ast_log(LOG_WARNING, "Unable to set flags: %s\n", strerror(errno));
			close(fd);
			goto outrun;
		}
		/* Setup buffering information */
		memset(&bi, 0, sizeof(bi));
		bi.bufsize = CONF_SIZE;
		bi.txbufpolicy = ZT_POLICY_IMMEDIATE;
		bi.rxbufpolicy = ZT_POLICY_IMMEDIATE;
		bi.numbufs = 4;
		if (ioctl(fd, ZT_SET_BUFINFO, &bi)) {
			ast_log(LOG_WARNING, "Unable to set buffering information: %s\n", strerror(errno));
			close(fd);
			goto outrun;
		}
		if (confflags & CONFFLAG_VIDEO) {	
			x = 1;
			if (ioctl(fd, ZT_SETLINEAR, &x)) {
				ast_log(LOG_WARNING, "Unable to set linear mode: %s\n", strerror(errno));
				close(fd);
				goto outrun;
			}
		}
		nfds = 1;
	} else {
		/* XXX Make sure we're not running on a pseudo channel XXX */
		fd = chan->fds[0];
		nfds = 0;
	}
	memset(&ztc, 0, sizeof(ztc));
	/* Check to see if we're in a conference... */
	ztc.chan = 0;	
	if (ioctl(fd, ZT_GETCONF, &ztc)) {
		ast_log(LOG_WARNING, "Error getting conference\n");
		close(fd);
		goto outrun;
	}
	if (ztc.confmode) {
		/* Whoa, already in a conference...  Retry... */
		if (!retryzap) {
			ast_log(LOG_DEBUG, "Zap channel is in a conference already, retrying with pseudo\n");
			retryzap = 1;
			goto zapretry;
		}
	}
	memset(&ztc, 0, sizeof(ztc));
	/* Add us to the conference */
	ztc.chan = 0;	
	ztc.confno = conf->zapconf;
	if (confflags & CONFFLAG_MONITOR)
		ztc.confmode = ZT_CONF_CONFMON | ZT_CONF_LISTENER;
	else if (confflags & CONFFLAG_TALKER)
		ztc.confmode = ZT_CONF_CONF | ZT_CONF_TALKER;
	else 
		ztc.confmode = ZT_CONF_CONF | ZT_CONF_TALKER | ZT_CONF_LISTENER;

	if (ioctl(fd, ZT_SETCONF, &ztc)) {
		ast_log(LOG_WARNING, "Error setting conference\n");
		close(fd);
		goto outrun;
	}
	ast_log(LOG_DEBUG, "Placed channel %s in ZAP conf %d\n", chan->name, conf->zapconf);
	if (!firstpass && !(confflags & CONFFLAG_MONITOR) && !(confflags & CONFFLAG_ADMIN)) {
		firstpass = 1;
		if (!(confflags & CONFFLAG_QUIET))
			conf_play(conf, ENTER);
	}

	if (confflags & CONFFLAG_AGI) {

		/* Get name of AGI file to run from $(MEETME_AGI_BACKGROUND)
		  or use default filename of conf-background.agi */

		agifile = pbx_builtin_getvar_helper(chan,"MEETME_AGI_BACKGROUND");
		if (!agifile)
			agifile = agifiledefault;

		if (!strcasecmp(chan->type,"Zap")) {
			/*  Set CONFMUTE mode on Zap channel to mute DTMF tones */
			x = 1;
			ast_channel_setoption(chan,AST_OPTION_TONE_VERIFY,&x,sizeof(char),0);
		}
		/* Find a pointer to the agi app and execute the script */
		app = pbx_findapp("agi");
		if (app) {
			ret = pbx_exec(chan, app, agifile, 1);
		} else {
			ast_log(LOG_WARNING, "Could not find application (agi)\n");
			ret = -2;
		}
                if (!strcasecmp(chan->type,"Zap")) {
                        /*  Remove CONFMUTE mode on Zap channel */
			x = 0;
                        ast_channel_setoption(chan,AST_OPTION_TONE_VERIFY,&x,sizeof(char),0);
                }
	} else for(;;) {
		outfd = -1;
		ms = -1;
		c = ast_waitfor_nandfds(&chan, 1, &fd, nfds, NULL, &outfd, &ms);
		/* trying to add moh for single person conf */
		if (confflags & CONFFLAG_MOH) {
			if (conf->users == 1) {
				if (musiconhold == 0) {
					ast_moh_start(chan, NULL);
					musiconhold = 1;
				} 
			} else {
				if (musiconhold) {
					ast_moh_stop(chan);
					musiconhold = 0;
				}
			}
		}
		/* end modifications */

		if (c) {
			if (c->fds[0] != origfd) {
				if (retryzap) {
					/* Kill old pseudo */
					close(fd);
				}
				ast_log(LOG_DEBUG, "Ooh, something swapped out under us, starting over\n");
				retryzap = 0;
				goto zapretry;
			}
			f = ast_read(c);
			if (!f) 
				break;
			if ((f->frametype == AST_FRAME_DTMF) && (f->subclass == '#') && (confflags & CONFFLAG_POUNDEXIT)) {
				ret = 0;
				break;
			} else if ((f->frametype == AST_FRAME_DTMF) && (f->subclass == '*') && (confflags & CONFFLAG_STARMENU)) {
					if ((confflags & CONFFLAG_ADMIN)) {
					/* Do admin stuff here */
					} else {
					/* Do user menu here */
					}

			} else if (fd != chan->fds[0]) {
				if (f->frametype == AST_FRAME_VOICE) {
					if (f->subclass == AST_FORMAT_ULAW) {
						/* Carefully write */
						careful_write(fd, f->data, f->datalen);
					} else
						ast_log(LOG_WARNING, "Huh?  Got a non-ulaw (%d) frame in the conference\n", f->subclass);
				}
			}
			ast_frfree(f);
		} else if (outfd > -1) {
			res = read(outfd, buf, CONF_SIZE);
			if (res > 0) {
				memset(&fr, 0, sizeof(fr));
				fr.frametype = AST_FRAME_VOICE;
				fr.subclass = AST_FORMAT_ULAW;
				fr.datalen = res;
				fr.samples = res;
				fr.data = buf;
				fr.offset = AST_FRIENDLY_OFFSET;
				if (ast_write(chan, &fr) < 0) {
					ast_log(LOG_WARNING, "Unable to write frame to channel: %s\n", strerror(errno));
					/* break; */
				}
			} else 
				ast_log(LOG_WARNING, "Failed to read frame: %s\n", strerror(errno));
		}
	}

	if (fd != chan->fds[0])
		close(fd);
	else {
		/* Take out of conference */
		/* Add us to the conference */
		ztc.chan = 0;	
		ztc.confno = 0;
		ztc.confmode = 0;
		if (ioctl(fd, ZT_SETCONF, &ztc)) {
			ast_log(LOG_WARNING, "Error setting conference\n");
		}
	}

	if (!(confflags & CONFFLAG_QUIET) && !(confflags & CONFFLAG_MONITOR) && !(confflags & CONFFLAG_ADMIN))
		conf_play(conf, LEAVE);

outrun:

	ast_mutex_lock(&conflock);
	/* Clean up */
	conf->users--;
	if (!conf->users) {
		/* No more users -- close this one out */
		cur = confs;
		while(cur) {
			if (cur == conf) {
				if (prev)
					prev->next = conf->next;
				else
					confs = conf->next;
				break;
			}
			prev = cur;
			cur = cur->next;
		}
		if (!cur) 
			ast_log(LOG_WARNING, "Conference not found\n");
		close(conf->fd);
		free(conf);
	}
	ast_mutex_unlock(&conflock);
	return ret;
}

static struct ast_conference *find_conf(char *confno, int make, int dynamic)
{
	struct ast_config *cfg;
	struct ast_variable *var;
	struct ast_conference *cnf = confs;

	/* Check first in the conference list */
	ast_mutex_lock(&conflock);
	while (cnf) {
		if (!strcmp(confno, cnf->confno)) 
			break;
		cnf = cnf->next;
	}
	ast_mutex_unlock(&conflock);

	if (!cnf) {
		if (dynamic) {
			/* No need to parse meetme.conf */
			ast_log(LOG_DEBUG, "Using dynamic conference '%s'\n", confno);
			cnf = build_conf(confno, "", make, dynamic);
		} else {
			/* Check the config */
			cfg = ast_load("meetme.conf");
			if (!cfg) {
				ast_log(LOG_WARNING, "No meetme.conf file :(\n");
				return NULL;
			}
			var = ast_variable_browse(cfg, "rooms");
			while(var) {
				if (!strcasecmp(var->name, "conf")) {
					/* Separate the PIN */
					char *pin, *conf;

					if ((pin = ast_strdupa(var->value))) {
						conf = strsep(&pin, "|,");
						if (!strcasecmp(conf, confno)) {
							/* Bingo it's a valid conference */
							if (pin)
								cnf = build_conf(confno, pin, make, dynamic);
							else
								cnf = build_conf(confno, "", make, dynamic);
							break;
						}
					}
				}
				var = var->next;
			}
			if (!var) {
				ast_log(LOG_DEBUG, "%s isn't a valid conference\n", confno);
			}
			ast_destroy(cfg);
		}
	}
	return cnf;
}

static int count_exec(struct ast_channel *chan, void *data)
{
	struct localuser *u;
	int res = 0;
	struct ast_conference *conf;
	int count;
	char *confnum, *localdata;
	char val[80] = "0"; 

	if (!data || !strlen(data)) {
		ast_log(LOG_WARNING, "MeetMeCount requires an argument (conference number)\n");
		return -1;
	}
	localdata = ast_strdupa(data);
	LOCAL_USER_ADD(u);
	confnum = strsep(&localdata,"|");       
	conf = find_conf(confnum, 0, 0);
	if (conf)
		count = conf->users;
	else
		count = 0;

	if (localdata && strlen(localdata)){
		/* have var so load it and exit */
		snprintf(val,sizeof(val), "%i",count);
		pbx_builtin_setvar_helper(chan, localdata,val);
	} else {
		if (chan->_state != AST_STATE_UP)
			ast_answer(chan);
		res = ast_say_number(chan, count, "", chan->language);
	}
	LOCAL_USER_REMOVE(u);
	return res;
}

static int conf_exec(struct ast_channel *chan, void *data)
{
	int res=-1;
	struct localuser *u;
	char confno[AST_MAX_EXTENSION] = "";
	int allowretry = 0;
	int retrycnt = 0;
	struct ast_conference *cnf;
	int confflags = 0;
	int dynamic = 0;
	char *notdata, *info, *inflags = NULL, *inpin = NULL;

	if (!data || !strlen(data)) {
		allowretry = 1;
		notdata = "";
	} else {
		notdata = data;
	}
	LOCAL_USER_ADD(u);
	if (chan->_state != AST_STATE_UP)
		ast_answer(chan);

	info = ast_strdupa((char *)notdata);

	if (info) {
		char *tmp = strsep(&info, "|");
		strncpy(confno, tmp, sizeof(confno));
		if (strlen(confno) == 0) {
			allowretry = 1;
		}
	}
	if (info)
		inflags = strsep(&info, "|");
	if (info)
		inpin = strsep(&info, "|");

	if (inflags) {
		if (strchr(inflags, 'a'))
			confflags |= CONFFLAG_ADMIN;
		if (strchr(inflags, 'm'))
			confflags |= CONFFLAG_MONITOR;
		if (strchr(inflags, 'p'))
			confflags |= CONFFLAG_POUNDEXIT;
		if (strchr(inflags, 's'))
			confflags |= CONFFLAG_STARMENU;
		if (strchr(inflags, 't'))
			confflags |= CONFFLAG_TALKER;
		if (strchr(inflags, 'q'))
			confflags |= CONFFLAG_QUIET;
		if (strchr(inflags, 'M'))
			confflags |= CONFFLAG_MOH;
		if (strchr(inflags, 'b'))
			confflags |= CONFFLAG_AGI;
		if (strchr(inflags, 'd'))
			dynamic = 1;
	}

	do {
		if (retrycnt > 3)
			allowretry = 0;
		while (allowretry && (!strlen(confno)) && (++retrycnt < 4)) {
			/* Prompt user for conference number */
			res = ast_app_getdata(chan, "conf-getconfno", confno, sizeof(confno) - 1, 0);
			if (res < 0) {
				/* Don't try to validate when we catch an error */
				strcpy(confno, "");
				allowretry = 0;
				break;
			}
		}
		if (strlen(confno)) {
			/* Check the validity of the conference */
			cnf = find_conf(confno, 1, dynamic);
			if (!cnf) {
				res = ast_streamfile(chan, "conf-invalid", chan->language);
				if (!res)
					ast_waitstream(chan, "");
				res = -1;
				if (allowretry)
					strcpy(confno, "");
			} else {
				if (strlen(cnf->pin)) {
					char pin[AST_MAX_EXTENSION];

					if (inpin && *inpin) {
						strncpy(pin, inpin, sizeof(pin) - 1);
					} else {
						/* Prompt user for pin if pin is required */
						res = ast_app_getdata(chan, "conf-getpin", pin, sizeof(pin) - 1, 0);
					}
					if (res >= 0) {
						if (!strcasecmp(pin, cnf->pin)) {
							/* Pin correct */
							allowretry = 0;
							/* Run the conference */
							res = conf_run(chan, cnf, confflags);
						} else {
							/* Pin invalid */
							res = ast_streamfile(chan, "conf-invalidpin", chan->language);
							if (!res)
								ast_waitstream(chan, "");
							res = -1;
							if (allowretry)
								strcpy(confno, "");
						}
					} else {
						res = -1;
						allowretry = 0;
					}
				} else {
					/* No pin required */
					allowretry = 0;

					/* Run the conference */
					res = conf_run(chan, cnf, confflags);
				}
			}
		}
	} while (allowretry);
	/* Do the conference */
	LOCAL_USER_REMOVE(u);
	return res;
}

int unload_module(void)
{
	STANDARD_HANGUP_LOCALUSERS;
	ast_cli_unregister(&cli_show_confs);
	ast_unregister_application(app2);
	return ast_unregister_application(app);
}

int load_module(void)
{
	ast_cli_register(&cli_show_confs);
	ast_register_application(app2, count_exec, synopsis2, descrip2);
	return ast_register_application(app, conf_exec, synopsis, descrip);
}

char *description(void)
{
	return tdesc;
}

int usecount(void)
{
	int res;
	STANDARD_USECOUNT(res);
	return res;
}

char *key()
{
	return ASTERISK_GPL_KEY;
}
