package org.ifsoft.skype; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.*; import java.io.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.net.URL; import java.net.URLEncoder; import java.net.URLDecoder; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.httpclient.*; import org.apache.commons.httpclient.methods.*; import org.apache.commons.httpclient.params.HttpClientParams; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.group.*; import org.jivesoftware.openfire.user.*; import org.jivesoftware.openfire.roster.Roster; import org.jivesoftware.openfire.roster.RosterItem; import org.jivesoftware.openfire.roster.RosterManager; import org.jivesoftware.openfire.vcard.VCardManager; import org.jivesoftware.openfire.event.GroupEventDispatcher; import org.jivesoftware.community.http.HttpClientManager; import org.jivesoftware.community.http.impl.HttpClientManagerImpl; import org.jivesoftware.community.util.DateUtils; import org.jivesoftware.community.util.StringUtils; import org.jivesoftware.util.*; import net.sf.json.JSONException; import net.sf.json.JSONObject; import net.sf.json.JSONArray; import com.google.common.collect.*; import com.google.common.base.Predicate; import com.microsoft.aad.adal4j.AuthenticationContext; import com.microsoft.aad.adal4j.AuthenticationResult; import org.ifsoft.lync.ucwa.*; import javax.sip.*; import javax.sip.message.*; import javax.sdp.SdpFactory; import javax.sdp.SessionDescription; import javax.sdp.MediaDescription; import javax.sdp.Attribute; import org.ifsoft.sip.*; import org.jivesoftware.openfire.plugin.ofskype.OfSkypePlugin; import org.jivesoftware.smack.*; import org.jivesoftware.smack.filter.*; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smackx.workgroup.*; import org.jivesoftware.smackx.workgroup.user.Workgroup; public class SkypeClient { private static final Logger Log = LoggerFactory.getLogger(SkypeClient.class); private HttpClientManager httpClientManager = new HttpClientManagerImpl(); private TaskEngine taskEngine = TaskEngine.getInstance(); private final List pendingRequests = (List)org.ifsoft.lync.ucwa.Lists.newArrayList(); private final ProviderStatus currentStatus = new ProviderStatus(); private final ArrayDeque updateQueue = new ArrayDeque(1000); public final Map<String, LyncBuddy> buddies = new ConcurrentHashMap<String, LyncBuddy>(); public final Map<String, ChatRoom> conversations = new ConcurrentHashMap<String, ChatRoom>(); public final Map<String, String> participants = new ConcurrentHashMap<String, String>(); public final Map<String, ActivePhoneAudio> callIds = new ConcurrentHashMap<String, ActivePhoneAudio>(); private static Map<String, Presence> workgroupPresence = new HashMap<String, Presence>(); private static Map workgroups = new HashMap(); private static Map<String, XMPPConnection> globalConnections = new HashMap<String, XMPPConnection>(); private final Set subscribedUsers = (Set)Sets.newHashSet(); public JID jid; public String sipUrl; public String service; public String emailAddress; public String myWorkPhoneNumber; public String myHomePhoneNumber; public String myMobilePhoneNumber; public String myOtherPhoneNumber; public String myName; public String myAvatar; public String myAvailability = ""; public String myConferenceNumber = null; public String myNote = ""; public String from = null; public String to = null; public String recordingPath = null; public String clientId = null; public String domain; public String oAuthToken = null; public RegisterProcessing registerProcessing = null; public Response response = null; public ServerTransaction serverTransaction = null; public boolean privateCall = false; public boolean incomingCall = false; public String privacyOwner = null; public String privacyRelationship = ""; public String sourceNetwork = "SameEnterprise"; private String signInData; private String autoDiscoverPath; private Pattern sipFromResourcePattern; private Pattern jsonLinePattern; private Pattern reasonPattern; private Pattern oauthPathHeaderPattern; private Thread pollingThread = null; private String host; private String password; private String user; private String oAuthGenerationUrl; private Date invalidateSubscriptionsTime; private boolean pollingRunning; private String selfApplicationResource; private String applicationPath; private String presenceSubscriptionsPath; private String searchPath; private String contactsPath; private String groupsPath; private String batchPath; private String eventsPath; private String makeMeAvailablePath; private String callForwardingSettingsPath; private String turnOffCallForwardingPath; private String simultaneousRingToContactPath; private String mePath; private AtomicInteger failCount; private String startMessagingPath; private String startPhoneAudioPath; private String myPresencePath; private String myNotePath; private String myPhotoPath; private String myPhonesPath; private String stopPhoneAudioHref = null; private String holdPhoneAudioHref = null; private String resumePhoneAudioHref = null; public SkypeClient(String username, String password, String domain, String url) { this.user = username; this.domain = domain; this.password = password; this.sipUrl = url; Log.info("SkypeClient " + username + " " + password + " " + domain + " " + url); sipFromResourcePattern = Pattern.compile("^.*?/people/([^/]*)/[^/]*$"); jsonLinePattern = Pattern.compile("^\\W*(\\{.*?\\})\\W*$"); reasonPattern = Pattern.compile("reason=\"([^\"]*)"); oauthPathHeaderPattern = Pattern.compile("^\\W*MsRtcOAuth\\W+href=\"([^\"]*)\".*"); failCount = new AtomicInteger(0); pollingRunning = false; } public String getDomain() { return domain; } public void setClientId(JID jid) throws Exception { this.jid = jid; } public void doLogin() throws Exception { //Log.info("Initializing lync client"); currentStatus.setErrorCode(ErrorCode.CONNECTING); try { getOnlineToken(); Log.info((new StringBuilder("Initializing UCWA provider. url: ")).append(host).toString()); logon(); currentStatus.setErrorCode(ErrorCode.SUCCESS); } catch(JSONException ex) { Log.error("Couldn't parse server data.", ex); currentStatus.setErrorCode(ErrorCode.INVALID_LOGIN_DATA); currentStatus.setErrorMessage(ex.getMessage()); } catch(Exception ex) { Log.error("Exception was thrown while initializing provider.", ex); currentStatus.setErrorCode(ErrorCode.FAILED); currentStatus.setErrorMessage(ex.getMessage()); } } private void getOnlineToken() { clientId = JiveGlobals.getProperty("skype.clientid." + domain, "ff8474ef-2f73-4d6a-b2ab-a6d7d8364ab2"); Log.info("getOnlineToken " + clientId); String authority = "https://login.microsoftonline.com/common/oauth2/authorize"; String resourceUri = "https://webdir1e.online.lync.com"; String rootResource = "https://webdir1e.online.lync.com/Autodiscover/AutodiscoverService.svc/root/oauth/user?originalDomain=" + domain; AuthenticationContext context = null; AuthenticationResult result = null; ExecutorService service = null; try { service = Executors.newFixedThreadPool(1); context = new AuthenticationContext(authority, false, service); Future<AuthenticationResult> future = context.acquireToken(resourceUri, clientId, sipUrl, password, null); result = future.get(); if (result != null) { String tempOAuthToken = result.getAccessToken(); Log.info("SkypeClient got 1st token " + tempOAuthToken); GetMethod request = new GetMethod(rootResource); request.addRequestHeader("Authorization", "Bearer " + tempOAuthToken); URL url2 = new URL(request.getURI().toString()); HttpClient httpClient = httpClientManager.getClient(url2); int responseCode = httpClient.executeMethod(request); Log.info("SkypeClient got response " + responseCode); if(responseCode == 200) { String jsonStr = getStringFromResponse(request); Log.info("SkypeClient got user resource json\n" + jsonStr); JSONObject jsonObject = sanitizeJson(jsonStr); JSONObject myLinks = jsonObject.getJSONObject("_links"); while (myLinks.has("redirect") || myLinks.has("user")) { String redirect = null; if (myLinks.has("redirect")) redirect = myLinks.getJSONObject("redirect").getString("href"); else redirect = myLinks.getJSONObject("user").getString("href"); Log.info("SkypeClient got redirect " + redirect); GetMethod request2 = new GetMethod(redirect); URL url3 = new URL(request2.getURI().toString()); resourceUri = url3.getProtocol() + "://" + url3.getAuthority(); future = context.acquireToken(resourceUri, clientId, sipUrl, password, null); result = future.get(); if (result != null) { tempOAuthToken = result.getAccessToken(); request2.addRequestHeader("Authorization", "Bearer " + tempOAuthToken); HttpClient httpClient2 = httpClientManager.getClient(url3); int responseCode2 = httpClient2.executeMethod(request2); Log.info("SkypeClient got response " + responseCode2 + " " + tempOAuthToken); if(responseCode == 200) { jsonStr = getStringFromResponse(request2); Log.info("SkypeClient got user resource json\n" + jsonStr); jsonObject = sanitizeJson(jsonStr); myLinks = jsonObject.getJSONObject("_links"); if (myLinks.has("applications")) { applicationPath = myLinks.getJSONObject("applications").getString("href"); host = myLinks.getJSONObject("self").getString("href");; Log.info("SkypeClient found application " + applicationPath + " " + host); url3 = new URL(host); host = url3.getProtocol() + "://" + url3.getAuthority(); future = context.acquireToken(host, clientId, sipUrl, password, null); result = future.get(); if (result != null) { oAuthToken = result.getAccessToken(); Log.info("SkypeClient got final token " + oAuthToken); } } } else { break; } } else { break; } } } } } catch (Exception e) { Log.error("of_authenticate_365", e); } finally { service.shutdown(); } } private void uninitialize() { //Log.info("Disconnecting Lync provider"); pollingRunning = false; currentStatus.setErrorCode(ErrorCode.UNASSIGNED); try { if (turnOffCallForwardingPath != null) { //postRequest(turnOffCallForwardingPath, null); } MethodExecutionResult executionResult = request("DELETE", selfApplicationResource, null); //Log.info((new StringBuilder("Response from DELETE application: ")).append(executionResult.getResponseCode()).toString()); } catch(Exception e) { String message = "Failed deleting the logical application created by the UCWA Plugin."; Log.error(message, e); } invalidateSubscriptions(); } public void makeMeAvailable(String status) { makeMeAvailable(status, true, true); } public void makeMeAvailable(final String status, final boolean messaging, final boolean phoneAudio) { try { Log.info("makeAvailable " + makeMeAvailablePath); JSONObject reqBody = new JSONObject(); //reqBody.put("phoneNumber", "+441634251467"); reqBody.put("signInAs", status); JSONArray supportedMessageFormats = new JSONArray(); supportedMessageFormats.put(0, "Plain"); //supportedMessageFormats.put(1, "Html"); reqBody.put("supportedMessageFormats", supportedMessageFormats); JSONArray supportedModalities = new JSONArray(); if (messaging) supportedModalities.put(0, "Messaging"); //if (phoneAudio) supportedModalities.put(1, "PhoneAudio"); if (phoneAudio) supportedModalities.put(1, "Audio"); reqBody.put("supportedModalities", supportedModalities); postRequest(makeMeAvailablePath, reqBody); } catch(Exception e) { Log.error("Failed makeMeAvailable", e); } } public void getMyLinks() { try { Log.info("getMyLinks " + mePath); MethodExecutionResult response = getRequest(mePath); JSONObject jsonObject = response.getJson(); JSONObject myLinks = jsonObject.getJSONObject("_links"); myName = jsonObject.getString("name"); Log.info(myName + " me " + jsonObject); if (myLinks.has("presence")) myPresencePath = myLinks.getJSONObject("presence").getString("href"); if (myLinks.has("note")) myNotePath = myLinks.getJSONObject("note").getString("href"); if (myLinks.has("photo")) myPhotoPath = myLinks.getJSONObject("photo").getString("href"); if (myLinks.has("phones")) myPhonesPath = myLinks.getJSONObject("phones").getString("href"); if (myPhotoPath != null) { myAvatar = pushAvatar(sipUrl, myPhotoPath, myName); } getMePhones(); if (myLinks.has("reportMyActivity")) { String reportMyActivity = myLinks.getJSONObject("reportMyActivity").getString("href"); taskEngine.scheduleAtFixedRate(new ReportMyActivity(reportMyActivity), 0, 180000); } if (jid != null) { UserManager userManager = XMPPServer.getInstance().getUserManager(); try { User user = userManager.getUser(jid.getNode()); user.setPassword(password); } catch (UserNotFoundException e) { try { Log.info("getMyLinks creating XMPP user " + jid); userManager.createUser(jid.getNode(), password, myName, sipUrl); } catch (Exception e1) { Log.error( "getMyLinks, cannot create username (user.domain) " + jid, e1); } } } } catch(Exception e) { Log.error("Failed getMyLinks", e); } } public void getMePhones() { Log.info("getMePhones " + myPresencePath + " " + myNotePath + " " + myPhotoPath + " " + myPhonesPath); try { if (myPhonesPath != null) { MethodExecutionResult phonesResponse = getRequest(myPhonesPath); JSONArray phones = phonesResponse.getJson().getJSONObject("_embedded").getJSONArray("phone"); for(int i = 0; i < phones.length(); i++) { JSONObject phone = phones.getJSONObject(i); String number = phone.getString("number"); if ("work".equals(phone.getString("type")) && "".equals(phone) == false) myWorkPhoneNumber = number; if ("mobile".equals(phone.getString("type")) && "".equals(phone) == false) myMobilePhoneNumber = number; if ("other".equals(phone.getString("type")) && "".equals(phone) == false) myOtherPhoneNumber = number; } Log.info(myName + " phones " + myWorkPhoneNumber + " " + myMobilePhoneNumber + " " + myOtherPhoneNumber); } } catch(Exception e) { Log.error("Failed getMePhones", e); } } public void acceptWithAnswer(String url, String sdp) { try { //sdp = sdp.replace("UDP/TLS/RTP/SAVPF", "RTP/SAVP"); sdp = sdp.replace("t=0 0", "b=CT:99980\nt=0 0\na=x-devicecaps:audio:send,recv;video:send,recv"); SessionDescription sd = SdpFactory.getInstance().createSessionDescription(sdp); MediaDescription md = ((MediaDescription) sd.getMediaDescriptions(false).get(0)); Vector<Attribute> attributes = (Vector<Attribute>) md.getAttributes(false); String ssrc = null; Vector<Attribute> deletes = new Vector<Attribute>(); boolean rtcpMux = false; try { for (Attribute attrib : attributes) { if (attrib.getName().equals("rtcp-mux")) rtcpMux = true; } for (Attribute attrib : attributes) { Log.info("acceptWithAnswer attribute " + attrib.getName() + "=" + attrib.getValue()); if (attrib.getName().equals("crypto")) { attrib.setValue(attrib.getValue() + "|2^31"); } if (attrib.getName().equals("ssrc")) { String value = attrib.getValue(); ssrc = value.substring(0, value.indexOf(" ")); //deletes.add(attrib); } if (rtcpMux && attrib.getName().equals("rtcp")) { deletes.add(attrib); } if (attrib.getName().equals("candidate")) { attrib.setValue(attrib.getValue().replace(" generation 0","").replace("udp","UDP").replace("tcp","TCP-PASS")); } if (attrib.getName().equals("msid-semantic")) deletes.add(attrib); } for (Attribute attrib : deletes) { attributes.remove(attrib); } attributes.add(SdpFactory.getInstance().createAttribute("x-ssrc-range", ssrc + "-" + ssrc)); //attributes.add(SdpFactory.getInstance().createAttribute("rtcp-fb", "* x-message app send:dsh recv:dsh")); //attributes.add(SdpFactory.getInstance().createAttribute("rtcp-rsize", null)); //attributes.add(SdpFactory.getInstance().createAttribute("label", "main-audio")); //attributes.add(SdpFactory.getInstance().createAttribute("x-source", "main-audio")); } catch (Exception ec) { Log.error("acceptWithAnswer error", ec); } String sessionContext = "ofskype-" + System.currentTimeMillis(); url = url.indexOf("http") == 0 ? url : host + url; HttpMethod request = new PostMethod(url + "?sessionContext=" + sessionContext); PostMethod postRequest = (PostMethod)request; postRequest.setRequestEntity(new StringRequestEntity(sd.toString(), "application/sdp", "UTF-8")); request.setRequestHeader("Authorization", "Bearer " + oAuthToken); MethodExecutionResult result = executeMethod(request); int responseCode = result.getResponseCode(); String body = result.getBody(); Log.info("acceptWithAnswer " + responseCode + ":" + body + "\n" + sd); } catch(Exception ex) { Log.error("acceptWithAnswer error", ex); } } public void setPresence(String availability) { if (mePath != null) { if (myPresencePath == null) myPresencePath = mePath + "/presence"; myAvailability = availability; try { JSONObject reqBody = new JSONObject(); reqBody.put("availability", availability); postRequest(myPresencePath, reqBody); Log.info("setPresence " + myName + " " + availability); } catch(Exception ex) { Log.error("setPresence error", ex); } } } public void setNote(String note) { if (mePath != null) { if (myNotePath == null) myNotePath = mePath + "/note"; myNote = note; try { JSONObject reqBody = new JSONObject(); reqBody.put("message", note); postRequest(myNotePath, reqBody); Log.info("setNote " + myName + " " + note); } catch(Exception ex) { Log.error("setNote error", ex); } } } private void logon() throws Exception { if (host == null) return; Log.info("logon with URI " + host); boolean initialized = false; failCount.set(0); if(host.endsWith("/")) host = host.substring(0, host.length() - 1); Log.info("logon - creating application"); createApplication(); Log.info("logon - creating poller"); if (pollingThread != null) pollingThread.stop(); pollingThread = new Thread(new AsyncDataChannelPollingTask(), "SkypeClient Presence poller"); pollingThread.setDaemon(true); pollingThread.start(); pollingRunning = true; currentStatus.setErrorCode(ErrorCode.SUCCESS); if (contactsPath != null) { Log.info("logon - get all contacts "); getContacts(contactsPath); } if (groupsPath != null) { Log.info("logon - get all groups/contacts"); getAllGroupContacts(groupsPath); } } public JSONArray fetchContacts() { //Log.info("fetchContacts " + contactsPath); JSONArray contacts = new JSONArray(); if (contactsPath != null) { try { MethodExecutionResult contactsResult = getRequest(contactsPath); contacts = contactsResult.getJson().getJSONObject("_embedded").getJSONArray("contact"); } catch(Exception e) { Log.error("fetchContacts error", e); } } return contacts; } public JSONArray searchContacts(String query, int limit) { //Log.info("searchContacts " + searchPath); JSONArray contacts = new JSONArray(); if (contactsPath != null) { try { MethodExecutionResult contactsResult = getRequest(searchPath + "?query=" + query + "&limit=" + limit); contacts = contactsResult.getJson().getJSONObject("_embedded").getJSONArray("contact"); } catch(Exception e) { Log.error("searchContacts error", e); } } return contacts; } private void getContacts(final String contactsPath) { try { Runnable callable = new Runnable() { public void run() { try { MethodExecutionResult contactsResult = getRequest(contactsPath); //Log.info("getContacts " + contactsResult.getJson()); JSONArray contacts = contactsResult.getJson().getJSONObject("_embedded").getJSONArray("contact"); JSONArray contactList = new JSONArray(); for(int i = 0; i < contacts.length(); i++) { JSONObject contact = contacts.getJSONObject(i); String uri = contact.getString("uri"); String contactJid = uri.substring(4).toLowerCase(); String name = contact.getString("name"); Log.info("found contact " + name); contactList.put(i, uri); String photoURL = contact.getJSONObject("_links").getJSONObject("contactPhoto").getString("href"); pushAvatar(contactJid, photoURL, name); } subscribePresence(contactList); } catch(Exception e) { Log.error("getContacts error", e); } } } ; taskEngine.submit(callable); } catch(Exception e) { Log.error("getContacts error", e); } } private String pushAvatar(String contactJid, String photoURL, String contactName) { Log.info("pushAvatar " + contactJid + " " + photoURL); String base64String = null; if (photoURL != null) { try { base64String = fetchAvatar(photoURL); Log.info("pushAvatar photo\n" + base64String); if (sipUrl.equals(contactJid) == false) // contacts { //buddies.put(contactJid, new LyncBuddy(this, jid, new JID(contactJid), "", "", contactName, base64String)); String fromUser = jid.getNode(); Roster roster = XMPPServer.getInstance().getRosterManager().getRoster(fromUser); ArrayList<String> groups = new ArrayList<String>(); if (roster != null) { JID toUserJid = new JID(contactJid); RosterItem gwitem = null; if (roster.isRosterItem(toUserJid) == false) { Log.info( "pushAvatar create friendship " + fromUser + " " + toUserJid + " " + contactName); gwitem = roster.createRosterItem(toUserJid, true, true); } else { Log.info( "pushAvatar update friendship " + fromUser + " " + toUserJid + " " + contactName); gwitem = roster.getRosterItem(toUserJid); } if (gwitem != null) { gwitem.setSubStatus(RosterItem.SUB_BOTH); gwitem.setAskStatus(RosterItem.ASK_NONE); gwitem.setNickname(contactName); gwitem.setGroups((List<String>)groups); roster.updateRosterItem(gwitem); roster.broadcast(gwitem, true); } else Log.warn("pushAvatar cannot create friendship " + fromUser + " " + toUserJid + " " + contactName); } else Log.warn("pushAvatar cannot find roster for user " + fromUser); } else { // me //component.addMe(new LyncBuddy(this, jid, new JID(contactJid), "", "", contactName, base64String)); } } catch(Exception e) { Log.error("pushAvatar error", e); } } return base64String; } public String fetchAvatar(String photoURL) { Log.info("fetchAvatar " + photoURL); String base64String = null; try { byte[] avatar = getBytesRequest(photoURL, "image/png"); if (avatar != null) { base64String = org.jivesoftware.util.Base64.encodeBytes(avatar); } else { Log.warn("Error fetchAvatar " + photoURL); } } catch(Exception e) { Log.error("fetchAvatar error", e); } return base64String; } private void getAllGroupContacts(final String groupsPath) { try { Runnable callable = new Runnable() { public void run() { try { MethodExecutionResult groupsResult = getRequest(groupsPath); JSONObject _embedded = groupsResult.getJson().getJSONObject("_embedded"); if (_embedded.has("pinnedGroup")) { JSONObject pinnedGroup = _embedded.getJSONObject("pinnedGroup"); getGroupContacts(pinnedGroup.getString("name"), pinnedGroup.getJSONObject("_links").getJSONObject("groupContacts").getString("href")); } JSONObject defaultGroup = _embedded.getJSONObject("defaultGroup"); getGroupContacts(defaultGroup.getString("name"), defaultGroup.getJSONObject("_links").getJSONObject("groupContacts").getString("href")); JSONArray distributionGroup = _embedded.getJSONArray("distributionGroup"); for(int i = 0; i < distributionGroup.length(); i++) { if (distributionGroup.getJSONObject(i).getJSONObject("_links").has("groupContacts")) { getGroupContacts(distributionGroup.getJSONObject(i).getString("name"), distributionGroup.getJSONObject(i).getJSONObject("_links").getJSONObject("groupContacts").getString("href")); } } JSONArray group = _embedded.getJSONArray("group"); for(int i = 0; i < group.length(); i++) { if (group.getJSONObject(i).getJSONObject("_links").has("groupContacts")) { getGroupContacts(group.getJSONObject(i).getString("name"), group.getJSONObject(i).getJSONObject("_links").getJSONObject("groupContacts").getString("href")); } } } catch(Exception e) { Log.error("getAllGroupContacts error", e); } } } ; taskEngine.submit(callable); } catch(Exception e) { Log.error("getAllGroupContacts error", e); } } private void getGroupContacts(final String groupName, final String href) { try { MethodExecutionResult response = getRequest(href); JSONObject jsonObject = response.getJson(); Log.info("getGroupContacts " + groupName + "\n" + jsonObject); JSONArray contacts = jsonObject.getJSONObject("_embedded").getJSONArray("contact"); int externalContacts = 0; for(int i = 0; i < contacts.length(); i++) { JSONObject contact = contacts.getJSONObject(i); String srcNetwork = contact.getString("sourceNetwork"); if (srcNetwork.contains("SameEnterprise") == false) externalContacts++; } boolean sharedGroup = externalContacts == 0 && groupName.indexOf("family") == -1; for(int i = 0; i < contacts.length(); i++) { JSONObject contact = contacts.getJSONObject(i); String sip = contact.getString("uri").substring(4).toLowerCase(); String workPhoneNumber = contact.has("workPhoneNumber") ? workPhoneNumber = contact.getString("workPhoneNumber") : ""; String displayName = contact.getString("name"); String srcNetwork = contact.getString("sourceNetwork"); boolean isInternal = srcNetwork.contains("SameEnterprise"); Log.info("getGroupContacts, found contact " + groupName + " " + displayName + " " + sip + " " + workPhoneNumber); String fromUser = jid.getNode(); Roster roster = XMPPServer.getInstance().getRosterManager().getRoster(fromUser); if (roster != null) { JID toUserJid = new JID(sip); RosterItem gwitem = null; if (roster.isRosterItem(toUserJid) == false) { Log.info( "getGroupContacts create friendship " + fromUser + " " + toUserJid + " " + displayName); gwitem = roster.createRosterItem(toUserJid, true, true); } else { Log.info( "getGroupContacts update friendship " + fromUser + " " + toUserJid + " " + displayName); gwitem = roster.getRosterItem(toUserJid); } if (gwitem != null) { List<String> groups = gwitem.getGroups(); groups.add(groupName); gwitem.setNickname(displayName); gwitem.setGroups(groups); roster.updateRosterItem(gwitem); roster.broadcast(gwitem, true); } else Log.warn("getGroupContacts cannot find friendship " + fromUser + " " + toUserJid + " " + displayName); } else Log.warn("getGroupContacts cannot find roster for user " + fromUser); if ("Other Contacts".equals(groupName) == false && "Pinned Contacts".equals(groupName) == false) { try { Group group = null; try { group = GroupManager.getInstance().getGroup(groupName); } catch (GroupNotFoundException e) { ; group = GroupManager.getInstance().createGroup(groupName); group.getProperties().put("sharedRoster.showInRoster", "onlyGroup"); group.getProperties().put("sharedRoster.displayName", groupName); group.getProperties().put("sharedRoster.groupList", ""); } try { group.getMembers().remove(jid); } catch (Exception e) {} group.getMembers().add(jid); Map<String, Object> params = new HashMap<String, Object>(); params.put("member", jid.toString()); GroupEventDispatcher.dispatchEvent(group, GroupEventDispatcher.EventType.member_added, params); } catch(Exception e) { Log.error("getGroupContacts exception ", e); } } if (buddies.containsKey(sip)) { LyncBuddy buddy = buddies.get(sip); buddy.workPhoneNumber = workPhoneNumber; JID contactJid = new JID(JID.escapeNode(sip) + "@" + jid.getDomain()); if (srcNetwork.contains("SameEnterprise")) { contactJid = new JID((new JID(sip)).getNode() + "@" + jid.getDomain()); } //component.addRosterItem(buddy, jid, contactJid, displayName, groupName, /*sharedGroup*/ false); buddy.sendContactPhoto(); } } } catch(Exception e) { Log.error("getGroupContacts error", e); } } public long getLastReceivedTimeA() { return 0; } public long getLastReceivedTimeB() { return 0; } public JID getClientId() { return jid; } public void close() { //Log.info("SkypeClient close"); uninitialize(); for (LyncBuddy buddy : buddies.values()) { try { buddy.session.close(); } catch ( Exception e ) { } } buddies.clear(); } private HttpMethod createHttpMethod(String path, String method, Object body, String token) { Log.info("createHttpMethod " + path + " " + method + " " + body + " " + token); if (path == null) path = ""; HttpMethod request = null; String uri = path.indexOf("http") == 0 ? path : host + path; try { if("POST".equalsIgnoreCase(method)) { request = new PostMethod(uri); PostMethod postRequest = (PostMethod)request; if (body != null) { if (body instanceof JSONObject) postRequest.setRequestEntity(new StringRequestEntity(body.toString(), "application/json", "UTF-8")); if (body instanceof String) postRequest.setRequestEntity(new StringRequestEntity(body.toString(), "text/plain", "UTF-8")); } } else { if("DELETE".equalsIgnoreCase(method)) request = new DeleteMethod(uri); else request = new GetMethod(uri); } request.setRequestHeader("Authorization", (new StringBuilder("Bearer ")).append(token).toString()); } catch(UnsupportedEncodingException e) { String message = (new StringBuilder("Failed creating ")).append(method).append(" method to URL [").append(uri).append("] due to unsupported encoding").toString(); Log.error(message, e); } return request; } public void subscribePresence(final JSONArray contacts) throws SessionExpiredException { if (presenceSubscriptionsPath != null) { try { Runnable callable = new Runnable() { public void run() { try { JSONObject reqBody = new JSONObject(); reqBody.put("duration", getSubscriptionDuration()); reqBody.put("uris", contacts); postRequest(presenceSubscriptionsPath, reqBody); } catch(Exception e) { Log.error("Error while subscribing to contacts", e); } } } ; taskEngine.submit(callable); } catch(Exception e) { Log.error("Failed to subscribe to user presence", e); } } } private int getSubscriptionDuration() { return 30; } private PollingResult getEvents() { PollingResult result = new PollingResult(true); try { MethodExecutionResult executionResult = getRequest(eventsPath); JSONObject res = executionResult.getJson(); Log.warn("getEvents \n" + res); if(executionResult.getResponseCode() > 204 || res == null) { String message = String.format("Error while running polling request. Got responde code %d from server with message: '%s'", new Object[] { Integer.valueOf(executionResult.getResponseCode()), executionResult.getBody() }); Log.error(message); result.setSuccess(false); result.setErrorMessage(message); result.setWebFail(true); result.setErrorResponse(executionResult.getBody()); return result; } else { //Log.info("getEvents " + res); } if (res.has("_links")) { eventsPath = res.getJSONObject("_links").getJSONObject("next").getString("href"); synchronized(updateQueue) { JSONArray senders = res.getJSONArray("sender"); for(int i = 0; i < senders.length(); i++) { JSONObject sender = senders.getJSONObject(i); JSONArray events = sender.getJSONArray("events"); //Log.info("Incoming event from " + sender); for(int j = 0; j < events.length(); j++) { JSONObject event = events.getJSONObject(j); JSONObject link = event.getJSONObject("link"); Log.info("Incoming link " + link); Log.info("Incoming event " + event); if("phoneAudio".equals(link.getString("rel"))) { JSONObject phoneAudio = event.getJSONObject("_embedded").getJSONObject("phoneAudio"); JSONObject phoneAudioLinks = phoneAudio.getJSONObject("_links"); if (phoneAudioLinks.has("stopPhoneAudio")) { stopPhoneAudioHref = phoneAudioLinks.getJSONObject("stopPhoneAudio").getString("href"); } if (phoneAudioLinks.has("holdPhoneAudio")) { holdPhoneAudioHref = phoneAudioLinks.getJSONObject("holdPhoneAudio").getString("href"); } if (phoneAudioLinks.has("resumePhoneAudio")) { resumePhoneAudioHref = phoneAudioLinks.getJSONObject("resumePhoneAudio").getString("href"); } String selfPhoneAudioHref = phoneAudioLinks.getJSONObject("self").getString("href"); String phoneAudioState = phoneAudio.getString("state"); if ("Disconnected".equals(phoneAudioState)) { for (ActivePhoneAudio activePhoneAudio : callIds.values()) { if (activePhoneAudio.selfPhoneAudioHref.equals(selfPhoneAudioHref)) { //TraderLyncComponent.self.outgoingCallNotification(this.user, activePhoneAudio.callId, false, null, null); } } } } if("audioVideoInvitation".equals(link.getString("rel"))) { JSONObject audioVideoInvitation = event.getJSONObject("_embedded").getJSONObject("audioVideoInvitation"); JSONObject audioVideoInvitationLinks = audioVideoInvitation.getJSONObject("_links"); if (audioVideoInvitationLinks.has("mediaOffer")) { String multipartAlternative = audioVideoInvitationLinks.getJSONObject("mediaOffer").getString("href"); int pos = multipartAlternative.indexOf(","); if (pos > -1) { String header = multipartAlternative.substring(0, pos); String body = multipartAlternative.substring(pos + 1); String boundary = header.substring(header.indexOf("boundary=") + 9); String[] multiparts = body.split("--" + boundary); for (int z=0; z<multiparts.length; z++) { String sdp = URLDecoder.decode(multiparts[z], "UTF-8"); if (sdp.indexOf("ms-proxy-2007fallback") == -1 && sdp.indexOf("handling=optional") > -1) { sdp = sdp.substring(sdp.indexOf("v=0")); OfSkypePlugin.self.makeCall(sipUrl, sdp, audioVideoInvitation); break; } } } } } if("phoneAudioInvitation".equals(link.getString("rel"))) { JSONObject phoneAudioInvitation = event.getJSONObject("_embedded").getJSONObject("phoneAudioInvitation"); JSONObject phoneAudioInvitationLinks = phoneAudioInvitation.getJSONObject("_links"); if (phoneAudioInvitationLinks.has("phoneAudio")) { String href = phoneAudioInvitationLinks.getJSONObject("phoneAudio").getString("href"); String callId = phoneAudioInvitation.getString("operationId"); ActivePhoneAudio activePhoneAudio = new ActivePhoneAudio(); activePhoneAudio.stopPhoneAudioHref = stopPhoneAudioHref; activePhoneAudio.holdPhoneAudioHref = holdPhoneAudioHref; activePhoneAudio.resumePhoneAudioHref = resumePhoneAudioHref; activePhoneAudio.selfPhoneAudioHref = href; activePhoneAudio.callId = callId; callIds.put(callId, activePhoneAudio); Log.info("phoneAudioInvitation " + href + " " + callId); } } if("contactPresence".equals(link.getString("rel")) || "contactNote".equals(link.getString("rel"))) { JSONObject contact = event.getJSONObject("in"); String rel = contact.getString("rel"); String href = contact.getString("href"); if("contact".equals(rel)) { updateQueue.push(new UCWAUpdateEvent("contactPresence", (new StringBuilder(String.valueOf(href))).append("/presence").toString())); updateQueue.push(new UCWAUpdateEvent("contactNote", (new StringBuilder(String.valueOf(href))).append("/note").toString())); result.setAddedEvents(true); } else { String msg = String.format("Error while parsing contactPresence or contactNote events. The rel property contained an invalid value '%s'.", new Object[] { rel }); Log.error(msg); result.setSuccess(false); result.setErrorMessage(msg); } } if("onlineMeetingInvitation".equals(link.getString("rel"))) { JSONObject onlineMeetingInvitation = event.getJSONObject("_embedded").getJSONObject("onlineMeetingInvitation"); JSONObject onlineMeetingInvitationLinks = onlineMeetingInvitation.getJSONObject("_links"); String sip = onlineMeetingInvitation.getJSONObject("_embedded").getJSONObject("from").getString("uri").substring(4); String type = event.getString("type"); Log.info("Got onlineMeetingInvitation " + sip + " " + type); if ("started".equals(type)) { if (onlineMeetingInvitationLinks.has("accept")) { String acceptPath = onlineMeetingInvitationLinks.getJSONObject("accept").getString("href"); // auto-accept postRequest(acceptPath, null); } } } if("messagingInvitation".equals(link.getString("rel"))) { JSONObject messagingInvitation = event.getJSONObject("_embedded").getJSONObject("messagingInvitation"); JSONObject invitationLinks = messagingInvitation.getJSONObject("_links"); if (invitationLinks.has("accept") && invitationLinks.has("decline")) { String acceptPath = invitationLinks.getJSONObject("accept").getString("href"); String declinePath = invitationLinks.getJSONObject("decline").getString("href"); String message = dataUriDecode(invitationLinks.getJSONObject("message").getString("href")); String conversationPath = invitationLinks.getJSONObject("conversation").getString("href"); String sip = messagingInvitation.getJSONObject("_embedded").getJSONObject("from").getString("uri").substring(4); Log.info("messagingInvitation lync -> xmpp " + sip + " " + conversationPath); conversations.put(conversationPath, new ChatRoom(sip, conversationPath)); conversations.put(sip, new ChatRoom(sip, conversationPath)); String skypeFastpath = JiveGlobals.getProperty("skype.fastpath." + sipUrl, null); Log.info("Got message invitation checking for skype.fastpath." + sipUrl + "=" + skypeFastpath); if (skypeFastpath != null) { String response = processWorkgroupRequest(skypeFastpath, message, sip, conversationPath); if (response == null) { postRequest(acceptPath, null); } else { JSONObject reqBody = new JSONObject(); reqBody.put("reason", response); postRequest(declinePath, reqBody); } } else { try { if (buddies.containsKey(sip)) { LyncBuddy buddy = buddies.get(sip); buddy.sendMessagingInvite(message); } } catch (Exception e) { Log.error("messagingInvitation ", e); } // auto-accept Log.info("Got message invitation " + message + " " + sip); postRequest(acceptPath, null); String sendMessageUri = conversationPath + "/messaging/messages"; postRequest(sendMessageUri + "?operationContext=" + System.currentTimeMillis(), "Hello " + sip + ":-)"); } } else { if (messagingInvitation.has("_embedded") && messagingInvitation.getJSONObject("_embedded").has("acceptedByParticipant")) { JSONArray acceptedByParticipant = messagingInvitation.getJSONObject("_embedded").getJSONArray("acceptedByParticipant"); if (acceptedByParticipant != null) { for(int z = 0; z < acceptedByParticipant.length(); z++) { JSONObject participant = acceptedByParticipant.getJSONObject(z); String sip = participant.getString("uri").substring(4); String conversationPath = participant.getJSONObject("_links").getJSONObject("conversation").getString("href"); Log.info("messagingInvitation xmpp -> lync " + sip + " " + conversationPath); conversations.put(conversationPath, new ChatRoom(sip, conversationPath, messagingInvitation.getString("operationId"))); conversations.put(sip, new ChatRoom(sip, conversationPath, messagingInvitation.getString("operationId"))); } } } } } if("message".equals(link.getString("rel"))) { JSONObject message = event.getJSONObject("_embedded").getJSONObject("message").getJSONObject("_links"); String conversationHref = sender.getString("href"); String msgUri = null; if (message.has("htmlMessage")) { msgUri = dataUriDecode(message.getJSONObject("htmlMessage").getString("href")); } if (message.has("plainMessage")) { msgUri = dataUriDecode(message.getJSONObject("plainMessage").getString("href")); } if (msgUri != null) { Log.info("New message " + msgUri + " " + conversationHref); String sendMessageUri = conversationHref + "/messaging/messages"; postRequest(sendMessageUri + "?operationContext=" + System.currentTimeMillis(), "re: " + msgUri); if (conversations.containsKey(conversationHref)) { ChatRoom chatRoom = conversations.get(conversationHref); if (buddies.containsKey(chatRoom.sip)) { LyncBuddy buddy = buddies.get(chatRoom.sip); buddy.sendMessage(msgUri, chatRoom.operationId); } //component.incomingChat(this, jid, chatRoom.sip, msgUri, chatRoom.operationId); } else { //component.incomingChat(this, jid, null, msgUri, null); } } } if("messaging".equals(link.getString("rel"))) { JSONObject messagingLinks= event.getJSONObject("_embedded").getJSONObject("messaging").getJSONObject("_links"); String state = event.getJSONObject("_embedded").getJSONObject("messaging").getString("state"); if ("Connected".equals(state)) { String sendMessageUri = messagingLinks.getJSONObject("sendMessage").getString("href"); } if ("Disconnected".equals(state)) { if (messagingLinks.has("addMessaging")) { String addMessagingUri = messagingLinks.getJSONObject("addMessaging").getString("href"); Log.info("Adding messaging modality " + addMessagingUri); //postRequest(addMessagingUri, null); } } } if("conversation".equals(link.getString("rel"))) { if (event.has("_embedded")) { JSONObject conversation = event.getJSONObject("_embedded").getJSONObject("conversation").getJSONObject("_links"); String state = event.getJSONObject("_embedded").getJSONObject("conversation").getString("state"); if ("Disconnected".equals(state)) { String conversationHref = conversation.getJSONObject("self").getString("href"); if (conversations.containsKey(conversationHref)) { ChatRoom chatRoom = conversations.get(conversationHref); if (buddies.containsKey(chatRoom.sip)) { LyncBuddy buddy = buddies.get(chatRoom.sip); buddy.leaveChatRoom(chatRoom.operationId); } conversations.remove(chatRoom.sip); conversations.remove(conversationHref); } } if ("Conferenced".equals(state)) // online meeting, we have to add partcipant to get messages { if (conversation.has("addParticipant")) { String addParticipantUri = conversation.getJSONObject("addParticipant").getString("href"); Log.info("Adding participant " + addParticipantUri); JSONObject reqBody = new JSONObject(); reqBody.put("sessionContext", "add-participant-" + System.currentTimeMillis()); reqBody.put("to", "sip:" + sipUrl); postRequest(addParticipantUri, reqBody); } } if ("Connected".equals(state)) { String conversationHref = conversation.getJSONObject("self").getString("href"); if (OfSkypePlugin.self.callSessions.containsKey(conversationHref)) { CallSession callSession = OfSkypePlugin.self.callSessions.get(conversationHref); callSession.performAck(); } } } } } } } } } catch(Exception e) { Log.error("Exception was thrown while calling GetEvents", e); return new PollingResult(false, (new StringBuilder("Error while polling for presence, details in logs: ")).append(e.getMessage()).toString()); } return result; } public String requestAction(String callId, String action) { Log.info("requestAction " + callId + " " + action); try { if (callIds.containsKey(callId)) { ActivePhoneAudio phoneAudio = callIds.get(callId); String href = null; if (action.equals("stopPhoneAudio")) { callIds.remove(callId); href = phoneAudio.stopPhoneAudioHref; } else if (action.equals("holdPhoneAudio")) { href = phoneAudio.holdPhoneAudioHref; } else if (action.equals("resumePhoneAudio")) { href = phoneAudio.resumePhoneAudioHref; } if (href != null) { MethodExecutionResult executionResult = postRequest(href, null); return executionResult.getResponseCode() < 400 ? null : "Normalization Failed"; } else { return "Action not supported"; } } else { return "Call Id not found"; } } catch(Exception e) { Log.error("requestAction error", e); return e.toString(); } } public String makePhoneCall(String to, String phoneNumber, String callId, String subject) { Log.info("makePhoneCall " + to + " " + callId + " " + subject + " " + phoneNumber + " " + startPhoneAudioPath); if (phoneNumber.indexOf("tel:") == -1) phoneNumber = "tel:" + phoneNumber; if (to.indexOf("tel:") == 0) to = to.substring(4); try { boolean ucwaEnabled = JiveGlobals.getBooleanProperty("skype.ucwa.enabled", true); if (ucwaEnabled) { JSONObject reqBody = new JSONObject(); reqBody.put("phoneNumber", phoneNumber); reqBody.put("operationId", callId); reqBody.put("subject", subject); reqBody.put("to", to); MethodExecutionResult executionResult = postRequest(startPhoneAudioPath, reqBody); return executionResult.getResponseCode() < 400 ? null : "makePhoneCall Failed"; } } catch(Exception e) { Log.error("makePhoneCall error", e); return e.toString(); } return "makePhoneCall Failed"; } private String extractDDI(String telephoneUri) { String ddi = null; if (telephoneUri != null) { int pos = telephoneUri.indexOf(";ddi="); if (pos > -1) { ddi = telephoneUri.substring(pos + 5); pos = ddi.indexOf(";"); if (pos == -1) pos = ddi.length(); ddi = ddi.substring(0, pos); } } return ddi; } private String extractTel(String telephoneUri) { String tel = ""; if (telephoneUri != null) { int pos = telephoneUri.indexOf(";"); if (pos == -1) pos = telephoneUri.length(); tel = telephoneUri.substring(0, pos); if (tel.startsWith("tel:")) tel = tel.substring(4); } return tel; } public void sendInvite(String sip, String roomId, String subject) { Log.info("sendInvite " + sip); if (conversations.containsKey(sip) == false) { Log.info("sendInvite new conversation " + sip); try { JSONObject reqBody = new JSONObject(); reqBody.put("importance", "Normal"); reqBody.put("sessionContext", "TL-SC-" + System.currentTimeMillis()); reqBody.put("operationId", roomId + "|" + System.currentTimeMillis()); reqBody.put("subject", subject); reqBody.put("to", "sip:" + sip); postRequest(startMessagingPath, reqBody); } catch(Exception e) { Log.error("sendInvite error", e); } } else { Log.info("sendInvite re-use existing conversation " + sip); } } public void closeConversation(String sip) { Log.info("closeConversation " + sip); try { if (conversations.containsKey(sip)) { //Log.info("closeConversation found " + conversations.get(sip).conversationHref); String conversationHref = conversations.get(sip).conversationHref; String stopMessageUri = conversationHref + "/messaging/terminate"; postRequest(stopMessageUri, null); //request("DELETE", conversationHref, null); } } catch(Exception e) { Log.error("closeConversation error", e); } } public void sendMessage(String sip, String text) { Log.info("sendMessage " + sip + " " + text); try { if (conversations.containsKey(sip)) { String sendMessageUri = conversations.get(sip).conversationHref + "/messaging/messages"; postRequest(sendMessageUri + "?operationContext=" + System.currentTimeMillis(), text); } } catch(Exception e) { Log.error("sendMessage error", e); } } private JSONObject sanitizeJson(String response) throws JSONException { return new JSONObject(response.replaceAll("^[^{]", "")); } private InitCallResponse getInitialPath() throws IOException, JSONException { Log.info("getInitialPath " + host + " " + oAuthToken); URI uri = new URI(host, false); String originalPath = ""; if(!StringUtils.isNullOrEmpty(uri.getPath())) { originalPath = uri.getPath(); if(originalPath.endsWith("/")) originalPath = originalPath.substring(0, originalPath.length() - 1); } uri.setPath((new StringBuilder(String.valueOf(originalPath))).append("/Autodiscover/AutodiscoverService.svc/root/oauth/user").toString()); if(uri.getHost() == null) { Log.error("The server URL is not valid " + host); return null; } uri.setQuery((new StringBuilder("originalDomain=")).append(urlEncode(uri.getHost())).toString()); GetMethod request = new GetMethod(uri.toString()); if(oAuthToken != null) request.addRequestHeader("Authorization", (new StringBuilder("Bearer ")).append(oAuthToken).toString()); URL url = new URL(uri.toString()); HttpClient httpClient = httpClientManager.getClient(url); int responseCode = httpClient.executeMethod(request); if(responseCode == 401) { Header responseHeaders[] = request.getResponseHeaders("WWW-Authenticate"); Header aheader[]; int k = (aheader = responseHeaders).length; for(int j = 0; j < k; j++) { Header h = aheader[j]; String value = h.getValue(); Matcher matcher = oauthPathHeaderPattern.matcher(value); if(matcher.find()) return new InitCallResponse(matcher.group(1), responseCode); } String message = "Got the expected 401 from server, but required headers were not present."; Log.error(message); } else if(responseCode == 200) { String jsonStr = getStringFromResponse(request); //Log.info((new StringBuilder("Response 200 from auto-discover: ")).append(jsonStr).toString()); JSONObject res = sanitizeJson(jsonStr); if(res.has("_links")) { JSONObject applications = res.getJSONObject("_links").getJSONObject("applications"); return new InitCallResponse(applications.getString("href"), responseCode); } if(res.has("User")) { JSONArray links = res.getJSONObject("User").getJSONArray("Links"); for(int i = 0; i < links.length(); i++) { JSONObject link = links.getJSONObject(i); if("Ucwa".equals(link.getString("token"))) return new InitCallResponse(link.getString("href"), responseCode); } } else { String message = "Got invalid JSON from autodiscover method."; Log.error((new StringBuilder(String.valueOf(message))).append(" Response: ").append(jsonStr).toString()); } } else { Log.error("Got an unexpected response code from server while trying to acquire oAuth tokens " + responseCode); return null; } Log.error("Failed to get oAuth tokens. oAuth acquisition headers were not found in the unauthenticated request."); return null; } private String urlEncode(String user) throws UnsupportedEncodingException { return URLEncoder.encode(user, "UTF-8"); } private MethodExecutionResult postBatchRequest(String resourcePath, List updates) { String uri = resourcePath.indexOf("http") == 0 ? resourcePath : host + resourcePath; MethodExecutionResult response; String boundary = UUID.randomUUID().toString(); try { PostMethod postRequest = new PostMethod(uri); URL url = new URL(uri); String body = parseBatchPresenceMultipartBody(boundary, updates, url.getHost()); postRequest.setRequestEntity(new StringRequestEntity(body, (new StringBuilder("multipart/batching;boundary=\"")).append(boundary).append("\"").toString(), "UTF-8")); postRequest.setRequestHeader("Authorization", (new StringBuilder("Bearer ")).append(oAuthToken).toString()); postRequest.setRequestHeader("Accept", "multipart/batching"); response = executeMethodString(postRequest); if(response == null || response.getBody() == null) return null; return response; } catch(Exception e) { Log.error((new StringBuilder("Failed creating batch method to URL [")).append(uri).append("]").toString(), e); return null; } } private String dataUriDecode(String uri) { if (!uri.toLowerCase().startsWith("data:")) { return uri; } try { String data = uri.substring(uri.indexOf(',') + 1); return URLDecoder.decode(data, "UTF-8"); } catch (UnsupportedEncodingException e) { return null; } } private String parseBatchPresenceMultipartBody(String boundary, List updates, String host) { StringBuilder sb = new StringBuilder(); sb.append("--").append(boundary); for(Iterator iterator = updates.iterator(); iterator.hasNext(); sb.append("\r\n\r\n").append("--").append(boundary)) { UCWAUpdateEvent presenceResource = (UCWAUpdateEvent)iterator.next(); sb.append("\r\n"); sb.append("Content-Type: application/http;msgtype=request\r\n\r\n"); sb.append("GET ").append(presenceResource.getHref()).append(" HTTP/1.1\r\n"); sb.append("Host: ").append(host).append("\r\n"); sb.append("Accept: application/json\r\n"); } return sb.append("--\r\n").toString(); } private MethodExecutionResult postRequest(String resourcePath, Object body) throws Exception { return request("POST", resourcePath, body); } private MethodExecutionResult getRequest(String resourcePath) throws Exception { return request("GET", resourcePath, null); } private byte[] getBytesRequest(String resourcePath, String mime) throws Exception { String uri = resourcePath.indexOf("http") == 0 ? resourcePath : host + resourcePath; HttpMethod request = new GetMethod(uri); request.setRequestHeader("Authorization", (new StringBuilder("Bearer ")).append(oAuthToken).toString()); request.setRequestHeader("Accept", mime); URL url = new URL(request.getURI().toString()); HttpClient httpClient = httpClientManager.getClient(url); int responseCode = httpClient.executeMethod(request); //Log.info("getBytesRequest " + resourcePath + " " + mime + " " + responseCode); if(responseCode == 200 || responseCode == 201) return request.getResponseBody(); else return null; } private MethodExecutionResult request(String method, String resourcePath, Object body) throws Exception { HttpMethod httpMethod = createHttpMethod(resourcePath, method, body, oAuthToken); if(httpMethod == null) return null; else return executeMethod(httpMethod); } public MethodExecutionResult executeMethod(HttpMethod request) throws Exception { MethodExecutionResult response = executeMethodString(request); if(response == null || response.getBody() == null) return null; else return response; } private MethodExecutionResult executeMethodString(HttpMethod request) throws Exception { Log.info("executeMethodString " + request.getURI().toString() + " " + request); URL url = new URL(request.getURI().toString()); HttpClient httpClient = httpClientManager.getClient(url); MethodExecutionResult result = new MethodExecutionResult(); int responseCode = httpClient.executeMethod(request); result.setResponseCode(responseCode); result.setBody(getStringFromResponse(request)); if(responseCode == 200 || responseCode == 201) { if(result.getBody() != null && !result.getBody().isEmpty()) return result; } else { if(responseCode == 204) { result.setBody("{}"); return result; } if(responseCode == 401) { String message = "Server returned unauthorized response (401)."; Header diagnosticsHeader = request.getResponseHeader("X-Ms-diagnostics"); if(diagnosticsHeader != null) { String value = diagnosticsHeader.getValue(); Matcher matcher = reasonPattern.matcher(value); if(matcher.find()) message = (new StringBuilder(String.valueOf(message))).append(" ").append(matcher.group(1)).toString(); } //Log.info((new StringBuilder("Got 401 from server, regenerating oAuth tokens. Message: ")).append(message).toString()); getOnlineToken(); } else if(responseCode == 409 || responseCode == 404) { if(currentStatus.getErrorCode() == ErrorCode.SUCCESS) { String message = String.format("Got %d response from server.", new Object[] { Integer.valueOf(responseCode) }); Log.error(message); } } else { Log.error("executeMethodString " + request.getURI().toString() + " " + request); String message = (new StringBuilder("Got unexpected result from server. Response received with responseCode: ")).append(responseCode).append(" and response data '").append(result.getBody()).append("'.").toString(); Log.error(message); } } return result; } private void createApplication() throws Exception { //Log.info("createApplication"); JSONObject req = new JSONObject(); req.accumulate("UserAgent", "UCWA Plugin for Openfire"); String randomObjectId = UUID.randomUUID().toString().substring(0, 8); req.accumulate("EndpointId", (new StringBuilder("UCWA_Client_")).append(randomObjectId).toString()); req.accumulate("Culture", "en-US"); MethodExecutionResult response = postRequest(applicationPath, req); JSONObject jsonObject = response.getJson(); if(jsonObject == null) { String message = String.format("Error while initializing UCWA connection, invalid response to application creation request. Got code %d with message '%s'", new Object[] { Integer.valueOf(response.getResponseCode()), response.getBody() }); Log.error(message); currentStatus.setErrorCode(ErrorCode.CONNECTION_ERROR); currentStatus.setErrorMessage(message); return; } //Log.info((new StringBuilder("Got valid response for application creation request: ")).append(jsonObject.toString()).toString()); try { JSONObject embedded = jsonObject.getJSONObject("_embedded"); presenceSubscriptionsPath = embedded.getJSONObject("people").getJSONObject("_links").getJSONObject("presenceSubscriptions").getString("href"); mePath = embedded.getJSONObject("me").getJSONObject("_links").getJSONObject("self").getString("href"); makeMeAvailablePath = embedded.getJSONObject("me").getJSONObject("_links").getJSONObject("makeMeAvailable").getString("href"); //callForwardingSettingsPath = embedded.getJSONObject("me").getJSONObject("_links").getJSONObject("callForwardingSettings").getString("href"); searchPath = embedded.getJSONObject("people").getJSONObject("_links").getJSONObject("search").getString("href"); contactsPath = embedded.getJSONObject("people").getJSONObject("_links").getJSONObject("myContacts").getString("href"); groupsPath = embedded.getJSONObject("people").getJSONObject("_links").getJSONObject("myGroups").getString("href"); JSONObject links = jsonObject.getJSONObject("_links"); eventsPath = links.getJSONObject("events").getString("href"); batchPath = links.getJSONObject("batch").getString("href"); selfApplicationResource = links.getJSONObject("self").getString("href"); startMessagingPath = embedded.getJSONObject("communication").getJSONObject("_links").getJSONObject("startMessaging").getString("href"); //startPhoneAudioPath = embedded.getJSONObject("communication").getJSONObject("_links").getJSONObject("startPhoneAudio").getString("href"); } catch(JSONException _ex) { String message = String.format("Error while initializing UCWA connection, invalid response to application creation request (response code %d). Invalid JSON object: '%s'", new Object[] { Integer.valueOf(response.getResponseCode()), response.getBody() }); Log.error(message, _ex); currentStatus.setErrorCode(ErrorCode.CONNECTION_ERROR); currentStatus.setErrorMessage(message); } } private void invalidateSubscriptions() { invalidateSubscriptionsTime = new Date(); } private String getStringFromResponse(HttpMethod method) throws IOException { StringBuilder responseContent; BufferedReader reader; responseContent = new StringBuilder(); reader = null; try { reader = new BufferedReader(new InputStreamReader(method.getResponseBodyAsStream())); String line; while((line = reader.readLine()) != null) responseContent.append(line).append(System.getProperty("line.separator")); } catch(Exception e) { Log.warn("Error while extracting response body from response"); if(reader != null) try { reader.close(); } catch(IOException _ex) { } return null; } if(reader != null) try { reader.close(); } catch(IOException _ex) { } return responseContent.toString(); } private class InitCallResponse { private String path; private int responseCode; public int getResponseCode() { return responseCode; } public String getPath() { return path; } public InitCallResponse(String path, int responseCode) { this.path = path; this.responseCode = responseCode; } } public class MethodExecutionResult { private String body; private int responseCode; public String getBody() { return body; } public void setBody(String body) { this.body = body; } public int getResponseCode() { return responseCode; } public void setResponseCode(int responseCode) { this.responseCode = responseCode; } public boolean isEmpty() { return body == null || body.isEmpty(); } public JSONObject getJson() { if(isEmpty()) return null; try { return sanitizeJson(body); } catch(JSONException e) { SkypeClient.Log.error(String.format("Error while parging string '%s' to JSON object", new Object[] {body}), e); } return null; } public MethodExecutionResult() { body = ""; responseCode = 0; } } private class AsyncDataChannelPollingTask implements Runnable { public void run() { SkypeClient.Log.info("AsyncDataChannelPollingTask started"); for(; pollingRunning; SkypeClient.Log.info("AsyncDataChannelPollingTask finished")) { long now = System.currentTimeMillis(); int maxErrors = 16; if(maxErrors == -1 || failCount.get() < maxErrors) { PollingResult result = getEvents(); if(result.isSuccess()) { failCount.set(0); if(result.isAddedEvents()) taskEngine.submit(new AsyncWorkerChannelTask()); } else { failCount.incrementAndGet(); SkypeClient.Log.error(result.getErrorMessage()); } } else { String message = "Aborting IM async data channel polling due to multiple error count"; currentStatus.setErrorCode(ErrorCode.CONNECTION_ERROR); currentStatus.setErrorMessage(message); SkypeClient.Log.info(message); break; } } } } private class AsyncWorkerChannelTask implements Runnable { public void run() { SkypeClient.Log.info("AsyncWorkerChannelTask started"); try { List updates = org.ifsoft.lync.ucwa.Lists.newArrayList(); synchronized(updateQueue) { for(int i = 0; i < 64; i++) { if(updateQueue.isEmpty()) break; UCWAUpdateEvent update = (UCWAUpdateEvent)updateQueue.pop(); if(!updates.contains(update)) updates.add(update); } } if(!updates.isEmpty()) { batchUpdatePresence(updates); } } catch(Exception e) { SkypeClient.Log.error("Failed executing presence actions", e); } SkypeClient.Log.info("AsyncWorkerChannelTask finished"); } } private void batchUpdatePresence(List updates) { Log.info("batchUpdatePresence " + updates + " " + batchPath); MethodExecutionResult batchResponse = postBatchRequest(batchPath, updates); if(batchResponse != null) { List userPresenceList = org.ifsoft.lync.ucwa.Lists.newArrayList(); analyzeBatchResponse(batchResponse.getBody(), userPresenceList); if(!userPresenceList.isEmpty()) handle(new UserPresenceChangedEvent(userPresenceList)); } } public void handle(UserPresenceChangedEvent e) { List usersPresence = e.getUsersPresence(); //Log.info((new StringBuilder("Received user presence event with ")).append(usersPresence.size()).append(" entries").toString()); for(Iterator iterator = usersPresence.iterator(); iterator.hasNext();) { UserPresence userPresence = (UserPresence)iterator.next(); try { String sip = userPresence.getUserId(); //Log.info("UserPresenceChangedEvent " + sip + " " + userPresence.getShow() + " " + userPresence.getStatusMessage()); if (buddies.containsKey(sip)) { LyncBuddy buddy = buddies.get(sip); buddy.updatePresenceItem(userPresence.getShow(), userPresence.getStatusMessage()); } //component.incomingPresence(this, jid, sip, userPresence.getShow(), userPresence.getStatusMessage()); } catch(Exception _ex) { String message = (new StringBuilder("Failed handling user presence event entry: ")).append(userPresence).toString(); Log.error(message); } } } private String getUserId(JSONObject response) { try { if(response.has("_links")) return getSipFromResource(response.getJSONObject("_links").getJSONObject("self").getString("href")); } catch(Exception e) { String message = "There was an error while updating user presence."; Log.error(message, e); } return null; } private int getNumericStatus(String availability, String activity) { if("Online".equals(availability)) return 3000; if("Away".equals(availability)) return !"Off work".equals(activity) ? 15000 : 0x76506bbf; if("BeRightBack".equals(availability)) return 12000; if("Busy".equals(availability)) return 6000; if("DoNotDisturb".equals(availability)) return 9000; if("IdleBusy".equals(availability)) return 6000; if("IdleOnline".equals(availability)) return 4500; return !"Offline".equals(availability) ? 5 : 18000; } private String getSipFromResource(String resource) { Matcher matcher = sipFromResourcePattern.matcher(resource); if(matcher.find() && matcher.groupCount() > 0) { return matcher.group(1); } else { String message = String.format("Could not parse resource '%s'.", new Object[] { resource }); Log.error(message); return null; } } private void getUserPresence(JSONObject response, UserPresence userPresence) { try { if(response.has("availability")) { String availability = response.getString("availability"); String activity = null; if(response.has("activity")) activity = response.getString("activity"); userPresence.setShow(availability); userPresence.setStatus(getNumericStatus(availability, activity)); } if(response.has("message")) { String text = response.getString("message"); userPresence.setStatusMessage(text); } } catch(Exception e) { String message = "There was an error while updating user presence."; Log.error(message, e); } } private void analyzeBatchResponse(String batchResponse, List userPresenceList) { Scanner scanner = new Scanner(batchResponse); while (scanner.hasNextLine()) { String json; String line = scanner.nextLine(); Matcher matcher = jsonLinePattern.matcher(line); if(!matcher.find() || matcher.groupCount() != 1) continue; json = matcher.group(1); JSONObject response; final String userId; response = new JSONObject(json); userId = getUserId(response); if (userId != null) { try { UnmodifiableIterator iterator = Iterators.filter(userPresenceList.iterator(), new Predicate() { public boolean apply(UserPresence input) { return input != null && input.getUserId().equals(userId); } public boolean apply(Object obj) { return apply((UserPresence)obj); } }); boolean add = true; UserPresence userPresence; if(iterator.hasNext()) { userPresence = (UserPresence)iterator.next(); add = false; } else { userPresence = new UserPresence(userId); } getUserPresence(response, userPresence); if(add) userPresenceList.add(userPresence); } catch(JSONException e) { Log.error("Updating user presence for user failed", e); } } } } private String processWorkgroupRequest(final String workgroupNodeName, String question, String username, final String conversationPath) { Log.info("processWorkgroupRequest " + workgroupNodeName + " " + question + " " + username); String response = null; try { if (username != null) { if (question != null) { Log.info("processWorkgroupRequest body " + question); question = question.replace("\n", " ").replace("\r", "").replace("\t", "").replace("\"", "'"); String workgroupName = workgroupNodeName + "@workgroup." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); if (globalConnections.containsKey(username) == false) { Log.info("processWorkgroupRequest smack session " + workgroupName); ConnectionConfiguration config = new ConnectionConfiguration("localhost", 0); XMPPConnection connection = new XMPPConnection(config); connection.connect(); connection.loginAnonymousUser(username); globalConnections.put(username, connection); } final XMPPConnection globalConnection = globalConnections.get(username); Workgroup workgroup = getWorkgroup(workgroupName, globalConnection); workgroup.addInvitationListener(new WorkgroupInvitationListener() { public void invitationReceived(WorkgroupInvitation workgroupInvitation) { try { String room = workgroupInvitation.getGroupChatName().split("@")[0]; String videoUrl = "https://" + XMPPServer.getInstance().getServerInfo().getHostname() + ":" + JiveGlobals.getProperty("httpbind.port.secure", "7443") + "/ofmeet/?r=" + room; String audioUrl = videoUrl + "&novideo=true"; String response = "Hello,\n\nA member of " + workgroupNodeName + " is inviting you to a conference\n\nTo join, please click\n" + videoUrl + "\nFor audio only with no webcan, please click\n" + audioUrl; postRequest(conversationPath + "/messaging/messages?operationContext=" + System.currentTimeMillis(), response); } catch (Exception e) { Log.error("workgroup.addInvitationListener", e); } } }); if (isOnline(workgroupName, globalConnection)) { Map details = new HashMap(); details.put("username", username); details.put("email", username); details.put("question", question); if (workgroup != null) { try { workgroup.joinQueue(details, username); } catch (XMPPException e) { response = "Unable to join chat queue." + workgroupName; Log.error(response, e); } } } else { response = "processWorkgroupRequest workgroup is offline " + workgroupName; Log.warn(response); } } else { response = "processWorkgroupRequest question is empty" + question; Log.warn(response); } } else { response = "processWorkgroupRequest bad username " + username; Log.warn(response); } } catch (Exception e) { response = "processWorkgroupRequest " + e; Log.error(response, e); } return response; } private boolean isOnline(final String workgroupName, XMPPConnection globalConnection) { Presence presence = workgroupPresence.get(workgroupName); if (presence == null) { Workgroup workgroup = getWorkgroup(workgroupName, globalConnection); boolean isAvailable = workgroup.isAvailable(); presence = new Presence(isAvailable ? Presence.Type.available : Presence.Type.unavailable); workgroupPresence.put(workgroupName, presence); // Otherwise PacketFilter fromFilter = new FromContainsFilter(workgroupName); PacketFilter presenceFilter = new PacketTypeFilter(Presence.class); PacketFilter andFilter = new AndFilter(fromFilter, presenceFilter); globalConnection.addPacketListener(new PacketListener() { public void processPacket(Packet packet) { Presence presence = (Presence)packet; workgroupPresence.put(workgroupName, presence); } }, andFilter); return isAvailable; } return presence != null && presence.getType() == Presence.Type.available; } private Workgroup getWorkgroup(final String workgroupName, final XMPPConnection globalConnection) { Workgroup workgroup = (Workgroup)workgroups.get(workgroupName); if (workgroup == null) { workgroup = new Workgroup(workgroupName, globalConnection); workgroups.put(workgroupName, workgroup); } return workgroup; } private class PollingResult { private boolean isSuccess; private boolean isWebFail; private String errorMessage; private String errorResponse; private boolean addedEvents; private boolean isSuccess() { return isSuccess; } private void setSuccess(boolean success) { isSuccess = success; } private boolean isWebFail() { return isWebFail; } private void setWebFail(boolean webFail) { isWebFail = webFail; } private String getErrorMessage() { return errorMessage; } private void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } private String getErrorResponse() { return errorResponse; } private void setErrorResponse(String errorResponse) { this.errorResponse = errorResponse; } private boolean isAddedEvents() { return addedEvents; } public void setAddedEvents(boolean addedEvents) { this.addedEvents = addedEvents; } private PollingResult(boolean success, String errorMessage) { isWebFail = false; errorResponse = null; this.errorMessage = errorMessage; isSuccess = success; } public PollingResult(boolean success) { isWebFail = false; errorResponse = null; isSuccess = success; } } private class RegisterSubscribedUserTask implements Runnable { private UserPresence userPresence; public void run() { SkypeClient.Log.info("RegisterSubscribedUserTask - Started"); synchronized(subscribedUsers) { subscribedUsers.add(userPresence.getUserId()); } SkypeClient.Log.info("RegisterSubscribedUserTask - Done"); } public RegisterSubscribedUserTask(UserPresence userPresence) { this.userPresence = userPresence; } } private class UCWAUpdateEvent { String rel; String href; private String getHref() { return href; } private String getRel() { return rel; } public int hashCode() { return (new StringBuilder(String.valueOf(rel))).append(href).toString().hashCode(); } public boolean equals(Object obj) { return (obj instanceof UCWAUpdateEvent) && hashCode() == obj.hashCode(); } public UCWAUpdateEvent(String rel, String href) { this.rel = rel; this.href = href; } } private class ReportMyActivity extends TimerTask { private String activityPath; public ReportMyActivity(String activityPath) { this.activityPath = activityPath; } public void run() { //Log.info("ReportMyActivity started"); try { postRequest(activityPath, null); } catch(Exception e) { Log.error("ReportMyActivity error", e); } //Log.info("ReportMyActivity finished"); } } private class ActivePhoneAudio { public String stopPhoneAudioHref = null; public String holdPhoneAudioHref = null; public String resumePhoneAudioHref = null; public String selfPhoneAudioHref = null; public String callId = null; } private class ChatRoom { public String sip = null; public String operationId = null; public String conversationHref = null; public ChatRoom(String sip, String conversationHref, String operationId) { this.sip = sip; this.conversationHref = conversationHref; int pos = operationId.indexOf("|"); if (pos > -1) this.operationId = operationId.substring(0, pos); else this.operationId = operationId; } public ChatRoom(String sip, String conversationHref) { this.sip = sip; this.conversationHref = conversationHref; } } }