package jpbx.plugins.core; import java.io.*; import javaforce.*; import javaforce.voip.*; import jpbx.core.*; /** Low-level plugin for handling INVITEs to voicemail. */ public class VoiceMail implements Plugin, DialChain, PBXEventHandler { public final static int pid = 10; //priority public static enum State { VM_GREETING, VM_PASSWORD, VM_BAD_PASSWORD, VM_REC_MSG_BEEP, VM_REC_MSG, VM_REC_MSG_MENU, VM_REC_GREETING_BEEP, VM_REC_GREETING, VM_REC_GREETING_MENU, VM_MAIN_MENU, VM_PLAY_MSG, VM_GOODBYE } private PBXAPI api; //interface Plugin public void init(PBXAPI api) { this.api = api; api.hookDialChain(this); JFLog.log("Voicemail plugin init"); } public void uninit(PBXAPI api) { api.unhookDialChain(this); JFLog.log("Voicemail plugin uninit"); } public void install(PBXAPI api) { //nothing to do } public void uninstall(PBXAPI api) { //nothing to do } //interface DialChain public int getPriority() {return pid;} public int onInvite(CallDetailsPBX cd, SQL sql, boolean src) { //NOTE : there is no dst - it's a one-sided call if (!src) return -1; if (!cd.authorized) { if (!cd.anon) return -1; } String ext = sql.select1value("SELECT ext FROM exts WHERE ext=" + sql.quote(cd.dialed)); if (ext == null) return -1; //an extension is not being dialed if (cd.invited) { //reINVITE (just get new codecs) api.log(cd, "VM : reINVITE"); SDP.Stream astream = cd.src.sdp.getFirstAudioStream(); cd.audioRelay.change_src(astream); cd.pbxsrc.sdp.getFirstAudioStream().codecs = cd.src.sdp.getFirstAudioStream().codecs; cd.sip.buildsdp(cd, cd.pbxsrc); api.reply(cd, 200, "OK", null, true, true); return pid; } String value = sql.select1value("SELECT value FROM extopts WHERE ext=" + sql.quote(cd.dialed) + " AND id='vm'"); if ((value == null) || (!value.equals("true"))) { api.log(cd, "VM : ext doesn't have voicemail enabled"); return -1; //extension doesn't have voicemail } cd.pbxsrc.to = cd.src.to.clone(); cd.pbxsrc.from = cd.src.from.clone(); if (cd.audioRelay == null) { cd.audioRelay = new RTPRelay(); cd.audioRelay.init(); } cd.audioRelay.init(cd, this, api); cd.pbxsrc.host = cd.src.host; cd.pbxsrc.sdp = new SDP(); cd.pbxsrc.sdp.ip = api.getlocalhost(cd); SDP.Stream astream = cd.pbxsrc.sdp.addStream(SDP.Type.audio); astream.port = cd.audioRelay.getPort_src(); astream.codecs = cd.src.sdp.getFirstAudioStream().codecs; cd.sip.buildsdp(cd, cd.pbxsrc); cd.invited = true; cd.connected = true; cd.pbxsrc.to = SIP.replacetag(cd.pbxsrc.to, SIP.generatetag()); //assign tag api.reply(cd, 200, "OK", null, true, true); start(cd, sql); return pid; } public void onRinging(CallDetailsPBX cd, SQL sql, boolean src) { } public void onSuccess(CallDetailsPBX cd, SQL sql, boolean src) { } public void onCancel(CallDetailsPBX cd, SQL sql, boolean src) { } public void onError(CallDetailsPBX cd, SQL sql, int code, boolean src) { } public void onTrying(CallDetailsPBX cd, SQL sql, boolean src) { } public void onBye(CallDetailsPBX cd, SQL sql, boolean src) { if (!src) return; api.reply(cd, 200, "OK", null, false, true); if (cd.audioRelay != null) { cd.audioRelay.uninit(); cd.audioRelay = null; } } public void onFeature(CallDetailsPBX cd, SQL sql, String cmd, String cmddata, boolean src) { } //interface PBXEventHandler public void event(CallDetailsPBX cd, int type, char digit, boolean interrupted) { //type = DTMF digit or end of playSound() JFLog.log("event:type=" + type + ":digit=" + digit + ":vmstate=" + cd.vmstate); switch (type) { case PBXEventHandler.DIGIT: switch (cd.vmstate) { case VM_GREETING: if (digit == '*') { cd.vmstate = State.VM_PASSWORD; cd.vmstr = ""; cd.audioRelay.playSound("vm-enter-password"); cd.audioRelay.addSound("vm-pause"); } else { cd.vmstate = State.VM_REC_MSG_BEEP; cd.audioRelay.playSound("vm-beep"); } break; case VM_PASSWORD: JFLog.log("vm password digit=" + digit); switch (digit) { case '#': checkPassword(cd); break; case '*': //reset password cd.vmstr = ""; cd.audioRelay.playSound("vm-pause"); break; default: cd.vmstr += digit; if (cd.vmstr.length() > 32) { checkPassword(cd); } else { cd.audioRelay.playSound("vm-pause"); //play a long pause to cause a timeout } break; } break; case VM_REC_MSG_MENU: switch (digit) { case '1': cd.vmstate = State.VM_GOODBYE; cd.audioRelay.playSound("vm-goodbye"); break; case '2': deleteRecording(cd); cd.vmstate = State.VM_REC_MSG_BEEP; cd.audioRelay.playSound("vm-beep"); break; } break; case VM_REC_GREETING_MENU: switch (digit) { case '1': saveNewGreeting(cd); cd.vmstate = State.VM_MAIN_MENU; cd.audioRelay.playSound("vm-main-menu"); cd.audioRelay.addSound("vm-pause"); break; case '2': deleteRecording(cd); cd.vmstate = State.VM_REC_GREETING_BEEP; cd.audioRelay.playSound("vm-beep"); break; } break; case VM_MAIN_MENU: cd.vmattempts = 0; switch (digit) { case '1': //listen messages if (getMsgLists(cd)) { playMsg(cd); } break; case '2': //record greeting cd.vmstate = State.VM_REC_GREETING_BEEP; cd.audioRelay.playSound("vm-rec-greeting"); cd.audioRelay.addSound("vm-beep"); break; case '*': cd.vmstate = State.VM_GOODBYE; cd.audioRelay.playSound("vm-goodbye"); break; default: cd.audioRelay.playSound("vm-incorrect"); break; } break; case VM_PLAY_MSG: switch (digit) { case '1': //replay message playMsg(cd); break; case '3': //skip message nextMsg(cd); break; case '7': //delete message deleteMsg(cd); nextMsg(cd); break; case '9': //save message saveMsg(cd); nextMsg(cd); case '*': //prev menu cd.vmstate = State.VM_MAIN_MENU; cd.audioRelay.playSound("vm-main-menu"); cd.audioRelay.addSound("vm-pause"); break; } break; } break; case PBXEventHandler.SOUND: switch (cd.vmstate) { case VM_GREETING: if (interrupted) break; cd.vmstate = State.VM_REC_MSG_BEEP; cd.audioRelay.playSound("vm-beep"); break; case VM_REC_MSG_BEEP: cd.vmstate = State.VM_REC_MSG; api.log(cd, "Recording voicemail for ext " + cd.dialed); api.makePath(Paths.lib + "voicemail/" + cd.dialed); cd.vmrecfn = Paths.lib + "voicemail/" + cd.dialed + "/msg-new-" + System.currentTimeMillis(); cd.audioRelay.recordSoundFull(cd.vmrecfn, 5 * 60); cd.vmattempts = 0; break; case VM_REC_MSG: if (cd.audioRelay.getRecordingLength() < 3) { deleteRecording(cd); cd.audioRelay.playSound("vm-msg"); cd.audioRelay.addSound("vm-too-short"); cd.vmstate = State.VM_REC_MSG_BEEP; cd.audioRelay.playSound("vm-beep"); } else { cd.vmstate = State.VM_REC_MSG_MENU; cd.audioRelay.playSound("vm-rec-menu"); cd.audioRelay.addSound("vm-pause"); } break; case VM_REC_MSG_MENU: case VM_REC_GREETING_MENU: if (interrupted) break; cd.vmattempts++; if (cd.vmattempts > 3) { hangup(cd); } else { cd.audioRelay.playSound("vm-rec-menu"); cd.audioRelay.addSound("vm-pause"); } break; case VM_REC_GREETING_BEEP: cd.vmstate = State.VM_REC_GREETING; api.log(cd, "Recording voicemail greeting for ext " + cd.dialed); api.makePath(Paths.lib + "voicemail/" + cd.dialed); cd.vmrecfn = Paths.lib + "voicemail/" + cd.dialed + "/new-greeting"; cd.audioRelay.recordSoundFull(cd.vmrecfn, 5 * 60); cd.vmattempts = 0; break; case VM_REC_GREETING: if (cd.audioRelay.getRecordingLength() < 5) { deleteRecording(cd); cd.audioRelay.playSound("vm-msg"); cd.audioRelay.addSound("vm-too-short"); cd.vmstate = State.VM_REC_GREETING_BEEP; cd.audioRelay.playSound("vm-beep"); } else { cd.vmstate = State.VM_REC_GREETING_MENU; cd.audioRelay.playSound("vm-rec-menu"); cd.audioRelay.addSound("vm-pause"); } break; case VM_PASSWORD: if (interrupted) break; checkPassword(cd); break; case VM_BAD_PASSWORD: if (cd.vmattempts > 3) { cd.vmstate = State.VM_GOODBYE; cd.audioRelay.playSound("vm-goodbye"); } else { cd.vmstate = State.VM_PASSWORD; cd.vmstr = ""; cd.audioRelay.playSound("vm-enter-password"); } break; case VM_MAIN_MENU: if (interrupted) break; cd.vmattempts++; if (cd.vmattempts > 3) { hangup(cd); } else { cd.audioRelay.playSound("vm-main-menu"); cd.audioRelay.addSound("vm-pause"); } break; case VM_PLAY_MSG: if (interrupted) break; cd.vmattempts++; if (cd.vmattempts > 3) { cd.vmstate = State.VM_MAIN_MENU; break; } cd.audioRelay.playSound("vm-msg-menu"); cd.audioRelay.addSound("vm-pause"); break; case VM_GOODBYE: hangup(cd); break; } break; } } public void samples(CallDetailsPBX cd, short sam[]) { System.arraycopy(sam, 0, RTPRelay.silence, 0, 160); } public void video(CallDetailsPBX cd, byte data[], int off, int len) {} //private code protected void start(CallDetailsPBX cd, SQL sql) { String lang = "en"; //TODO : query for ext cd.vmstate = State.VM_GREETING; cd.vmstr = ""; cd.vmpass = sql.select1value("SELECT value FROM extopts WHERE ext=" + sql.quote(cd.dialed) + " AND id='vmpass'"); cd.audioRelay.setLang(lang); cd.audioRelay.setRawMode(false); if (api.getExtension(cd.fromnumber) != null) { //NAT src cd.src.sdp.getFirstAudioStream().port = -1; } cd.audioRelay.start_src(cd.src.sdp.getFirstAudioStream()); File file; String fn = Paths.lib + "voicemail/" + cd.dialed + "/greeting.wav"; try { file = new File(fn); if (file.exists()) { cd.audioRelay.playSoundFull(fn); } else { cd.audioRelay.playSound("vm-greeting"); } } catch (Exception e) {} } private void hangup(CallDetailsPBX cd) { cd.cmd = "BYE"; cd.src.cseq++; api.issue(cd, null, false, true); if (cd.audioRelay != null) { cd.audioRelay.uninit(); cd.audioRelay = null; } } private void checkPassword(CallDetailsPBX cd) { //check passcode cd.vmattempts++; if (!cd.vmstr.equals(cd.vmpass)) { cd.vmstate = State.VM_BAD_PASSWORD; cd.audioRelay.playSound("vm-incorrect"); } else { cd.vmstate = State.VM_MAIN_MENU; cd.vmattempts = 0; cd.audioRelay.playSound("vm-main-menu"); cd.audioRelay.addSound("vm-pause"); } } private boolean getMsgLists(CallDetailsPBX cd) { try { File file = new File(Paths.lib + "voicemail/" + cd.dialed + "/"); cd.vmlistnew = file.list(new FilenameFilter() { public boolean accept(File dir, String fn) { return (fn.startsWith("msg-new-") && fn.endsWith(".wav")); } }); if ((cd.vmlistnew != null) && (cd.vmlistnew.length == 0)) cd.vmlistnew = null; cd.vmlistold = file.list(new FilenameFilter() { public boolean accept(File dir, String fn) { return (fn.startsWith("msg-old-") && fn.endsWith(".wav")); } }); if ((cd.vmlistold != null) && (cd.vmlistold.length == 0)) cd.vmlistold = null; } catch (Exception e) { } if ((cd.vmlistnew == null) && (cd.vmlistold == null)) { cd.vmstate = State.VM_MAIN_MENU; cd.audioRelay.playSound("vm-no-msgs"); return false; } else if (cd.vmlistnew == null) { cd.vmnew = false; } else { cd.vmnew = true; } cd.vmstate = State.VM_PLAY_MSG; cd.vmpos = 0; return true; } private void playMsg(CallDetailsPBX cd) { cd.vmstate = State.VM_PLAY_MSG; if (cd.vmnew) cd.audioRelay.playSound("vm-new"); else cd.audioRelay.playSound("vm-old"); cd.audioRelay.addSound("vm-msg"); cd.audioRelay.addNumber(cd.vmpos+1); if (cd.vmnew) cd.audioRelay.addSoundFull(Paths.lib + "voicemail/" + cd.dialed + "/" + cd.vmlistnew[cd.vmpos]); else cd.audioRelay.addSoundFull(Paths.lib + "voicemail/" + cd.dialed + "/" + cd.vmlistold[cd.vmpos]); cd.audioRelay.addSound("vm-msg-menu"); cd.audioRelay.addSound("vm-pause"); } private void nextMsg(CallDetailsPBX cd) { if (cd.vmnew) { renameMsg(cd); cd.vmpos++; if (cd.vmpos == cd.vmlistnew.length) { if (cd.vmlistold == null) { cd.vmpos = -1; } else { cd.vmpos = 0; cd.vmnew = false; } } } else { cd.vmpos++; if (cd.vmpos == cd.vmlistold.length) { cd.vmpos = -1; } } if (cd.vmpos == -1) { cd.vmstate = State.VM_MAIN_MENU; cd.audioRelay.playSound("vm-end-msgs"); cd.audioRelay.addSound("vm-main-menu"); cd.audioRelay.addSound("vm-pause"); } else { cd.audioRelay.playSound("vm-next"); cd.audioRelay.addSound("vm-msg"); playMsg(cd); } } private void renameMsg(CallDetailsPBX cd) { //rename 'new' to 'old' String fnnew = cd.vmlistnew[cd.vmpos]; String fnold = fnnew.replaceAll("new", "old"); try { File fnew = new File(fnnew); File fold = new File(fnold); fnew.renameTo(fold); } catch (Exception e) { api.log(cd, "VM : Unable to rename 'new' msg to 'old' : " + fnnew); } } private void deleteMsg(CallDetailsPBX cd) { File file = null; try { file = new File(Paths.lib + "voicemail/" + cd.dialed + "/" + (cd.vmnew ? cd.vmlistnew[cd.vmpos] : cd.vmlistold[cd.vmpos]) ); file.delete(); cd.audioRelay.playSound("vm-msg"); cd.audioRelay.addSound("vm-deleted"); } catch (Exception e) { if (file != null) api.log(cd, "Failed to delete VM msg : " + file.toString() ); } } private void saveMsg(CallDetailsPBX cd) { //TODO : for now msgs are never auto deleted } private void deleteRecording(CallDetailsPBX cd) { File file = null; try { file = new File( cd.vmrecfn + ".wav" ); file.delete(); } catch (Exception e) { if (file != null) api.log(cd, "Failed to delete VM msg : " + file.toString() ); } } private void saveNewGreeting(CallDetailsPBX cd) { File file, newFile; try { file = new File(Paths.lib + "voicemail/" + cd.dialed + "/greeting.wav"); file.delete(); newFile = new File(cd.vmrecfn + ".wav"); newFile.renameTo(file); } catch (Exception e) { api.log(cd, "Failed to save new VM greeting for ext " + cd.dialed ); } } }