001/** 002 * 003 * Copyright 2003-2007 Jive Software. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smackx.workgroup.agent; 019 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.logging.Level; 029import java.util.logging.Logger; 030 031import org.jivesoftware.smack.SmackException; 032import org.jivesoftware.smack.SmackException.NoResponseException; 033import org.jivesoftware.smack.SmackException.NotConnectedException; 034import org.jivesoftware.smack.StanzaCollector; 035import org.jivesoftware.smack.StanzaListener; 036import org.jivesoftware.smack.XMPPConnection; 037import org.jivesoftware.smack.XMPPException; 038import org.jivesoftware.smack.XMPPException.XMPPErrorException; 039import org.jivesoftware.smack.filter.AndFilter; 040import org.jivesoftware.smack.filter.FromMatchesFilter; 041import org.jivesoftware.smack.filter.OrFilter; 042import org.jivesoftware.smack.filter.StanzaTypeFilter; 043import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 044import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 045import org.jivesoftware.smack.packet.IQ; 046import org.jivesoftware.smack.packet.Message; 047import org.jivesoftware.smack.packet.Presence; 048import org.jivesoftware.smack.packet.PresenceBuilder; 049import org.jivesoftware.smack.packet.StandardExtensionElement; 050import org.jivesoftware.smack.packet.Stanza; 051 052import org.jivesoftware.smackx.muc.packet.MUCUser; 053import org.jivesoftware.smackx.search.ReportedData; 054import org.jivesoftware.smackx.workgroup.MetaData; 055import org.jivesoftware.smackx.workgroup.QueueUser; 056import org.jivesoftware.smackx.workgroup.WorkgroupInvitation; 057import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener; 058import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory; 059import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata; 060import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup; 061import org.jivesoftware.smackx.workgroup.ext.macros.Macros; 062import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes; 063import org.jivesoftware.smackx.workgroup.packet.AgentStatus; 064import org.jivesoftware.smackx.workgroup.packet.DepartQueuePacket; 065import org.jivesoftware.smackx.workgroup.packet.MonitorPacket; 066import org.jivesoftware.smackx.workgroup.packet.OccupantsInfo; 067import org.jivesoftware.smackx.workgroup.packet.OfferRequestProvider; 068import org.jivesoftware.smackx.workgroup.packet.OfferRevokeProvider; 069import org.jivesoftware.smackx.workgroup.packet.QueueDetails; 070import org.jivesoftware.smackx.workgroup.packet.QueueOverview; 071import org.jivesoftware.smackx.workgroup.packet.RoomInvitation; 072import org.jivesoftware.smackx.workgroup.packet.RoomTransfer; 073import org.jivesoftware.smackx.workgroup.packet.SessionID; 074import org.jivesoftware.smackx.workgroup.packet.Transcript; 075import org.jivesoftware.smackx.workgroup.packet.Transcripts; 076import org.jivesoftware.smackx.workgroup.settings.GenericSettings; 077import org.jivesoftware.smackx.workgroup.settings.SearchSettings; 078import org.jivesoftware.smackx.xdata.form.FillableForm; 079import org.jivesoftware.smackx.xdata.form.Form; 080 081import org.jxmpp.jid.EntityBareJid; 082import org.jxmpp.jid.EntityJid; 083import org.jxmpp.jid.Jid; 084import org.jxmpp.jid.parts.Resourcepart; 085import org.jxmpp.stringprep.XmppStringprepException; 086 087/** 088 * This class embodies the agent's active presence within a given workgroup. The application 089 * should have N instances of this class, where N is the number of workgroups to which the 090 * owning agent of the application belongs. This class provides all functionality that a 091 * session within a given workgroup is expected to have from an agent's perspective -- setting 092 * the status, tracking the status of queues to which the agent belongs within the workgroup, and 093 * dequeuing customers. 094 * 095 * @author Matt Tucker 096 * @author Derek DeMoro 097 */ 098public class AgentSession { 099 private static final Logger LOGGER = Logger.getLogger(AgentSession.class.getName()); 100 101 private final XMPPConnection connection; 102 103 private final EntityBareJid workgroupJID; 104 105 private boolean online = false; 106 private Presence.Mode presenceMode; 107 private int maxChats; 108 private final Map<String, List<String>> metaData; 109 110 private final Map<Resourcepart, WorkgroupQueue> queues = new HashMap<>(); 111 112 private final List<OfferListener> offerListeners; 113 private final List<WorkgroupInvitationListener> invitationListeners; 114 private final List<QueueUsersListener> queueUsersListeners; 115 116 private AgentRoster agentRoster = null; 117 private final TranscriptManager transcriptManager; 118 private final TranscriptSearchManager transcriptSearchManager; 119 private final Agent agent; 120 private final StanzaListener packetListener; 121 122 /** 123 * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)} 124 * method must be called with an argument of <code>true</code> to mark the agent 125 * as available to accept chat requests. 126 * 127 * @param connection a connection instance which must have already gone through 128 * authentication. 129 * @param workgroupJID the fully qualified JID of the workgroup. 130 */ 131 public AgentSession(EntityBareJid workgroupJID, XMPPConnection connection) { 132 // Login must have been done before passing in connection. 133 if (!connection.isAuthenticated()) { 134 throw new IllegalStateException("Must login to server before creating workgroup."); 135 } 136 137 this.workgroupJID = workgroupJID; 138 this.connection = connection; 139 this.transcriptManager = new TranscriptManager(connection); 140 this.transcriptSearchManager = new TranscriptSearchManager(connection); 141 142 this.maxChats = -1; 143 144 this.metaData = new HashMap<>(); 145 146 offerListeners = new ArrayList<>(); 147 invitationListeners = new ArrayList<>(); 148 queueUsersListeners = new ArrayList<>(); 149 150 // Create a filter to listen for packets we're interested in. 151 OrFilter filter = new OrFilter( 152 new StanzaTypeFilter(Presence.class), 153 new StanzaTypeFilter(Message.class)); 154 155 packetListener = new StanzaListener() { 156 @Override 157 public void processStanza(Stanza packet) { 158 try { 159 handlePacket(packet); 160 } 161 catch (Exception e) { 162 LOGGER.log(Level.SEVERE, "Error processing packet", e); 163 } 164 } 165 }; 166 connection.addAsyncStanzaListener(packetListener, filter); 167 168 connection.registerIQRequestHandler(new AbstractIqRequestHandler( 169 OfferRequestProvider.OfferRequestPacket.ELEMENT, 170 OfferRequestProvider.OfferRequestPacket.NAMESPACE, IQ.Type.set, 171 Mode.async) { 172 173 @Override 174 public IQ handleIQRequest(IQ iqRequest) { 175 // Acknowledge the IQ set. 176 IQ reply = IQ.createResultIQ(iqRequest); 177 178 fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket) iqRequest); 179 return reply; 180 } 181 }); 182 183 connection.registerIQRequestHandler(new AbstractIqRequestHandler( 184 OfferRevokeProvider.OfferRevokePacket.ELEMENT, 185 OfferRevokeProvider.OfferRevokePacket.NAMESPACE, IQ.Type.set, 186 Mode.async) { 187 188 @Override 189 public IQ handleIQRequest(IQ iqRequest) { 190 // Acknowledge the IQ set. 191 IQ reply = IQ.createResultIQ(iqRequest); 192 193 fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket) iqRequest); 194 return reply; 195 } 196 }); 197 198 // Create the agent associated to this session 199 agent = new Agent(connection, workgroupJID); 200 } 201 202 /** 203 * Close the agent session. The underlying connection will remain opened but the 204 * stanza listeners that were added by this agent session will be removed. 205 */ 206 public void close() { 207 connection.removeAsyncStanzaListener(packetListener); 208 } 209 210 /** 211 * Returns the agent roster for the workgroup, which contains. 212 * 213 * @return the AgentRoster 214 * @throws NotConnectedException if the XMPP connection is not connected. 215 * @throws InterruptedException if the calling thread was interrupted. 216 */ 217 public AgentRoster getAgentRoster() throws NotConnectedException, InterruptedException { 218 if (agentRoster == null) { 219 agentRoster = new AgentRoster(connection, workgroupJID); 220 } 221 222 // This might be the first time the user has asked for the roster. If so, we 223 // want to wait up to 2 seconds for the server to send back the list of agents. 224 // This behavior shields API users from having to worry about the fact that the 225 // operation is asynchronous, although they'll still have to listen for changes 226 // to the roster. 227 int elapsed = 0; 228 while (!agentRoster.rosterInitialized && elapsed <= 2000) { 229 try { 230 Thread.sleep(500); 231 } 232 catch (Exception e) { 233 // Ignore 234 } 235 elapsed += 500; 236 } 237 return agentRoster; 238 } 239 240 /** 241 * Returns the agent's current presence mode. 242 * 243 * @return the agent's current presence mode. 244 */ 245 public Presence.Mode getPresenceMode() { 246 return presenceMode; 247 } 248 249 /** 250 * Returns the maximum number of chats the agent can participate in. 251 * 252 * @return the maximum number of chats the agent can participate in. 253 */ 254 public int getMaxChats() { 255 return maxChats; 256 } 257 258 /** 259 * Returns true if the agent is online with the workgroup. 260 * 261 * @return true if the agent is online with the workgroup. 262 */ 263 public boolean isOnline() { 264 return online; 265 } 266 267 /** 268 * Allows the addition of a new key-value pair to the agent's meta data, if the value is 269 * new data, the revised meta data will be rebroadcast in an agent's presence broadcast. 270 * 271 * @param key the meta data key 272 * @param val the non-null meta data value 273 * @throws XMPPException if an exception occurs. 274 * @throws SmackException if Smack detected an exceptional situation. 275 * @throws InterruptedException if the calling thread was interrupted. 276 */ 277 public void setMetaData(String key, String val) throws XMPPException, SmackException, InterruptedException { 278 synchronized (this.metaData) { 279 List<String> oldVals = metaData.get(key); 280 281 if (oldVals == null || !oldVals.get(0).equals(val)) { 282 oldVals.set(0, val); 283 284 setStatus(presenceMode, maxChats); 285 } 286 } 287 } 288 289 /** 290 * Allows the removal of data from the agent's meta data, if the key represents existing data, 291 * the revised meta data will be rebroadcast in an agent's presence broadcast. 292 * 293 * @param key the meta data key. 294 * @throws XMPPException if an exception occurs. 295 * @throws SmackException if Smack detected an exceptional situation. 296 * @throws InterruptedException if the calling thread was interrupted. 297 */ 298 public void removeMetaData(String key) throws XMPPException, SmackException, InterruptedException { 299 synchronized (this.metaData) { 300 List<String> oldVal = metaData.remove(key); 301 302 if (oldVal != null) { 303 setStatus(presenceMode, maxChats); 304 } 305 } 306 } 307 308 /** 309 * Allows the retrieval of meta data for a specified key. 310 * 311 * @param key the meta data key 312 * @return the meta data value associated with the key or <code>null</code> if the meta-data 313 * doesn't exist.. 314 */ 315 public List<String> getMetaData(String key) { 316 return metaData.get(key); 317 } 318 319 /** 320 * Sets whether the agent is online with the workgroup. If the user tries to go online with 321 * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will 322 * be thrown. 323 * 324 * @param online true to set the agent as online with the workgroup. 325 * @throws XMPPException if an error occurs setting the online status. 326 * @throws SmackException assertEquals(SmackException.Type.NO_RESPONSE_FROM_SERVER, e.getType()); 327 return; 328 * @throws InterruptedException if the calling thread was interrupted. 329 */ 330 public void setOnline(boolean online) throws XMPPException, SmackException, InterruptedException { 331 // If the online status hasn't changed, do nothing. 332 if (this.online == online) { 333 return; 334 } 335 336 Presence presence; 337 338 // If the user is going online... 339 if (online) { 340 presence = connection.getStanzaFactory().buildPresenceStanza() 341 .ofType(Presence.Type.available) 342 .to(workgroupJID) 343 .build(); 344 345 presence.addExtension(new StandardExtensionElement(AgentStatus.ELEMENT_NAME, 346 AgentStatus.NAMESPACE)); 347 348 StanzaCollector collector = this.connection.createStanzaCollectorAndSend(new AndFilter( 349 new StanzaTypeFilter(Presence.class), FromMatchesFilter.create(workgroupJID)), presence); 350 351 presence = collector.nextResultOrThrow(); 352 353 // We can safely update this iv since we didn't get any error 354 this.online = online; 355 } 356 // Otherwise the user is going offline... 357 else { 358 // Update this iv now since we don't care at this point of any error 359 this.online = online; 360 361 presence = connection.getStanzaFactory().buildPresenceStanza() 362 .ofType(Presence.Type.unavailable) 363 .to(workgroupJID) 364 .build(); 365 presence.addExtension(new StandardExtensionElement(AgentStatus.ELEMENT_NAME, 366 AgentStatus.NAMESPACE)); 367 connection.sendStanza(presence); 368 } 369 } 370 371 /** 372 * Sets the agent's current status with the workgroup. The presence mode affects 373 * how offers are routed to the agent. The possible presence modes with their 374 * meanings are as follows:<ul> 375 * 376 * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats 377 * (equivalent to Presence.Mode.CHAT). 378 * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed. 379 * However, special case, or extreme urgency chats may still be offered to the agent. 380 * <li>Presence.Mode.AWAY -- the agent is not available and should not 381 * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul> 382 * 383 * The max chats value is the maximum number of chats the agent is willing to have 384 * routed to them at once. Some servers may be configured to only accept max chat 385 * values in a certain range; for example, between two and five. In that case, the 386 * maxChats value the agent sends may be adjusted by the server to a value within that 387 * range. 388 * 389 * @param presenceMode the presence mode of the agent. 390 * @param maxChats the maximum number of chats the agent is willing to accept. 391 * @throws XMPPException if an error occurs setting the agent status. 392 * @throws SmackException if Smack detected an exceptional situation. 393 * @throws InterruptedException if the calling thread was interrupted. 394 * @throws IllegalStateException if the agent is not online with the workgroup. 395 */ 396 public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException, SmackException, InterruptedException { 397 setStatus(presenceMode, maxChats, null); 398 } 399 400 /** 401 * Sets the agent's current status with the workgroup. The presence mode affects how offers 402 * are routed to the agent. The possible presence modes with their meanings are as follows:<ul> 403 * 404 * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats 405 * (equivalent to Presence.Mode.CHAT). 406 * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed. 407 * However, special case, or extreme urgency chats may still be offered to the agent. 408 * <li>Presence.Mode.AWAY -- the agent is not available and should not 409 * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul> 410 * 411 * The max chats value is the maximum number of chats the agent is willing to have routed to 412 * them at once. Some servers may be configured to only accept max chat values in a certain 413 * range; for example, between two and five. In that case, the maxChats value the agent sends 414 * may be adjusted by the server to a value within that range. 415 * 416 * @param presenceMode the presence mode of the agent. 417 * @param maxChats the maximum number of chats the agent is willing to accept. 418 * @param status sets the status message of the presence update. 419 * @throws XMPPErrorException if there was an XMPP error returned. 420 * @throws NoResponseException if there was no response from the remote entity. 421 * @throws NotConnectedException if the XMPP connection is not connected. 422 * @throws InterruptedException if the calling thread was interrupted. 423 * @throws IllegalStateException if the agent is not online with the workgroup. 424 */ 425 public void setStatus(Presence.Mode presenceMode, int maxChats, String status) 426 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 427 if (!online) { 428 throw new IllegalStateException("Cannot set status when the agent is not online."); 429 } 430 431 if (presenceMode == null) { 432 presenceMode = Presence.Mode.available; 433 } 434 this.presenceMode = presenceMode; 435 this.maxChats = maxChats; 436 437 PresenceBuilder presenceBuilder = connection.getStanzaFactory().buildPresenceStanza() 438 .ofType(Presence.Type.available) 439 .setMode(presenceMode) 440 .to(workgroupJID) 441 .setStatus(status) 442 ; 443 444 // Send information about max chats and current chats as a packet extension. 445 StandardExtensionElement.Builder builder = StandardExtensionElement.builder(AgentStatus.ELEMENT_NAME, 446 AgentStatus.NAMESPACE); 447 builder.addElement("max_chats", Integer.toString(maxChats)); 448 presenceBuilder.addExtension(builder.build()); 449 presenceBuilder.addExtension(new MetaData(this.metaData)); 450 451 Presence presence = presenceBuilder.build(); 452 StanzaCollector collector = this.connection.createStanzaCollectorAndSend(new AndFilter( 453 new StanzaTypeFilter(Presence.class), 454 FromMatchesFilter.create(workgroupJID)), presence); 455 456 collector.nextResultOrThrow(); 457 } 458 459 /** 460 * Sets the agent's current status with the workgroup. The presence mode affects how offers 461 * are routed to the agent. The possible presence modes with their meanings are as follows:<ul> 462 * 463 * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats 464 * (equivalent to Presence.Mode.CHAT). 465 * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed. 466 * However, special case, or extreme urgency chats may still be offered to the agent. 467 * <li>Presence.Mode.AWAY -- the agent is not available and should not 468 * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul> 469 * 470 * @param presenceMode the presence mode of the agent. 471 * @param status sets the status message of the presence update. 472 * @throws XMPPErrorException if there was an XMPP error returned. 473 * @throws NoResponseException if there was no response from the remote entity. 474 * @throws NotConnectedException if the XMPP connection is not connected. 475 * @throws InterruptedException if the calling thread was interrupted. 476 * @throws IllegalStateException if the agent is not online with the workgroup. 477 */ 478 public void setStatus(Presence.Mode presenceMode, String status) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 479 if (!online) { 480 throw new IllegalStateException("Cannot set status when the agent is not online."); 481 } 482 483 if (presenceMode == null) { 484 presenceMode = Presence.Mode.available; 485 } 486 this.presenceMode = presenceMode; 487 488 PresenceBuilder presenceBuilder = connection.getStanzaFactory().buildPresenceStanza() 489 .ofType(Presence.Type.available) 490 .setMode(presenceMode) 491 .to(getWorkgroupJID()); 492 493 if (status != null) { 494 presenceBuilder.setStatus(status); 495 } 496 497 Presence presence = presenceBuilder.build(); 498 presence.addExtension(new MetaData(this.metaData)); 499 500 StanzaCollector collector = this.connection.createStanzaCollectorAndSend(new AndFilter(new StanzaTypeFilter(Presence.class), 501 FromMatchesFilter.create(workgroupJID)), presence); 502 503 collector.nextResultOrThrow(); 504 } 505 506 /** 507 * Removes a user from the workgroup queue. This is an administrative action that the 508 * 509 * The agent is not guaranteed of having privileges to perform this action; an exception 510 * denying the request may be thrown. 511 * 512 * @param userID the ID of the user to remove. 513 * @throws XMPPException if an exception occurs. 514 * @throws NotConnectedException if the XMPP connection is not connected. 515 * @throws InterruptedException if the calling thread was interrupted. 516 */ 517 public void dequeueUser(EntityJid userID) throws XMPPException, NotConnectedException, InterruptedException { 518 // todo: this method simply won't work right now. 519 DepartQueuePacket departPacket = new DepartQueuePacket(workgroupJID, userID); 520 521 // PENDING 522 this.connection.sendStanza(departPacket); 523 } 524 525 /** 526 * Returns the transcripts of a given user. The answer will contain the complete history of 527 * conversations that a user had. 528 * 529 * @param userID the id of the user to get his conversations. 530 * @return the transcripts of a given user. 531 * @throws XMPPException if an error occurs while getting the information. 532 * @throws SmackException if Smack detected an exceptional situation. 533 * @throws InterruptedException if the calling thread was interrupted. 534 */ 535 public Transcripts getTranscripts(Jid userID) throws XMPPException, SmackException, InterruptedException { 536 return transcriptManager.getTranscripts(workgroupJID, userID); 537 } 538 539 /** 540 * Returns the full conversation transcript of a given session. 541 * 542 * @param sessionID the id of the session to get the full transcript. 543 * @return the full conversation transcript of a given session. 544 * @throws XMPPException if an error occurs while getting the information. 545 * @throws SmackException if Smack detected an exceptional situation. 546 * @throws InterruptedException if the calling thread was interrupted. 547 */ 548 public Transcript getTranscript(String sessionID) throws XMPPException, SmackException, InterruptedException { 549 return transcriptManager.getTranscript(workgroupJID, sessionID); 550 } 551 552 /** 553 * Returns the Form to use for searching transcripts. It is unlikely that the server 554 * will change the form (without a restart) so it is safe to keep the returned form 555 * for future submissions. 556 * 557 * @return the Form to use for searching transcripts. 558 * @throws XMPPException if an error occurs while sending the request to the server. 559 * @throws SmackException if Smack detected an exceptional situation. 560 * @throws InterruptedException if the calling thread was interrupted. 561 */ 562 public Form getTranscriptSearchForm() throws XMPPException, SmackException, InterruptedException { 563 return transcriptSearchManager.getSearchForm(workgroupJID.asDomainBareJid()); 564 } 565 566 /** 567 * Submits the completed form and returns the result of the transcript search. The result 568 * will include all the data returned from the server so be careful with the amount of 569 * data that the search may return. 570 * 571 * @param completedForm the filled out search form. 572 * @return the result of the transcript search. 573 * @throws SmackException if Smack detected an exceptional situation. 574 * @throws XMPPException if an XMPP protocol error was received. 575 * @throws InterruptedException if the calling thread was interrupted. 576 */ 577 public ReportedData searchTranscripts(FillableForm completedForm) throws XMPPException, SmackException, InterruptedException { 578 return transcriptSearchManager.submitSearch(workgroupJID.asDomainBareJid(), 579 completedForm); 580 } 581 582 /** 583 * Asks the workgroup for information about the occupants of the specified room. The returned 584 * information will include the real JID of the occupants, the nickname of the user in the 585 * room as well as the date when the user joined the room. 586 * 587 * @param roomID the room to get information about its occupants. 588 * @return information about the occupants of the specified room. 589 * @throws XMPPErrorException if there was an XMPP error returned. 590 * @throws NoResponseException if there was no response from the remote entity. 591 * @throws NotConnectedException if the XMPP connection is not connected. 592 * @throws InterruptedException if the calling thread was interrupted. 593 */ 594 public OccupantsInfo getOccupantsInfo(String roomID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 595 OccupantsInfo request = new OccupantsInfo(roomID); 596 request.setType(IQ.Type.get); 597 request.setTo(workgroupJID); 598 599 OccupantsInfo response = (OccupantsInfo) connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 600 return response; 601 } 602 603 /** 604 * Get workgroup JID. 605 * @return the fully-qualified name of the workgroup for which this session exists 606 */ 607 public Jid getWorkgroupJID() { 608 return workgroupJID; 609 } 610 611 /** 612 * Returns the Agent associated to this session. 613 * 614 * @return the Agent associated to this session. 615 */ 616 public Agent getAgent() { 617 return agent; 618 } 619 620 /** 621 * Get queue. 622 * 623 * @param queueName the name of the queue 624 * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists 625 */ 626 public WorkgroupQueue getQueue(String queueName) { 627 Resourcepart queueNameResourcepart; 628 try { 629 queueNameResourcepart = Resourcepart.from(queueName); 630 } 631 catch (XmppStringprepException e) { 632 throw new IllegalArgumentException(e); 633 } 634 return getQueue(queueNameResourcepart); 635 } 636 637 /** 638 * Get queue. 639 * 640 * @param queueName the name of the queue 641 * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists 642 */ 643 public WorkgroupQueue getQueue(Resourcepart queueName) { 644 return queues.get(queueName); 645 } 646 647 public Iterator<WorkgroupQueue> getQueues() { 648 return Collections.unmodifiableMap(new HashMap<>(queues)).values().iterator(); 649 } 650 651 public void addQueueUsersListener(QueueUsersListener listener) { 652 synchronized (queueUsersListeners) { 653 if (!queueUsersListeners.contains(listener)) { 654 queueUsersListeners.add(listener); 655 } 656 } 657 } 658 659 public void removeQueueUsersListener(QueueUsersListener listener) { 660 synchronized (queueUsersListeners) { 661 queueUsersListeners.remove(listener); 662 } 663 } 664 665 /** 666 * Adds an offer listener. 667 * 668 * @param offerListener the offer listener. 669 */ 670 public void addOfferListener(OfferListener offerListener) { 671 synchronized (offerListeners) { 672 if (!offerListeners.contains(offerListener)) { 673 offerListeners.add(offerListener); 674 } 675 } 676 } 677 678 /** 679 * Removes an offer listener. 680 * 681 * @param offerListener the offer listener. 682 */ 683 public void removeOfferListener(OfferListener offerListener) { 684 synchronized (offerListeners) { 685 offerListeners.remove(offerListener); 686 } 687 } 688 689 /** 690 * Adds an invitation listener. 691 * 692 * @param invitationListener the invitation listener. 693 */ 694 public void addInvitationListener(WorkgroupInvitationListener invitationListener) { 695 synchronized (invitationListeners) { 696 if (!invitationListeners.contains(invitationListener)) { 697 invitationListeners.add(invitationListener); 698 } 699 } 700 } 701 702 /** 703 * Removes an invitation listener. 704 * 705 * @param invitationListener the invitation listener. 706 */ 707 public void removeInvitationListener(WorkgroupInvitationListener invitationListener) { 708 synchronized (invitationListeners) { 709 invitationListeners.remove(invitationListener); 710 } 711 } 712 713 private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) { 714 Offer offer = new Offer(this.connection, this, requestPacket.getUserID(), 715 requestPacket.getUserJID(), this.getWorkgroupJID(), 716 new Date(new Date().getTime() + (requestPacket.getTimeout() * 1000)), 717 requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent()); 718 719 synchronized (offerListeners) { 720 for (OfferListener listener : offerListeners) { 721 listener.offerReceived(offer); 722 } 723 } 724 } 725 726 private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) { 727 RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(), 728 this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date()); 729 730 synchronized (offerListeners) { 731 for (OfferListener listener : offerListeners) { 732 listener.offerRevoked(revokedOffer); 733 } 734 } 735 } 736 737 private void fireInvitationEvent(Jid groupChatJID, String sessionID, String body, 738 Jid from, Map<String, List<String>> metaData) { 739 WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID, 740 workgroupJID, sessionID, body, from, metaData); 741 742 synchronized (invitationListeners) { 743 for (WorkgroupInvitationListener listener : invitationListeners) { 744 listener.invitationReceived(invitation); 745 } 746 } 747 } 748 749 private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status, 750 int averageWaitTime, Date oldestEntry, Set<QueueUser> users) { 751 synchronized (queueUsersListeners) { 752 for (QueueUsersListener listener : queueUsersListeners) { 753 if (status != null) { 754 listener.statusUpdated(queue, status); 755 } 756 if (averageWaitTime != -1) { 757 listener.averageWaitTimeUpdated(queue, averageWaitTime); 758 } 759 if (oldestEntry != null) { 760 listener.oldestEntryUpdated(queue, oldestEntry); 761 } 762 if (users != null) { 763 listener.usersUpdated(queue, users); 764 } 765 } 766 } 767 } 768 769 // PacketListener Implementation. 770 771 private void handlePacket(Stanza packet) { 772 if (packet instanceof Presence) { 773 Presence presence = (Presence) packet; 774 775 // The workgroup can send us a number of different presence packets. We 776 // check for different packet extensions to see what type of presence 777 // packet it is. 778 779 Resourcepart queueName = presence.getFrom().getResourceOrNull(); 780 WorkgroupQueue queue = queues.get(queueName); 781 // If there isn't already an entry for the queue, create a new one. 782 if (queue == null) { 783 queue = new WorkgroupQueue(queueName); 784 queues.put(queueName, queue); 785 } 786 787 // QueueOverview packet extensions contain basic information about a queue. 788 QueueOverview queueOverview = (QueueOverview) presence.getExtensionElement(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE); 789 if (queueOverview != null) { 790 if (queueOverview.getStatus() == null) { 791 queue.setStatus(WorkgroupQueue.Status.CLOSED); 792 } 793 else { 794 queue.setStatus(queueOverview.getStatus()); 795 } 796 queue.setAverageWaitTime(queueOverview.getAverageWaitTime()); 797 queue.setOldestEntry(queueOverview.getOldestEntry()); 798 // Fire event. 799 fireQueueUsersEvent(queue, queueOverview.getStatus(), 800 queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(), 801 null); 802 return; 803 } 804 805 // QueueDetails packet extensions contain information about the users in 806 // a queue. 807 QueueDetails queueDetails = (QueueDetails) packet.getExtensionElement(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE); 808 if (queueDetails != null) { 809 queue.setUsers(queueDetails.getUsers()); 810 // Fire event. 811 fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers()); 812 return; 813 } 814 815 // Notify agent packets gives an overview of agent activity in a queue. 816 StandardExtensionElement notifyAgents = (StandardExtensionElement) presence.getExtensionElement("notify-agents", "http://jabber.org/protocol/workgroup"); 817 if (notifyAgents != null) { 818 int currentChats = Integer.parseInt(notifyAgents.getFirstElement("current-chats", "http://jabber.org/protocol/workgroup").getText()); 819 int maxChats = Integer.parseInt(notifyAgents.getFirstElement("max-chats", "http://jabber.org/protocol/workgroup").getText()); 820 queue.setCurrentChats(currentChats); 821 queue.setMaxChats(maxChats); 822 // Fire event. 823 // TODO: might need another event for current chats and max chats of queue 824 return; 825 } 826 } 827 else if (packet instanceof Message) { 828 Message message = (Message) packet; 829 830 // Check if a room invitation was sent and if the sender is the workgroup 831 MUCUser mucUser = MUCUser.from(message); 832 MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null; 833 if (invite != null && workgroupJID.equals(invite.getFrom())) { 834 String sessionID = null; 835 Map<String, List<String>> metaData = null; 836 837 SessionID sessionIDExt = (SessionID) message.getExtensionElement(SessionID.ELEMENT_NAME, 838 SessionID.NAMESPACE); 839 if (sessionIDExt != null) { 840 sessionID = sessionIDExt.getSessionID(); 841 } 842 843 MetaData metaDataExt = (MetaData) message.getExtensionElement(MetaData.ELEMENT_NAME, 844 MetaData.NAMESPACE); 845 if (metaDataExt != null) { 846 metaData = metaDataExt.getMetaData(); 847 } 848 849 this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(), 850 message.getFrom(), metaData); 851 } 852 } 853 } 854 855 /** 856 * Creates a ChatNote that will be mapped to the given chat session. 857 * 858 * @param sessionID the session id of a Chat Session. 859 * @param note the chat note to add. 860 * @throws XMPPErrorException if there was an XMPP error returned. 861 * @throws NoResponseException if there was no response from the remote entity. 862 * @throws NotConnectedException if the XMPP connection is not connected. 863 * @throws InterruptedException if the calling thread was interrupted. 864 */ 865 public void setNote(String sessionID, String note) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 866 ChatNotes notes = new ChatNotes(); 867 notes.setType(IQ.Type.set); 868 notes.setTo(workgroupJID); 869 notes.setSessionID(sessionID); 870 notes.setNotes(note); 871 connection.createStanzaCollectorAndSend(notes).nextResultOrThrow(); 872 } 873 874 /** 875 * Retrieves the ChatNote associated with a given chat session. 876 * 877 * @param sessionID the sessionID of the chat session. 878 * @return the <code>ChatNote</code> associated with a given chat session. 879 * @throws XMPPErrorException if an error occurs while retrieving the ChatNote. 880 * @throws NoResponseException if there was no response from the remote entity. 881 * @throws NotConnectedException if the XMPP connection is not connected. 882 * @throws InterruptedException if the calling thread was interrupted. 883 */ 884 public ChatNotes getNote(String sessionID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 885 ChatNotes request = new ChatNotes(); 886 request.setType(IQ.Type.get); 887 request.setTo(workgroupJID); 888 request.setSessionID(sessionID); 889 890 ChatNotes response = connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 891 return response; 892 } 893 894 /** 895 * Retrieves the AgentChatHistory associated with a particular agent jid. 896 * 897 * @param jid the jid of the agent. 898 * @param maxSessions the max number of sessions to retrieve. 899 * @param startDate point in time from which on history should get retrieved. 900 * @return the chat history associated with a given jid. 901 * @throws XMPPException if an error occurs while retrieving the AgentChatHistory. 902 * @throws NotConnectedException if the XMPP connection is not connected. 903 * @throws InterruptedException if the calling thread was interrupted. 904 */ 905 public AgentChatHistory getAgentHistory(EntityBareJid jid, int maxSessions, Date startDate) throws XMPPException, NotConnectedException, InterruptedException { 906 AgentChatHistory request; 907 if (startDate != null) { 908 request = new AgentChatHistory(jid, maxSessions, startDate); 909 } 910 else { 911 request = new AgentChatHistory(jid, maxSessions); 912 } 913 914 request.setType(IQ.Type.get); 915 request.setTo(workgroupJID); 916 917 AgentChatHistory response = connection.createStanzaCollectorAndSend( 918 request).nextResult(); 919 920 return response; 921 } 922 923 /** 924 * Asks the workgroup for it's Search Settings. 925 * 926 * @return SearchSettings the search settings for this workgroup. 927 * @throws XMPPErrorException if there was an XMPP error returned. 928 * @throws NoResponseException if there was no response from the remote entity. 929 * @throws NotConnectedException if the XMPP connection is not connected. 930 * @throws InterruptedException if the calling thread was interrupted. 931 */ 932 public SearchSettings getSearchSettings() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 933 SearchSettings request = new SearchSettings(); 934 request.setType(IQ.Type.get); 935 request.setTo(workgroupJID); 936 937 SearchSettings response = connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 938 return response; 939 } 940 941 /** 942 * Asks the workgroup for it's Global Macros. 943 * 944 * @param global true to retrieve global macros, otherwise false for personal macros. 945 * @return MacroGroup the root macro group. 946 * @throws XMPPErrorException if an error occurs while getting information from the server. 947 * @throws NoResponseException if there was no response from the remote entity. 948 * @throws NotConnectedException if the XMPP connection is not connected. 949 * @throws InterruptedException if the calling thread was interrupted. 950 */ 951 public MacroGroup getMacros(boolean global) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 952 Macros request = new Macros(); 953 request.setType(IQ.Type.get); 954 request.setTo(workgroupJID); 955 request.setPersonal(!global); 956 957 Macros response = connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 958 return response.getRootGroup(); 959 } 960 961 /** 962 * Persists the Personal Macro for an agent. 963 * 964 * @param group the macro group to save. 965 * @throws XMPPErrorException if there was an XMPP error returned. 966 * @throws NoResponseException if there was no response from the remote entity. 967 * @throws NotConnectedException if the XMPP connection is not connected. 968 * @throws InterruptedException if the calling thread was interrupted. 969 */ 970 public void saveMacros(MacroGroup group) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 971 Macros request = new Macros(); 972 request.setType(IQ.Type.set); 973 request.setTo(workgroupJID); 974 request.setPersonal(true); 975 request.setPersonalMacroGroup(group); 976 977 connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 978 } 979 980 /** 981 * Query for metadata associated with a session id. 982 * 983 * @param sessionID the sessionID to query for. 984 * @return Map a map of all metadata associated with the sessionID. 985 * @throws XMPPException if an error occurs while getting information from the server. 986 * @throws NotConnectedException if the XMPP connection is not connected. 987 * @throws InterruptedException if the calling thread was interrupted. 988 */ 989 public Map<String, List<String>> getChatMetadata(String sessionID) throws XMPPException, NotConnectedException, InterruptedException { 990 ChatMetadata request = new ChatMetadata(); 991 request.setType(IQ.Type.get); 992 request.setTo(workgroupJID); 993 request.setSessionID(sessionID); 994 995 ChatMetadata response = connection.createStanzaCollectorAndSend(request).nextResult(); 996 997 return response.getMetadata(); 998 } 999 1000 /** 1001 * Invites a user or agent to an existing session support. The provided invitee's JID can be of 1002 * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service 1003 * will decide the best agent to receive the invitation.<p> 1004 * 1005 * This method will return either when the service returned an ACK of the request or if an error occurred 1006 * while requesting the invitation. After sending the ACK the service will send the invitation to the target 1007 * entity. When dealing with agents the common sequence of offer-response will be followed. However, when 1008 * sending an invitation to a user a standard MUC invitation will be sent.<p> 1009 * 1010 * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make 1011 * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee 1012 * accepted the offer but failed to join the room. 1013 * 1014 * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the 1015 * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room or 1016 * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases 1017 * (or other failing cases) the inviter will get an error message with the failed notification. 1018 * 1019 * @param type type of entity that will get the invitation. 1020 * @param invitee JID of entity that will get the invitation. 1021 * @param sessionID ID of the support session that the invitee is being invited. 1022 * @param reason the reason of the invitation. 1023 * @throws XMPPErrorException if the sender of the invitation is not an agent or the service failed to process 1024 * the request. 1025 * @throws NoResponseException if there was no response from the remote entity. 1026 * @throws NotConnectedException if the XMPP connection is not connected. 1027 * @throws InterruptedException if the calling thread was interrupted. 1028 */ 1029 public void sendRoomInvitation(RoomInvitation.Type type, Jid invitee, String sessionID, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 1030 final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason); 1031 IQ iq = new RoomInvitation.RoomInvitationIQ(invitation); 1032 iq.setType(IQ.Type.set); 1033 iq.setTo(workgroupJID); 1034 iq.setFrom(connection.getUser()); 1035 1036 connection.createStanzaCollectorAndSend(iq).nextResultOrThrow(); 1037 } 1038 1039 /** 1040 * Transfer an existing session support to another user or agent. The provided invitee's JID can be of 1041 * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service 1042 * will decide the best agent to receive the invitation.<p> 1043 * 1044 * This method will return either when the service returned an ACK of the request or if an error occurred 1045 * while requesting the transfer. After sending the ACK the service will send the invitation to the target 1046 * entity. When dealing with agents the common sequence of offer-response will be followed. However, when 1047 * sending an invitation to a user a standard MUC invitation will be sent.<p> 1048 * 1049 * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p> 1050 * 1051 * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the 1052 * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room 1053 * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases 1054 * (or other failing cases) the inviter will get an error message with the failed notification. 1055 * 1056 * @param type type of entity that will get the invitation. 1057 * @param invitee JID of entity that will get the invitation. 1058 * @param sessionID ID of the support session that the invitee is being invited. 1059 * @param reason the reason of the invitation. 1060 * @throws XMPPErrorException if the sender of the invitation is not an agent or the service failed to process 1061 * the request. 1062 * @throws NoResponseException if there was no response from the remote entity. 1063 * @throws NotConnectedException if the XMPP connection is not connected. 1064 * @throws InterruptedException if the calling thread was interrupted. 1065 */ 1066 public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 1067 final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason); 1068 IQ iq = new RoomTransfer.RoomTransferIQ(transfer); 1069 iq.setType(IQ.Type.set); 1070 iq.setTo(workgroupJID); 1071 iq.setFrom(connection.getUser()); 1072 1073 connection.createStanzaCollectorAndSend(iq).nextResultOrThrow(); 1074 } 1075 1076 /** 1077 * Returns the generic metadata of the workgroup the agent belongs to. 1078 * 1079 * @param con the XMPPConnection to use. 1080 * @param query an optional query object used to tell the server what metadata to retrieve. This can be null. 1081 * @return the settings for the workgroup. 1082 * @throws XMPPErrorException if an error occurs while sending the request to the server. 1083 * @throws NoResponseException if there was no response from the remote entity. 1084 * @throws NotConnectedException if the XMPP connection is not connected. 1085 * @throws InterruptedException if the calling thread was interrupted. 1086 */ 1087 public GenericSettings getGenericSettings(XMPPConnection con, String query) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 1088 GenericSettings setting = new GenericSettings(); 1089 setting.setType(IQ.Type.get); 1090 setting.setTo(workgroupJID); 1091 1092 GenericSettings response = connection.createStanzaCollectorAndSend( 1093 setting).nextResultOrThrow(); 1094 return response; 1095 } 1096 1097 public boolean hasMonitorPrivileges(XMPPConnection con) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 1098 MonitorPacket request = new MonitorPacket(); 1099 request.setType(IQ.Type.get); 1100 request.setTo(workgroupJID); 1101 1102 MonitorPacket response = connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 1103 return response.isMonitor(); 1104 } 1105 1106 public void makeRoomOwner(XMPPConnection con, String sessionID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 1107 MonitorPacket request = new MonitorPacket(); 1108 request.setType(IQ.Type.set); 1109 request.setTo(workgroupJID); 1110 request.setSessionID(sessionID); 1111 1112 connection.createStanzaCollectorAndSend(request).nextResultOrThrow(); 1113 } 1114}