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