package jpbx.plugins.core; /** Queues (ACD : Automatic Call Distributor) * * @author pquiring * * Created : Jun 27, 2014 */ import java.util.*; import javaforce.*; import javaforce.voip.*; import jpbx.core.*; public class Queues implements Plugin, DialChain, PBXEventHandler { public final static int pid = 25; //priority private static PBXAPI api; public enum MemberState { WELCOME, WAITING, CALLING, CONNECTED } public static class Queue { public ArrayList<CallDetailsPBX> queue = new ArrayList<CallDetailsPBX>(); public String agentList[]; public String ext; public void add(CallDetailsPBX cd) { synchronized(this) { queue.add(cd); } } public void remove(CallDetailsPBX cd) { synchronized(this) { queue.remove(cd); } } public void process() { synchronized(this) { if (queue.isEmpty()) { return; } callAgents(); } } private void callAgents() { CallDetailsPBX member = queue.get(0); switch (member.qstate) { case WELCOME: return; case WAITING: member.qstate = MemberState.CALLING; break; } if (member.agents == null) { member.agents = new CallDetailsPBX[agentList.length]; } if (member.pids == null) { member.pids = new int[agentList.length]; } SQL sql = new SQL(); if (!sql.connect(Service.jdbc)) return; for(int a=0;a<agentList.length;a++) { if (agentList[a].length() == 0) continue; JFLog.log("Calling agent:" + agentList[a]); if (member.agents[a] != null) continue; //call agents using dial chain (see Extension/Trunks) String callid = member.sip.getcallid(); //generate a new callid CallDetailsPBX agent = (CallDetailsPBX)member.sip.getCallDetailsServer(callid); agent.isAgent = true; agent.member = member; agent.user = member.user; agent.dialed = agentList[a]; agent.src.contact = member.src.contact; agent.src.branch = member.src.branch; agent.src.to = new String[] {"Agent", agentList[a], "127.0.0.1"}; agent.src.from = member.src.from.clone(); agent.src.host = "127.0.0.1"; agent.src.port = api.getlocalport(); agent.src.sdp = (SDP)member.src.sdp.clone(); agent.pbxsrc = (CallDetails.SideDetails)agent.src.clone(); agent.authorized = true; agent.queue = member.queue; agent.cmd = "INVITE"; member.pids[a] = api.onInvite(agent, sql, true, 0); //simulate call from PBX to agent //BUG : another inbound msg here could bypass Queues control agent.pid = pid; member.agents[a] = agent; } sql.close(); } } private HashMap<String, Queue> queues = new HashMap<String, Queue>(); private Timer timer; public void init(PBXAPI api) { this.api = api; api.hookDialChain(this); timer = new Timer(); timer.schedule(new TimerTask() {public void run() { synchronized(queues) { Queue list[] = queues.values().toArray(new Queue[queues.size()]); for(int a=0;a<list.length;a++) { list[a].process(); } } }}, 30 * 1000, 30 * 1000); JFLog.log("Queues plugin init"); } public void uninit(PBXAPI api) { timer.cancel(); timer = null; api.unhookDialChain(this); JFLog.log("Queues plugin uninit"); } public void install(PBXAPI api) { } public void uninstall(PBXAPI api) { } 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 queues WHERE ext=" + sql.quote(cd.dialed)); if (ext == null) return -1; //a queue is not being dialed if (cd.invited) { //reINVITE (just get new codecs) api.log(cd, "ACD : 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; } 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) { try { if (cd.isAgent) { int idx = -1; CallDetailsPBX agent = cd; CallDetailsPBX member = cd.member; synchronized(member.queue) { for(int a=0;a<member.agents.length;a++) { if (member.agents[a] == agent) { idx = a; break; } } if (idx == -1) { JFLog.log(pid, "Lost track of agents???"); return; } if (member.agent == agent) { //dup msg return; } api.onSuccess(cd, sql, src, member.pids[idx]); //this will generate dup msg if (member.qstate == MemberState.CALLING) { //connect current member to agent member.qstate = MemberState.CONNECTED; member.audioRelay.setRawMode(true); //switch to relay mode api.connect(member, agent); member.audioRelay.setMOH(false); member.queue.remove(member); member.agent = agent; //send cancel to remaining agents for(int a=0;a<member.agents.length;a++) { if (a == idx) continue; agent = member.agents[a]; if (agent == null) continue; agent.cmd = "CANCEL"; agent.dst.cseq++; api.issue(agent, null, false, false); member.agents[a] = null; } } else { //agent too late - send bye member.agents[idx] = null; cd.cmd = "BYE"; cd.dst.cseq++; api.issue(cd, null, false, false); } member.queue.process(); } } else { JFLog.log("Unknown???"); } } catch (Exception e) { JFLog.log(e); } } public void onCancel(CallDetailsPBX cd, SQL sql, boolean src) { onBye(cd, sql, src); } public void onBye(CallDetailsPBX cd, SQL sql, boolean src) { if (cd.queue == null) return; synchronized(cd.queue) { if (src) { if (cd.isAgent) { JFLog.log("Assertion : Queue Agent must be dst"); return; } //member bye api.reply(cd, 200, "OK", null, false, true); if (cd.agent != null) { cd.agent.cmd = "BYE"; cd.agent.dst.cseq++; api.issue(cd.agent, null, false, false); api.disconnect(cd.agent); cd.agent = null; } api.disconnect(cd); } else { if (!cd.isAgent) { JFLog.log("Assertion : Queue Member must be src"); return; } //agent bye api.reply(cd, 200, "OK", null, false, false); api.disconnect(cd); for(int a=0;a<cd.member.agents.length;a++) { if (cd.member.agents[a] == cd) { cd.member.agents[a] = null; break; } } if (cd.member.qstate != MemberState.CONNECTED) return; if (cd.member.agent != cd) return; cd.member.cmd = "BYE"; cd.member.src.cseq++; api.issue(cd.member, null, false, true); api.disconnect(cd.member); cd = cd.member; } //now disconnect all remaining agents and remove from queue if (cd.agents == null) return; for(int a=0;a<cd.agents.length;a++) { CallDetailsPBX agent = cd.agents[a]; if (agent == null) continue; agent.cmd = "CANCEL"; agent.dst.cseq++; api.issue(agent, null, false, false); cd.agents[a] = null; } cd.queue.remove(cd); } } public void onError(CallDetailsPBX cd, SQL sql, int code, boolean src) { onBye(cd, sql, src); } public void onTrying(CallDetailsPBX cd, SQL sql, boolean src) { } public void onFeature(CallDetailsPBX cd, SQL sql, String cmd, String cmddata, boolean src) { } //PBXEventHandler public void event(CallDetailsPBX cd, int type, char digit, boolean interrupted) { try { switch (type) { case PBXEventHandler.SOUND: if (cd.qstate == MemberState.WELCOME) { cd.qstate = MemberState.WAITING; cd.audioRelay.setMOH(true); //play MOH until an agent picks up cd.queue.process(); } break; } } catch (Exception e) { JFLog.log(e); } } public void samples(CallDetailsPBX cd, short[] sam) {} //private code private void start(CallDetailsPBX cd, SQL sql) { String lang = "en"; //TODO : query for ext cd.qstate = MemberState.WELCOME; String message = null; synchronized(queues) { Queue q = queues.get(cd.dialed); String list[] = sql.select1value("SELECT agents FROM queues WHERE ext=" + sql.quote(cd.dialed)).split(","); message = sql.select1value("SELECT message FROM queues WHERE ext=" + sql.quote(cd.dialed)); if (q == null) { q = new Queue(); q.ext = cd.dialed; q.agentList = list; queues.put(cd.dialed, q); } else { q.agentList = list; //update } q.queue.add(cd); cd.queue = q; } 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()); cd.audioRelay.playSound(message); } }