convert lists of constants in channel.h to enums instead of #defines
[asterisk/asterisk.git] / apps / app_directory.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Mark Spencer <markster@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Provide a directory of extensions
22  *
23  * \author Mark Spencer <markster@digium.com>
24  * 
25  * \ingroup applications
26  */
27  
28 #include "asterisk.h"
29
30 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
31
32 #include <string.h>
33 #include <ctype.h>
34 #include <stdlib.h>
35 #include <stdio.h>
36
37 #include "asterisk/lock.h"
38 #include "asterisk/file.h"
39 #include "asterisk/logger.h"
40 #include "asterisk/channel.h"
41 #include "asterisk/pbx.h"
42 #include "asterisk/module.h"
43 #include "asterisk/config.h"
44 #include "asterisk/say.h"
45 #include "asterisk/utils.h"
46 #include "asterisk/app.h"
47
48 #ifdef USE_ODBC_STORAGE
49 #include <errno.h>
50 #include <sys/mman.h>
51 #include "asterisk/res_odbc.h"
52
53 static char odbc_database[80] = "asterisk";
54 static char odbc_table[80] = "voicemessages";
55 static char vmfmts[80] = "wav";
56 #endif
57
58 static char *app = "Directory";
59
60 static char *synopsis = "Provide directory of voicemail extensions";
61 static char *descrip =
62 "  Directory(vm-context[|dial-context[|options]]): This application will present\n"
63 "the calling channel with a directory of extensions from which they can search\n"
64 "by name. The list of names and corresponding extensions is retrieved from the\n"
65 "voicemail configuration file, voicemail.conf.\n"
66 "  This application will immediately exit if one of the following DTMF digits are\n"
67 "received and the extension to jump to exists:\n"
68 "    0 - Jump to the 'o' extension, if it exists.\n"
69 "    * - Jump to the 'a' extension, if it exists.\n\n"
70 "  Parameters:\n"
71 "    vm-context   - This is the context within voicemail.conf to use for the\n"
72 "                   Directory.\n"
73 "    dial-context - This is the dialplan context to use when looking for an\n"
74 "                   extension that the user has selected, or when jumping to the\n"
75 "                   'o' or 'a' extension.\n\n"
76 "  Options:\n"
77 "    e - In addition to the name, also read the extension number to the\n"
78 "        caller before presenting dialing options.\n"
79 "    f - Allow the caller to enter the first name of a user in the directory\n"
80 "        instead of using the last name.\n";
81
82 /* For simplicity, I'm keeping the format compatible with the voicemail config,
83    but i'm open to suggestions for isolating it */
84
85 #define VOICEMAIL_CONFIG "voicemail.conf"
86
87 /* How many digits to read in */
88 #define NUMDIGITS 3
89
90 LOCAL_USER_DECL;
91
92 #ifdef USE_ODBC_STORAGE
93 static void retrieve_file(char *dir)
94 {
95         int x = 0;
96         int res;
97         int fd=-1;
98         size_t fdlen = 0;
99         void *fdm=NULL;
100         SQLHSTMT stmt;
101         char sql[256];
102         char fmt[80]="";
103         char *c;
104         SQLLEN colsize;
105         char full_fn[256];
106
107         odbc_obj *obj;
108         obj = fetch_odbc_obj(odbc_database, 0);
109         if (obj) {
110                 do {
111                         ast_copy_string(fmt, vmfmts, sizeof(fmt));
112                         c = strchr(fmt, '|');
113                         if (c)
114                                 *c = '\0';
115                         if (!strcasecmp(fmt, "wav49"))
116                                 strcpy(fmt, "WAV");
117                         snprintf(full_fn, sizeof(full_fn), "%s.%s", dir, fmt);
118                         res = SQLAllocHandle(SQL_HANDLE_STMT, obj->con, &stmt);
119                         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
120                                 ast_log(LOG_WARNING, "SQL Alloc Handle failed!\n");
121                                 break;
122                         }
123                         snprintf(sql, sizeof(sql), "SELECT recording FROM %s WHERE dir=? AND msgnum=-1", odbc_table);
124                         res = SQLPrepare(stmt, (unsigned char *)sql, SQL_NTS);
125                         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
126                                 ast_log(LOG_WARNING, "SQL Prepare failed![%s]\n", sql);
127                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
128                                 break;
129                         }
130                         SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, strlen(dir), 0, (void *)dir, 0, NULL);
131                         res = odbc_smart_execute(obj, stmt);
132                         if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
133                                 ast_log(LOG_WARNING, "SQL Execute error!\n[%s]\n\n", sql);
134                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
135                                 break;
136                         }
137                         res = SQLFetch(stmt);
138                         if (res == SQL_NO_DATA) {
139                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
140                                 break;
141                         } else if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
142                                 ast_log(LOG_WARNING, "SQL Fetch error!\n[%s]\n\n", sql);
143                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
144                                 break;
145                         }
146                         fd = open(full_fn, O_RDWR | O_CREAT | O_TRUNC, 0770);
147                         if (fd < 0) {
148                                 ast_log(LOG_WARNING, "Failed to write '%s': %s\n", full_fn, strerror(errno));
149                                 SQLFreeHandle(SQL_HANDLE_STMT, stmt);
150                                 break;
151                         }
152
153                         res = SQLGetData(stmt, 1, SQL_BINARY, NULL, 0, &colsize);
154                         fdlen = colsize;
155                         if (fd > -1) {
156                                 char tmp[1]="";
157                                 lseek(fd, fdlen - 1, SEEK_SET);
158                                 if (write(fd, tmp, 1) != 1) {
159                                         close(fd);
160                                         fd = -1;
161                                         break;
162                                 }
163                                 if (fd > -1)
164                                         fdm = mmap(NULL, fdlen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
165                         }
166                         if (fdm) {
167                                 memset(fdm, 0, fdlen);
168                                 res = SQLGetData(stmt, x + 1, SQL_BINARY, fdm, fdlen, &colsize);
169                                 if ((res != SQL_SUCCESS) && (res != SQL_SUCCESS_WITH_INFO)) {
170                                         ast_log(LOG_WARNING, "SQL Get Data error!\n[%s]\n\n", sql);
171                                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
172                                         break;
173                                 }
174                         }
175                         SQLFreeHandle(SQL_HANDLE_STMT, stmt);
176                 } while (0);
177         } else
178                 ast_log(LOG_WARNING, "Failed to obtain database object for '%s'!\n", odbc_database);
179         if (fdm)
180                 munmap(fdm, fdlen);
181         if (fd > -1)
182                 close(fd);
183         return;
184 }
185 #endif
186
187 static char *convert(char *lastname)
188 {
189         char *tmp;
190         int lcount = 0;
191         tmp = ast_malloc(NUMDIGITS + 1);
192         if (tmp) {
193                 while((*lastname > 32) && lcount < NUMDIGITS) {
194                         switch(toupper(*lastname)) {
195                         case '1':
196                                 tmp[lcount++] = '1';
197                                 break;
198                         case '2':
199                         case 'A':
200                         case 'B':
201                         case 'C':
202                                 tmp[lcount++] = '2';
203                                 break;
204                         case '3':
205                         case 'D':
206                         case 'E':
207                         case 'F':
208                                 tmp[lcount++] = '3';
209                                 break;
210                         case '4':
211                         case 'G':
212                         case 'H':
213                         case 'I':
214                                 tmp[lcount++] = '4';
215                                 break;
216                         case '5':
217                         case 'J':
218                         case 'K':
219                         case 'L':
220                                 tmp[lcount++] = '5';
221                                 break;
222                         case '6':
223                         case 'M':
224                         case 'N':
225                         case 'O':
226                                 tmp[lcount++] = '6';
227                                 break;
228                         case '7':
229                         case 'P':
230                         case 'Q':
231                         case 'R':
232                         case 'S':
233                                 tmp[lcount++] = '7';
234                                 break;
235                         case '8':
236                         case 'T':
237                         case 'U':
238                         case 'V':
239                                 tmp[lcount++] = '8';
240                                 break;
241                         case '9':
242                         case 'W':
243                         case 'X':
244                         case 'Y':
245                         case 'Z':
246                                 tmp[lcount++] = '9';
247                                 break;
248                         }
249                         lastname++;
250                 }
251                 tmp[lcount] = '\0';
252         }
253         return tmp;
254 }
255
256 /* play name of mailbox owner.
257  * returns:  -1 for bad or missing extension
258  *           '1' for selected entry from directory
259  *           '*' for skipped entry from directory
260  */
261 static int play_mailbox_owner(struct ast_channel *chan, char *context,
262                 char *dialcontext, char *ext, char *name, int readext)
263 {
264         int res = 0;
265         int loop;
266         char fn[256];
267
268         /* Check for the VoiceMail2 greeting first */
269         snprintf(fn, sizeof(fn), "%s/voicemail/%s/%s/greet",
270                 ast_config_AST_SPOOL_DIR, context, ext);
271 #ifdef USE_ODBC_STORAGE
272         retrieve_file(fn);
273 #endif
274
275         if (ast_fileexists(fn, NULL, chan->language) <= 0) {
276                 /* no file, check for an old-style Voicemail greeting */
277                 snprintf(fn, sizeof(fn), "%s/vm/%s/greet",
278                         ast_config_AST_SPOOL_DIR, ext);
279         }
280 #ifdef USE_ODBC_STORAGE
281         retrieve_file(fn2);
282 #endif
283
284         if (ast_fileexists(fn, NULL, chan->language) > 0) {
285                 res = ast_stream_and_wait(chan, fn, chan->language, AST_DIGIT_ANY);
286                 ast_stopstream(chan);
287                 /* If Option 'e' was specified, also read the extension number with the name */
288                 if (readext) {
289                         ast_stream_and_wait(chan, "vm-extension", chan->language, AST_DIGIT_ANY);
290                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
291                 }
292         } else {
293                 res = ast_say_character_str(chan, S_OR(name, ext), AST_DIGIT_ANY, chan->language);
294                 if (!ast_strlen_zero(name) && readext) {
295                         ast_stream_and_wait(chan, "vm-extension", chan->language, AST_DIGIT_ANY);
296                         res = ast_say_character_str(chan, ext, AST_DIGIT_ANY, chan->language);
297                 }
298         }
299 #ifdef USE_ODBC_STORAGE
300         ast_filedelete(fn, NULL);       
301         ast_filedelete(fn2, NULL);      
302 #endif
303
304         for (loop = 3 ; loop > 0; loop--) {
305                 if (!res)
306                         res = ast_stream_and_wait(chan, "dir-instr", chan->language, AST_DIGIT_ANY);
307                 if (!res)
308                         res = ast_waitfordigit(chan, 3000);
309                 ast_stopstream(chan);
310         
311                 if (res < 0) /* User hungup, so jump out now */
312                         break;
313                 if (res == '1') {       /* Name selected */
314                         if (ast_goto_if_exists(chan, dialcontext, ext, 1)) {
315                                 ast_log(LOG_WARNING,
316                                         "Can't find extension '%s' in context '%s'.  "
317                                         "Did you pass the wrong context to Directory?\n",
318                                         ext, dialcontext);
319                                 res = -1;
320                         }
321                         break;
322                 }
323                 if (res == '*') /* Skip to next match in list */
324                         break;
325
326                 /* Not '1', or '*', so decrement number of tries */
327                 res = 0;
328         }
329
330         return(res);
331 }
332
333 static struct ast_config *realtime_directory(char *context)
334 {
335         struct ast_config *cfg;
336         struct ast_config *rtdata;
337         struct ast_category *cat;
338         struct ast_variable *var;
339         char *mailbox;
340         char *fullname;
341         char *hidefromdir;
342         char tmp[100];
343
344         /* Load flat file config. */
345         cfg = ast_config_load(VOICEMAIL_CONFIG);
346
347         if (!cfg) {
348                 /* Loading config failed. */
349                 ast_log(LOG_WARNING, "Loading config failed.\n");
350                 return NULL;
351         }
352
353         /* Get realtime entries, categorized by their mailbox number
354            and present in the requested context */
355         rtdata = ast_load_realtime_multientry("voicemail", "mailbox LIKE", "%", "context", context, NULL);
356
357         /* if there are no results, just return the entries from the config file */
358         if (!rtdata)
359                 return cfg;
360
361         /* Does the context exist within the config file? If not, make one */
362         cat = ast_category_get(cfg, context);
363         if (!cat) {
364                 cat = ast_category_new(context);
365                 if (!cat) {
366                         ast_log(LOG_WARNING, "Out of memory\n");
367                         ast_config_destroy(cfg);
368                         return NULL;
369                 }
370                 ast_category_append(cfg, cat);
371         }
372
373         mailbox = NULL;
374         while ( (mailbox = ast_category_browse(rtdata, mailbox)) ) {
375                 fullname = ast_variable_retrieve(rtdata, mailbox, "fullname");
376                 hidefromdir = ast_variable_retrieve(rtdata, mailbox, "hidefromdir");
377                 snprintf(tmp, sizeof(tmp), "no-password,%s,hidefromdir=%s",
378                          fullname ? fullname : "",
379                          hidefromdir ? hidefromdir : "no");
380                 var = ast_variable_new(mailbox, tmp);
381                 if (var)
382                         ast_variable_append(cat, var);
383                 else
384                         ast_log(LOG_WARNING, "Out of memory adding mailbox '%s'\n", mailbox);
385         }
386         ast_config_destroy(rtdata);
387
388         return cfg;
389 }
390
391 static int do_directory(struct ast_channel *chan, struct ast_config *cfg, char *context, char *dialcontext, char digit, int last, int readext)
392 {
393         /* Read in the first three digits..  "digit" is the first digit, already read */
394         char ext[NUMDIGITS + 1];
395         char name[80] = "";
396         struct ast_variable *v;
397         int res;
398         int found=0;
399         int lastuserchoice = 0;
400         char *start, *pos, *conv,*stringp=NULL;
401
402         if (ast_strlen_zero(context)) {
403                 ast_log(LOG_WARNING,
404                         "Directory must be called with an argument "
405                         "(context in which to interpret extensions)\n");
406                 return -1;
407         }
408         if (digit == '0') {
409                 if (!ast_goto_if_exists(chan, chan->context, "o", 1) ||
410                     (!ast_strlen_zero(chan->macrocontext) &&
411                      !ast_goto_if_exists(chan, chan->macrocontext, "o", 1))) {
412                         return 0;
413                 } else {
414                         ast_log(LOG_WARNING, "Can't find extension 'o' in current context.  "
415                                 "Not Exiting the Directory!\n");
416                         res = 0;
417                 }
418         }       
419         if (digit == '*') {
420                 if (!ast_goto_if_exists(chan, chan->context, "a", 1) ||
421                     (!ast_strlen_zero(chan->macrocontext) &&
422                      !ast_goto_if_exists(chan, chan->macrocontext, "a", 1))) {
423                         return 0;
424                 } else {
425                         ast_log(LOG_WARNING, "Can't find extension 'a' in current context.  "
426                                 "Not Exiting the Directory!\n");
427                         res = 0;
428                 }
429         }       
430         memset(ext, 0, sizeof(ext));
431         ext[0] = digit;
432         res = 0;
433         if (ast_readstring(chan, ext + 1, NUMDIGITS - 1, 3000, 3000, "#") < 0) res = -1;
434         if (!res) {
435                 /* Search for all names which start with those digits */
436                 v = ast_variable_browse(cfg, context);
437                 while(v && !res) {
438                         /* Find all candidate extensions */
439                         while(v) {
440                                 /* Find a candidate extension */
441                                 start = strdup(v->value);
442                                 if (start && !strcasestr(start, "hidefromdir=yes")) {
443                                         stringp=start;
444                                         strsep(&stringp, ",");
445                                         pos = strsep(&stringp, ",");
446                                         if (pos) {
447                                                 ast_copy_string(name, pos, sizeof(name));
448                                                 /* Grab the last name */
449                                                 if (last && strrchr(pos,' '))
450                                                         pos = strrchr(pos, ' ') + 1;
451                                                 conv = convert(pos);
452                                                 if (conv) {
453                                                         if (!strcmp(conv, ext)) {
454                                                                 /* Match! */
455                                                                 found++;
456                                                                 free(conv);
457                                                                 free(start);
458                                                                 break;
459                                                         }
460                                                         free(conv);
461                                                 }
462                                         }
463                                         free(start);
464                                 }
465                                 v = v->next;
466                         }
467
468                         if (v) {
469                                 /* We have a match -- play a greeting if they have it */
470                                 res = play_mailbox_owner(chan, context, dialcontext, v->name, name, readext);
471                                 switch (res) {
472                                         case -1:
473                                                 /* user pressed '1' but extension does not exist, or
474                                                  * user hungup
475                                                  */
476                                                 lastuserchoice = 0;
477                                                 break;
478                                         case '1':
479                                                 /* user pressed '1' and extensions exists;
480                                                    play_mailbox_owner will already have done
481                                                    a goto() on the channel
482                                                  */
483                                                 lastuserchoice = res;
484                                                 break;
485                                         case '*':
486                                                 /* user pressed '*' to skip something found */
487                                                 lastuserchoice = res;
488                                                 res = 0;
489                                                 break;
490                                         default:
491                                                 break;
492                                 }
493                                 v = v->next;
494                         }
495                 }
496
497                 if (lastuserchoice != '1') {
498                         res = ast_streamfile(chan, found ? "dir-nomore" : "dir-nomatch", chan->language);
499                         if (!res)
500                                 res = 1;
501                         return res;
502                 }
503                 return 0;
504         }
505         return res;
506 }
507
508 static int directory_exec(struct ast_channel *chan, void *data)
509 {
510         int res = 0;
511         struct localuser *u;
512         struct ast_config *cfg;
513         int last = 1;
514         int readext = 0;
515         char *dirintro, *parse;
516         AST_DECLARE_APP_ARGS(args,
517                 AST_APP_ARG(vmcontext);
518                 AST_APP_ARG(dialcontext);
519                 AST_APP_ARG(options);
520         );
521
522         if (ast_strlen_zero(data)) {
523                 ast_log(LOG_WARNING, "Directory requires an argument (context[,dialcontext])\n");
524                 return -1;
525         }
526
527         LOCAL_USER_ADD(u);
528
529         parse = ast_strdupa(data);
530
531         AST_STANDARD_APP_ARGS(args, parse);
532                 
533         if (args.options) {
534                 if (strchr(args.options, 'f'))
535                         last = 0;
536                 if (strchr(args.options, 'e'))
537                         readext = 1;
538         }
539
540         if (ast_strlen_zero(args.dialcontext))  
541                 args.dialcontext = args.vmcontext;
542
543         cfg = realtime_directory(args.vmcontext);
544         if (!cfg) {
545                 ast_log(LOG_ERROR, "Unable to read the configuration data!\n");
546                 LOCAL_USER_REMOVE(u);
547                 return -1;
548         }
549
550         dirintro = ast_variable_retrieve(cfg, args.vmcontext, "directoryintro");
551         if (ast_strlen_zero(dirintro))
552                 dirintro = ast_variable_retrieve(cfg, "general", "directoryintro");
553         if (ast_strlen_zero(dirintro))
554                 dirintro = last ? "dir-intro" : "dir-intro-fn";
555
556         if (chan->_state != AST_STATE_UP) 
557                 res = ast_answer(chan);
558
559         for (;;) {
560                 if (!res)
561                         res = ast_stream_and_wait(chan, dirintro, chan->language, AST_DIGIT_ANY);
562                 ast_stopstream(chan);
563                 if (!res)
564                         res = ast_waitfordigit(chan, 5000);
565                 if (res > 0) {
566                         res = do_directory(chan, cfg, args.vmcontext, args.dialcontext, res, last, readext);
567                         if (res > 0) {
568                                 res = ast_waitstream(chan, AST_DIGIT_ANY);
569                                 ast_stopstream(chan);
570                                 if (res >= 0)
571                                         continue;
572                         }
573                 }
574                 break;
575         }
576         ast_config_destroy(cfg);
577         LOCAL_USER_REMOVE(u);
578         return res;
579 }
580
581 static int unload_module(void *mod)
582 {
583         int res;
584         res = ast_unregister_application(app);
585         return res;
586 }
587
588 static int load_module(void *mod)
589 {
590         __mod_desc = mod;
591 #ifdef USE_ODBC_STORAGE
592         struct ast_config *cfg = ast_config_load(VOICEMAIL_CONFIG);
593         char *tmp;
594
595         if (cfg) {
596                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbcstorage"))) {
597                         ast_copy_string(odbc_database, tmp, sizeof(odbc_database));
598                 }
599                 if ((tmp = ast_variable_retrieve(cfg, "general", "odbctable"))) {
600                         ast_copy_string(odbc_table, tmp, sizeof(odbc_table));
601                 }
602                 if ((tmp = ast_variable_retrieve(cfg, "general", "format"))) {
603                         ast_copy_string(vmfmts, tmp, sizeof(vmfmts));
604                 }
605                 ast_config_destroy(cfg);
606         } else
607                 ast_log(LOG_WARNING, "Unable to load " VOICEMAIL_CONFIG " - ODBC defaults will be used\n");
608 #endif
609
610         return ast_register_application(app, directory_exec, synopsis, descrip);
611 }
612
613 static const char *description(void)
614 {
615         return "Extension Directory";
616 }
617
618 static const char *key(void)
619 {
620         return ASTERISK_GPL_KEY;
621 }
622
623 STD_MOD1;