252b43787254c165f0e9ab43a15a4c60dac1267c
[asterisk/asterisk.git] / apps / app_directory.c
1 /*
2  * Asterisk -- A telephony toolkit for Linux.
3  *
4  * Provide a directory of extensions
5  * 
6  * Copyright (C) 1999 - 2005, Digium, Inc.
7  *
8  * Mark Spencer <markster@digium.com>
9  *
10  * This program is free software, distributed under the terms of
11  * the GNU General Public License
12  */
13  
14 #include <string.h>
15 #include <ctype.h>
16 #include <stdlib.h>
17 #include <stdio.h>
18
19 #include "asterisk.h"
20
21 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
22
23 #include "asterisk/lock.h"
24 #include "asterisk/file.h"
25 #include "asterisk/logger.h"
26 #include "asterisk/channel.h"
27 #include "asterisk/pbx.h"
28 #include "asterisk/module.h"
29 #include "asterisk/config.h"
30 #include "asterisk/say.h"
31 #include "asterisk/utils.h"
32
33 static char *tdesc = "Extension Directory";
34 static char *app = "Directory";
35
36 static char *synopsis = "Provide directory of voicemail extensions";
37 static char *descrip =
38 "  Directory(vm-context[|dial-context[|options]]): Presents the user with a directory\n"
39 "of extensions from which they  may  select  by name. The  list  of  names \n"
40 "and  extensions  is discovered from  voicemail.conf. The  vm-context  argument\n"
41 "is required, and specifies  the  context  of voicemail.conf to use.  The\n"
42 "dial-context is the context to use for dialing the users, and defaults to\n"
43 "the vm-context if unspecified. The 'f' option causes the directory to match\n"
44 "based on the first name in voicemail.conf instead of the last name.\n"
45 "Returns 0 unless the user hangs up. It  also sets up the channel on exit\n"
46 "to enter the extension the user selected.  If the user enters '0' and there\n"
47 "exists an extension 'o' in the current context, the directory will exit with 0\n"
48 "and call control will resume at that extension.  Entering '*' will exit similarly,\n"
49 "but to the 'a' extension, much like app_voicemail's behavior.\n";
50
51 /* For simplicity, I'm keeping the format compatible with the voicemail config,
52    but i'm open to suggestions for isolating it */
53
54 #define VOICEMAIL_CONFIG "voicemail.conf"
55
56 /* How many digits to read in */
57 #define NUMDIGITS 3
58
59 STANDARD_LOCAL_USER;
60
61 LOCAL_USER_DECL;
62
63 static char *convert(char *lastname)
64 {
65         char *tmp;
66         int lcount = 0;
67         tmp = malloc(NUMDIGITS + 1);
68         if (tmp) {
69                 while((*lastname > 32) && lcount < NUMDIGITS) {
70                         switch(toupper(*lastname)) {
71                         case '1':
72                                 tmp[lcount++] = '1';
73                                 break;
74                         case '2':
75                         case 'A':
76                         case 'B':
77                         case 'C':
78                                 tmp[lcount++] = '2';
79                                 break;
80                         case '3':
81                         case 'D':
82                         case 'E':
83                         case 'F':
84                                 tmp[lcount++] = '3';
85                                 break;
86                         case '4':
87                         case 'G':
88                         case 'H':
89                         case 'I':
90                                 tmp[lcount++] = '4';
91                                 break;
92                         case '5':
93                         case 'J':
94                         case 'K':
95                         case 'L':
96                                 tmp[lcount++] = '5';
97                                 break;
98                         case '6':
99                         case 'M':
100                         case 'N':
101                         case 'O':
102                                 tmp[lcount++] = '6';
103                                 break;
104                         case '7':
105                         case 'P':
106                         case 'Q':
107                         case 'R':
108                         case 'S':
109                                 tmp[lcount++] = '7';
110                                 break;
111                         case '8':
112                         case 'T':
113                         case 'U':
114                         case 'V':
115                                 tmp[lcount++] = '8';
116                                 break;
117                         case '9':
118                         case 'W':
119                         case 'X':
120                         case 'Y':
121                         case 'Z':
122                                 tmp[lcount++] = '9';
123                                 break;
124                         }
125                         lastname++;
126                 }
127                 tmp[lcount] = '\0';
128         }
129         return tmp;
130 }
131
132 /* play name of mailbox owner.
133  * returns:  -1 for bad or missing extension
134  *           '1' for selected entry from directory
135  *           '*' for skipped entry from directory
136  */
137 static int play_mailbox_owner(struct ast_channel *chan, char *context, char *dialcontext, char *ext, char *name) {
138         int res = 0;
139         int loop = 3;
140         char fn[256];
141         char fn2[256];
142
143         /* Check for the VoiceMail2 greeting first */
144         snprintf(fn, sizeof(fn), "%s/voicemail/%s/%s/greet",
145                 (char *)ast_config_AST_SPOOL_DIR, context, ext);
146
147         /* Otherwise, check for an old-style Voicemail greeting */
148         snprintf(fn2, sizeof(fn2), "%s/vm/%s/greet",
149                 (char *)ast_config_AST_SPOOL_DIR, ext);
150
151         if (ast_fileexists(fn, NULL, chan->language) > 0) {
152                 res = ast_streamfile(chan, fn, chan->language);
153                 if (!res) {
154                         res = ast_waitstream(chan, AST_DIGIT_ANY);
155                 }
156                 ast_stopstream(chan);
157         } else if (ast_fileexists(fn2, NULL, chan->language) > 0) {
158                 res = ast_streamfile(chan, fn2, chan->language);
159                 if (!res) {
160                         res = ast_waitstream(chan, AST_DIGIT_ANY);
161                 }
162                 ast_stopstream(chan);
163         } else {
164                 res = ast_say_character_str(chan, !ast_strlen_zero(name) ? name : ext,
165                                         AST_DIGIT_ANY, chan->language);
166         }
167
168         while (loop) {
169                 if (!res) {
170                         res = ast_streamfile(chan, "dir-instr", chan->language);
171                 }
172                 if (!res) {
173                         res = ast_waitstream(chan, AST_DIGIT_ANY);
174                 }
175                 if (!res) {
176                         res = ast_waitfordigit(chan, 3000);
177                 }
178                 ast_stopstream(chan);
179         
180                 if (res > -1) {
181                         switch (res) {
182                                 case '1':
183                                         /* Name selected */
184                                         loop = 0;
185                                         if (ast_exists_extension(chan,dialcontext,ext,1,chan->cid.cid_num)) {
186                                                 ast_copy_string(chan->exten, ext, sizeof(chan->exten));
187                                                 chan->priority = 0;
188                                                 ast_copy_string(chan->context, dialcontext, sizeof(chan->context));
189                                         } else {
190                                                 ast_log(LOG_WARNING,
191                                                         "Can't find extension '%s' in context '%s'.  "
192                                                         "Did you pass the wrong context to Directory?\n",
193                                                         ext, dialcontext);
194                                                 res = -1;
195                                         }
196                                         break;
197         
198                                 case '*':   
199                                         /* Skip to next match in list */
200                                         loop = 0;
201                                         break;
202         
203                                 default:
204                                         /* Not '1', or '*', so decrement number of tries */
205                                         res = 0;
206                                         loop--;
207                                         break;
208                         } /* end switch */
209                 } /* end if */
210                 else {
211                         /* User hungup, so jump out now */
212                         loop = 0;
213                 }
214         } /* end while */
215
216         return(res);
217 }
218
219 static struct ast_config *realtime_directory(char *context)
220 {
221         struct ast_config *cfg;
222         struct ast_config *rtdata;
223         struct ast_category *cat;
224         struct ast_variable *var;
225         char *mailbox;
226         char *fullname;
227         char *hidefromdir;
228         char tmp[100];
229
230         /* Load flat file config. */
231         cfg = ast_config_load(VOICEMAIL_CONFIG);
232
233         if (!cfg) {
234                 /* Loading config failed. */
235                 ast_log(LOG_WARNING, "Loading config failed.\n");
236                 return NULL;
237         }
238
239         /* Get realtime entries, categorized by their mailbox number
240            and present in the requested context */
241         rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, NULL);
242
243         /* if there are no results, just return the entries from the config file */
244         if (!rtdata)
245                 return cfg;
246
247         /* Does the context exist within the config file? If not, make one */
248         cat = ast_category_get(cfg, context);
249         if (!cat) {
250                 cat = ast_category_new(context);
251                 if (!cat) {
252                         ast_log(LOG_WARNING, "Out of memory\n");
253                         ast_config_destroy(cfg);
254                         return NULL;
255                 }
256                 ast_category_append(cfg, cat);
257         }
258
259         mailbox = ast_category_browse(rtdata, NULL);
260         while (mailbox) {
261                 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
262                 hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir");
263                 snprintf(tmp, sizeof(tmp), "no-password,%s,hidefromdir=%s",
264                          fullname ? fullname : "",
265                          hidefromdir ? hidefromdir : "no");
266                 var = ast_variable_new(mailbox, tmp);
267                 if (var)
268                         ast_variable_append(cat, var);
269                 else
270                         ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
271                 mailbox = ast_category_browse(rtdata, mailbox);
272         }
273         ast_config_destroy(rtdata);
274
275         return cfg;
276 }
277
278 static int do_directory(struct ast_channel *chan, struct ast_config *cfg, char *context, char *dialcontext, char digit, int last)
279 {
280         /* Read in the first three digits..  "digit" is the first digit, already read */
281         char ext[NUMDIGITS + 1];
282         char name[80] = "";
283         struct ast_variable *v;
284         int res;
285         int found=0;
286         int lastuserchoice = 0;
287         char *start, *pos, *conv,*stringp=NULL;
288
289         if (!context || ast_strlen_zero(context)) {
290                 ast_log(LOG_WARNING,
291                         "Directory must be called with an argument "
292                         "(context in which to interpret extensions)\n");
293                 return -1;
294         }
295         if (digit == '0') {
296                 if (ast_exists_extension(chan,chan->context,"o",1,chan->cid.cid_num) || 
297                         (!ast_strlen_zero(chan->macrocontext) &&
298                      ast_exists_extension(chan, chan->macrocontext, "o", 1, chan->cid.cid_num))) {
299                         strcpy(chan->exten, "o");
300                         chan->priority = 0;
301                         return 0;
302                 } else {
303
304                         ast_log(LOG_WARNING, "Can't find extension 'o' in current context.  "
305                                 "Not Exiting the Directory!\n");
306                         res = 0;
307                 }
308         }       
309         if (digit == '*') {
310                 if (ast_exists_extension(chan,chan->context,"a",1,chan->cid.cid_num) || 
311                         (!ast_strlen_zero(chan->macrocontext) &&
312                      ast_exists_extension(chan, chan->macrocontext, "a", 1, chan->cid.cid_num))) {
313                         strcpy(chan->exten, "a");
314                         chan->priority = 0;
315                         return 0;
316                 } else {
317
318                         ast_log(LOG_WARNING, "Can't find extension 'a' in current context.  "
319                                 "Not Exiting the Directory!\n");
320                         res = 0;
321                 }
322         }       
323         memset(ext, 0, sizeof(ext));
324         ext[0] = digit;
325         res = 0;
326         if (ast_readstring(chan, ext + 1, NUMDIGITS - 1, 3000, 3000, "#") < 0) res = -1;
327         if (!res) {
328                 /* Search for all names which start with those digits */
329                 v = ast_variable_browse(cfg, context);
330                 while(v && !res) {
331                         /* Find all candidate extensions */
332                         while(v) {
333                                 /* Find a candidate extension */
334                                 start = strdup(v->value);
335                                 if (start && !ast_strcasestr(start, "hidefromdir=yes")) {
336                                         stringp=start;
337                                         strsep(&stringp, ",");
338                                         pos = strsep(&stringp, ",");
339                                         if (pos) {
340                                                 ast_copy_string(name, pos, sizeof(name));
341                                                 /* Grab the last name */
342                                                 if (last && strrchr(pos,' '))
343                                                         pos = strrchr(pos, ' ') + 1;
344                                                 conv = convert(pos);
345                                                 if (conv) {
346                                                         if (!strcmp(conv, ext)) {
347                                                                 /* Match! */
348                                                                 found++;
349                                                                 free(conv);
350                                                                 free(start);
351                                                                 break;
352                                                         }
353                                                         free(conv);
354                                                 }
355                                         }
356                                         free(start);
357                                 }
358                                 v = v->next;
359                         }
360
361                         if (v) {
362                                 /* We have a match -- play a greeting if they have it */
363                                 res = play_mailbox_owner(chan, context, dialcontext, v->name, name);
364                                 switch (res) {
365                                         case -1:
366                                                 /* user pressed '1' but extension does not exist, or
367                                                  * user hungup
368                                                  */
369                                                 lastuserchoice = 0;
370                                                 break;
371                                         case '1':
372                                                 /* user pressed '1' and extensions exists */
373                                                 lastuserchoice = res;
374                                                 ast_copy_string(chan->context, dialcontext, sizeof(chan->context));
375                                                 ast_copy_string(chan->exten, v->name, sizeof(chan->exten));
376                                                 chan->priority = 0;
377                                                 break;
378                                         case '*':
379                                                 /* user pressed '*' to skip something found */
380                                                 lastuserchoice = res;
381                                                 res = 0;
382                                                 break;
383                                         default:
384                                                 break;
385                                 }
386                                 v = v->next;
387                         }
388                 }
389
390                 if (lastuserchoice != '1') {
391                         if (found) 
392                                 res = ast_streamfile(chan, "dir-nomore", chan->language);
393                         else
394                                 res = ast_streamfile(chan, "dir-nomatch", chan->language);
395                         if (!res)
396                                 res = 1;
397                         return res;
398                 }
399                 return 0;
400         }
401         return res;
402 }
403
404 static int directory_exec(struct ast_channel *chan, void *data)
405 {
406         int res = 0;
407         struct localuser *u;
408         struct ast_config *cfg;
409         int last = 1;
410         char *context, *dialcontext, *dirintro, *options;
411
412         if (!data) {
413                 ast_log(LOG_WARNING, "directory requires an argument (context[,dialcontext])\n");
414                 return -1;
415         }
416
417 top:
418         context = ast_strdupa(data);
419         dialcontext = strchr(context, '|');
420         if (dialcontext) {
421                 *dialcontext = '\0';
422                 dialcontext++;
423                 options = strchr(dialcontext, '|');
424                 if (options) {
425                         *options = '\0';
426                         options++; 
427                         if (strchr(options, 'f'))
428                                 last = 0;
429                 }
430         } else  
431                 dialcontext = context;
432
433         cfg = realtime_directory(context);
434         if (!cfg)
435                 return -1;
436
437         LOCAL_USER_ADD(u);
438
439         dirintro = ast_variable_retrieve(cfg, context, "directoryintro");
440         if (!dirintro || ast_strlen_zero(dirintro))
441                 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
442         if (!dirintro || ast_strlen_zero(dirintro)) {
443                 if (last)
444                         dirintro = "dir-intro"; 
445                 else
446                         dirintro = "dir-intro-fn";
447         }
448         if (chan->_state != AST_STATE_UP) 
449                 res = ast_answer(chan);
450         if (!res)
451                 res = ast_streamfile(chan, dirintro, chan->language);
452         if (!res)
453                 res = ast_waitstream(chan, AST_DIGIT_ANY);
454         ast_stopstream(chan);
455         if (!res)
456                 res = ast_waitfordigit(chan, 5000);
457         if (res > 0) {
458                 res = do_directory(chan, cfg, context, dialcontext, res, last);
459                 if (res > 0) {
460                         res = ast_waitstream(chan, AST_DIGIT_ANY);
461                         ast_stopstream(chan);
462                         if (res >= 0) {
463                                 goto top;
464                         }
465                 }
466         }
467         ast_config_destroy(cfg);
468         LOCAL_USER_REMOVE(u);
469         return res;
470 }
471
472 int unload_module(void)
473 {
474         STANDARD_HANGUP_LOCALUSERS;
475         return ast_unregister_application(app);
476 }
477
478 int load_module(void)
479 {
480         return ast_register_application(app, directory_exec, synopsis, descrip);
481 }
482
483 char *description(void)
484 {
485         return tdesc;
486 }
487
488 int usecount(void)
489 {
490         int res;
491         STANDARD_USECOUNT(res);
492         return res;
493 }
494
495 char *key()
496 {
497         return ASTERISK_GPL_KEY;
498 }