/* * Copyright (C) 2008 iptego GmbH * * This file is part of SEMS, a free SIP media server. * * SEMS is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * For a license to use the sems software under conditions * other than those described here, or to purchase support for this * software, please contact iptel.org by e-mail at the following addresses: * info@iptel.org * * SEMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "AnnRecorder.h" #include "AmConfig.h" #include "AmUtils.h" #include "AmPlugIn.h" #include "AmPromptCollection.h" #include "AmUriParser.h" #include "../msg_storage/MsgStorageAPI.h" #include "sems.h" #include "log.h" using std::map; #define MOD_NAME "annrecorder" #define DEFAULT_TYPE "vm" #define DOMAIN_PROMPT_SUFFIX "-prompts" #define TIMERID_START_TIMER 1 #define TIMERID_CONFIRM_TIMER 2 #define START_RECORDING_TIMEOUT 20 #define CONFIRM_RECORDING_TIMEOUT 20 #define SEP_CONFIRM_BEGIN 1 #define SEP_MSG_BEGIN 2 #define MAX_MESSAGE_TIME 120 EXPORT_SESSION_FACTORY(AnnRecorderFactory,MOD_NAME); string AnnRecorderFactory::AnnouncePath; string AnnRecorderFactory::DefaultAnnounce; bool AnnRecorderFactory::SimpleMode=false; AmDynInvokeFactory* AnnRecorderFactory::message_storage_fact = NULL; const char* MsgStrError(int e) { switch (e) { case MSG_OK: return "MSG_OK"; break; case MSG_EMSGEXISTS: return "MSG_EMSGEXISTS"; break; case MSG_EUSRNOTFOUND: return "MSG_EUSRNOTFOUND"; break; case MSG_EMSGNOTFOUND: return "MSG_EMSGNOTFOUND"; break; case MSG_EALREADYCLOSED: return "MSG_EALREADYCLOSED"; break; case MSG_EREADERROR: return "MSG_EREADERROR"; break; case MSG_ENOSPC: return "MSG_ENOSPC"; break; case MSG_ESTORAGE: return "MSG_ESTORAGE"; break; default: return "Unknown Error"; } } AnnRecorderFactory::AnnRecorderFactory(const string& _app_name) : AmSessionFactory(_app_name) { } int AnnRecorderFactory::onLoad() { AmConfigReader cfg; if(cfg.loadFile(AmConfig::ModConfigPath + string(MOD_NAME ".conf"))) return -1; // get application specific global parameters configureModule(cfg); AnnouncePath = cfg.getParameter("announce_path",ANNOUNCE_PATH); if( !AnnouncePath.empty() && AnnouncePath[AnnouncePath.length()-1] != '/' ) AnnouncePath += "/"; DefaultAnnounce = cfg.getParameter("default_announce"); SimpleMode = (cfg.getParameter("simple_mode") == "yes"); AM_PROMPT_START; AM_PROMPT_ADD(WELCOME, ANNREC_ANNOUNCE_PATH WELCOME".wav"); AM_PROMPT_ADD(YOUR_PROMPT, ANNREC_ANNOUNCE_PATH YOUR_PROMPT".wav"); AM_PROMPT_ADD(TO_RECORD, ANNREC_ANNOUNCE_PATH TO_RECORD".wav"); AM_PROMPT_ADD(CONFIRM, ANNREC_ANNOUNCE_PATH CONFIRM".wav"); AM_PROMPT_ADD(GREETING_SET, ANNREC_ANNOUNCE_PATH GREETING_SET".wav"); AM_PROMPT_ADD(BYE, ANNREC_ANNOUNCE_PATH BYE".wav"); AM_PROMPT_ADD(BEEP, ANNREC_ANNOUNCE_PATH BEEP".wav"); AM_PROMPT_END(prompts, cfg, MOD_NAME); message_storage_fact = AmPlugIn::instance()->getFactory4Di("msg_storage"); if(!message_storage_fact) { ERROR("sorry, could not get msg_storage, please load a suitable plug-in\n"); return -1; } return 0; } void AnnRecorderFactory::getAppParams(const AmSipRequest& req, map& params) { string language; string domain; string user; string typ; if (SimpleMode){ AmUriParser p; p.uri = req.from_uri; if (!p.parse_uri()) { DBG("parsing From-URI '%s' failed\n", p.uri.c_str()); throw AmSession::Exception(500, MOD_NAME ": could not parse From-URI"); } user = p.uri_user; //domain = p.uri_domain; domain = "default"; typ = DEFAULT_TYPE; } else { string iptel_app_param = getHeader(req.hdrs, PARAM_HDR, true); if (!iptel_app_param.length()) { throw AmSession::Exception(500, MOD_NAME ": parameters not found"); } language = get_header_keyvalue(iptel_app_param, "lng", "Language"); user = get_header_keyvalue(iptel_app_param,"uid", "UserID"); if (user.empty()) { user = get_header_keyvalue(iptel_app_param,"usr", "User"); if (!user.length()) user = req.user; } domain = get_header_keyvalue(iptel_app_param, "did", "DomainID"); if (domain.empty()){ domain = get_header_keyvalue(iptel_app_param, "dom", "Domain"); if (domain.empty()) domain = req.domain; } typ = get_header_keyvalue(iptel_app_param, "typ", "Type"); if (!typ.length()) typ = DEFAULT_TYPE; } // checks if (user.empty()) throw AmSession::Exception(500, MOD_NAME ": user missing"); string announce_file = add2path(AnnouncePath,2, domain.c_str(), (user + ".wav").c_str()); if (file_exists(announce_file)) goto announce_found; if (!language.empty()) { announce_file = add2path(AnnouncePath,3, domain.c_str(), language.c_str(), DefaultAnnounce.c_str()); if (file_exists(announce_file)) goto announce_found; } announce_file = add2path(AnnouncePath,2, domain.c_str(), DefaultAnnounce.c_str()); if (file_exists(announce_file)) goto announce_found; if (!language.empty()) { announce_file = add2path(AnnouncePath,2, language.c_str(), DefaultAnnounce.c_str()); if (file_exists(announce_file)) goto announce_found; } announce_file = add2path(AnnouncePath,1, DefaultAnnounce.c_str()); if (!file_exists(announce_file)) announce_file = ""; announce_found: DBG(MOD_NAME " invocation parameters: \n"); DBG(" User: <%s> \n", user.c_str()); DBG(" Domain: <%s> \n", domain.c_str()); DBG(" Language: <%s> \n", language.c_str()); DBG(" Type: <%s> \n", typ.c_str()); DBG(" Def. File:<%s> \n", announce_file.c_str()); params["domain"] = domain; params["user"] = user; params["defaultfile"] = announce_file; params["type"] = typ; } AmSession* AnnRecorderFactory::onInvite(const AmSipRequest& req, const string& app_name, const map& app_params) { map params; getAppParams(req, params); return new AnnRecorderDialog(params, prompts, NULL); } AmSession* AnnRecorderFactory::onInvite(const AmSipRequest& req, const string& app_name, const AmArg& session_params) { UACAuthCred* cred = AmUACAuth::unpackCredentials(session_params); map params; getAppParams(req, params); AmSession* s = new AnnRecorderDialog(params, prompts, cred); if (NULL == cred) { WARN("discarding unknown session parameters.\n"); } else { AmUACAuth::enable(s); } return s; } AnnRecorderDialog::AnnRecorderDialog(const map& params, AmPromptCollection& prompts, UACAuthCred* credentials) : params(params), prompts(prompts), cred(credentials), playlist(this) { msg_storage = AnnRecorderFactory::message_storage_fact->getInstance(); if(!msg_storage){ ERROR("could not get a message storage reference\n"); throw AmSession::Exception(500,"could not get a message storage reference"); } } AnnRecorderDialog::~AnnRecorderDialog() { prompts.cleanup((long)this); if (msg_filename.length()) unlink(msg_filename.c_str()); } void AnnRecorderDialog::onSessionStart() { DBG("AnnRecorderDialog::onSessionStart\n"); prompts.addToPlaylist(WELCOME, (long)this, playlist); prompts.addToPlaylist(YOUR_PROMPT, (long)this, playlist); enqueueCurrent(); prompts.addToPlaylist(TO_RECORD, (long)this, playlist); enqueueSeparator(SEP_MSG_BEGIN); // set the playlist as input and output setInOut(&playlist,&playlist); state = S_WAIT_START; AmSession::onSessionStart(); } void AnnRecorderDialog::enqueueCurrent() { wav_file.close(); FILE* fp = getCurrentMessage(); if (!fp) { DBG("no recorded msg available, using default\n"); if (wav_file.open(params["defaultfile"], AmAudioFile::Read)) { ERROR("opening default greeting file '%s'!\n", params["defaultfile"].c_str()); return; } } else { if (wav_file.fpopen("aa.wav", AmAudioFile::Read, fp)) { ERROR("fpopen message file!\n"); return; } } playlist.addToPlaylist(new AmPlaylistItem(&wav_file, NULL)); } void AnnRecorderDialog::onDtmf(int event, int duration_msec) { DBG("DTMF %d, %d\n", event, duration_msec); // remove timer try { removeTimers(); } catch(...) { ERROR("Exception caught calling mod api\n"); } switch (state) { case S_WAIT_START: { DBG("received key %d in state S_WAIT_START: start recording\n", event); playlist.flush(); wav_file.close(); msg_filename = "/tmp/" + getLocalTag() + ".wav"; if(wav_file.open(msg_filename,AmAudioFile::Write,false)) { ERROR("AnnRecorder: couldn't open %s for writing\n", msg_filename.c_str()); dlg->bye(); setStopped(); } wav_file.setRecordTime(MAX_MESSAGE_TIME*1000); prompts.addToPlaylist(BEEP, (long)this, playlist); playlist.addToPlaylist(new AmPlaylistItem(NULL,&wav_file)); state = S_RECORDING; } break; case S_RECORDING: { DBG("received key %d in state S_RECORDING: replay recording\n", event); prompts.addToPlaylist(BEEP, (long)this, playlist); playlist.flush(); replayRecording(); } break; case S_CONFIRM: { DBG("received key %d in state S_CONFIRM save or redo\n", event); playlist.flush(); wav_file.close(); if (event == 1) { // save msg saveAndConfirm(); } else { prompts.addToPlaylist(TO_RECORD, (long)this, playlist); state = S_WAIT_START; } } break; default: { DBG("ignoring key %d in state %d\n", event, state); }break; } } void AnnRecorderDialog::saveAndConfirm() { // wav_file.setCloseOnDestroy(false); wav_file.close(); FILE* fp = fopen(msg_filename.c_str(), "r"); if (fp) { saveMessage(fp); prompts.addToPlaylist(GREETING_SET, (long)this, playlist); } prompts.addToPlaylist(BYE, (long)this, playlist); state = S_BYE; } void AnnRecorderDialog::onBye(const AmSipRequest& req) { DBG("onBye: stopSession\n"); setStopped(); } void AnnRecorderDialog::process(AmEvent* event) { AmPluginEvent* plugin_event = dynamic_cast(event); if(plugin_event && plugin_event->name == "timer_timeout") { event->processed = true; int timer_id = plugin_event->data.get(0).asInt(); if (timer_id == TIMERID_START_TIMER) { if (S_WAIT_START == state) { prompts.addToPlaylist(BYE, (long)this, playlist); state = S_BYE; } return; } if (timer_id == TIMERID_CONFIRM_TIMER) { saveAndConfirm(); return; } ERROR("unknown timer id!\n"); } AmAudioEvent* audio_event = dynamic_cast(event); if(audio_event && (audio_event->event_id == AmAudioEvent::noAudio)){ if (S_BYE == state) { dlg->bye(); setStopped(); return; } if (S_RECORDING == state) { replayRecording(); } } AmPlaylistSeparatorEvent* pl_ev = dynamic_cast(event); if (pl_ev) { if ((pl_ev->event_id == SEP_MSG_BEGIN) && (S_WAIT_START == state)) { // start timer setTimer(TIMERID_START_TIMER, START_RECORDING_TIMEOUT); return; } if ((pl_ev->event_id == SEP_CONFIRM_BEGIN) && (S_CONFIRM == state)) { // start timer setTimer(TIMERID_CONFIRM_TIMER, CONFIRM_RECORDING_TIMEOUT); return; } return; } AmSession::process(event); } void AnnRecorderDialog::replayRecording() { prompts.addToPlaylist(YOUR_PROMPT, (long)this, playlist); DBG("msg_filename = '%s'\n", msg_filename.c_str()); if (!wav_file.open(msg_filename, AmAudioFile::Read)) playlist.addToPlaylist(new AmPlaylistItem(&wav_file, NULL)); prompts.addToPlaylist(CONFIRM, (long)this, playlist); enqueueSeparator(SEP_CONFIRM_BEGIN); state = S_CONFIRM; } inline UACAuthCred* AnnRecorderDialog::getCredentials() { return cred.get(); } void AnnRecorderDialog::saveMessage(FILE* fp) { string msg_name = params["type"]+".wav"; DBG("message name is '%s'\n", msg_name.c_str()); AmArg di_args,ret; di_args.push((params["domain"]+DOMAIN_PROMPT_SUFFIX).c_str()); // domain di_args.push(params["user"].c_str()); // user di_args.push(msg_name.c_str()); // message name AmArg df; MessageDataFile df_arg(fp); df.setBorrowedPointer(&df_arg); di_args.push(df); try { msg_storage->invoke("msg_new",di_args,ret); } catch(string& s) { ERROR("invoking msg_new: '%s'\n", s.c_str()); } catch(...) { ERROR("invoking msg_new.\n"); } // TODO: evaluate ret return value } FILE* AnnRecorderDialog::getCurrentMessage() { string msgname = params["type"]+".wav"; string& user = params["user"]; string domain = params["domain"]+DOMAIN_PROMPT_SUFFIX; DBG("trying to get message '%s' for user '%s' domain '%s'\n", msgname.c_str(), user.c_str(), domain.c_str()); AmArg di_args,ret; di_args.push(domain.c_str()); // domain di_args.push(user.c_str()); // user di_args.push(msgname.c_str()); // msg name msg_storage->invoke("msg_get",di_args,ret); if (!ret.size() || !isArgInt(ret.get(0))) { ERROR("msg_get for user '%s' domain '%s' msg '%s'" " returned no (valid) result.\n", user.c_str(), domain.c_str(), msgname.c_str() ); return NULL; } int ecode = ret.get(0).asInt(); if (MSG_OK != ecode) { DBG("msg_get for user '%s' domain '%s' message '%s': %s\n", user.c_str(), domain.c_str(), msgname.c_str(), MsgStrError(ret.get(0).asInt())); if ((ret.size() > 1) && isArgAObject(ret.get(1))) { MessageDataFile* f = dynamic_cast(ret.get(1).asObject()); if (NULL != f) delete f; } return NULL; } if ((ret.size() < 2) || (!isArgAObject(ret.get(1)))) { ERROR("msg_get for user '%s' domain '%s' message '%s': " "invalid return value\n", user.c_str(), domain.c_str(), msgname.c_str()); return NULL; } MessageDataFile* f = dynamic_cast(ret.get(1).asObject()); if (NULL == f) return NULL; FILE* fp = f->fp; delete f; return fp; } void AnnRecorderDialog::enqueueSeparator(int id) { playlist_separator.reset(new AmPlaylistSeparator(this, id)); playlist.addToPlaylist(new AmPlaylistItem(playlist_separator.get(), NULL)); }