All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.connectors.socialcast.SocialcastController Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2017 VMware, Inc. All Rights Reserved.
 * SPDX-License-Identifier: BSD-2-Clause
 */

package com.vmware.connectors.socialcast;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.ReadContext;
import com.vmware.connectors.common.model.Message;
import com.vmware.connectors.common.model.MessageThread;
import com.vmware.connectors.common.model.UserRecord;
import com.vmware.connectors.common.model.SocialcastRequestContext;
import com.vmware.connectors.common.utils.Async;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.AsyncRestOperations;
import org.springframework.web.client.HttpServerErrorException;
import rx.Observable;
import rx.Single;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

/**
 * This class encapsulates interactions with the Socialcast API.
 * 

* 1. Perform a search to see which participants in the email thread are already Socialcast users: *

 *
 * // Join multiple addresses with " or "?
 * GET /api/users/search.json?q=<email address[ or email address]>
 * 
*

* 2. Create a new Group: *

 * POST /api/groups.json
 * {
 * "group": {
 * "name": <subject line of first email in the thread>,
 * "permission_mode": "external_contributor"
 * }
 * }
 * 
*

* 3. Add all senders/recipients to the Group: *

 * POST /api/groups/<group_id>/memberships/add_members.json
 * {
 * "group_memberships": [
 * {
 * // For existing Socialcast users (i.e. ones we found an ID for), use their user ID:
 * "user_id": <id of user>,
 * "role": <"admin" for the sender of the first message in the thread, "member" otherwise>
 * },
 * {
 * // For users without an ID, use their email address, and trigger an invitation to be sent by Socialcast
 * "email": <id of user>,
 * "role": "member",
 * "invite": "true"
 * },
 * <one block for each sender/recipient>
 * ]
 * }
 * 
*

* 4. Post each email to the Group as a new message: *

 * POST /api/messages.json
 * {
 * "message": {
 * "user": <user ID of sender of this email>,
 * "body": <message body, not including quoted replies>,
 * "attachment": <attachment, if any, as inline base64>
 * }
 * }
 * 
*/ @RestController public class SocialcastController { // The name of the incoming request header carrying our Socialcast authorization token private static final String SOCIALCAST_AUTH_HEADER = "x-socialcast-authorization"; // The name of the incoming request header carrying the base URL of the user's Socialcast server private static final String SOCIALCAST_BASE_URL_HEADER = "x-socialcast-base-url"; // The URL path on which this app listens for incoming requests private static final String CREATE_CONVERSATION_PATH = "/conversations"; // A sink for log messages, because System.err.println() is *so* 1990... private final static Logger logger = LoggerFactory.getLogger(SocialcastController.class); // Our engine for making asynchronous outgoing HTTP requests private final AsyncRestOperations rest; private final SocialcastMessageFormatter formatter; @Autowired public SocialcastController(AsyncRestOperations rest, SocialcastMessageFormatter formatter) { this.rest = rest; this.formatter = formatter; } // This is the entry point for requests to post an email thread as a new Socialcast group. // The expected body is a JSON document containing the relevant data from all emails in the thread; // see "src/test/resources/requests/normalRequest.json" for an example of the expected format. // TODO: convert to the schema.org EmailMessage schema @PostMapping(path = CREATE_CONVERSATION_PATH, consumes = APPLICATION_JSON_VALUE) public Single> postThreadAsConversation( @RequestHeader(name = SOCIALCAST_AUTH_HEADER) String scAuth, @RequestHeader(name = SOCIALCAST_BASE_URL_HEADER) String baseUrl, @RequestBody String json) throws IOException { HttpHeaders headers = new HttpHeaders(); headers.set(AUTHORIZATION, scAuth); headers.set(CONTENT_TYPE, APPLICATION_JSON_VALUE); SocialcastRequestContext ctx = new SocialcastRequestContext(baseUrl, headers); // Parse the request body, which should be JSON representing a MessageThread ctx.setMessageThread(MessageThread.parse(json)); // Get emails of all the participants (senders and receivers) in the thread, // and look for their Socialcast IDs to see if they're already Socialcast Users Single userRecordsStep = getExistingUserIds(ctx); // Create a new Group for the email thread Single groupIdStep = createGroup(ctx); // Add all participants to the Group, inviting those who are not yet Socialcast Users, // and then post each email in the thread, in order, as a Message in the Group return Single.zip(userRecordsStep, groupIdStep, (httpStatus, httpStatus2) -> null) .flatMap(stat -> addUsersToGroup(ctx)) .flatMap(stat -> postMessages(ctx)) .map(resp -> respondWithSummary(resp, ctx)); } ////////////////////// // Step 1: get IDs of existing users // Socialcast API doc: https://socialcast.github.io/socialcast/apidoc/users/search.html ////////////////////// // Check if an email maps to a registered user private Single getExistingUserIds(SocialcastRequestContext ctx) { MessageThread mt = ctx.getMessageThread(); // It's not documented in the Socialcast API docs, but one query can search for multiple // email addresses if they are concatenated with " or " String queryString = mt.allUsers().stream() .map(UserRecord::getEmailAddress) .collect(Collectors.joining(" or ")); queryString = ctx.getScBaseUrl() + "/api/users/search.json?q=" + queryString; ListenableFuture> future = rest.exchange(queryString, HttpMethod.GET, new HttpEntity(ctx.getHeaders()), String.class); return Async.toSingle(future) .map(result -> parseGetUserResponse(result, ctx)); } // Parse the response from the user-search query and update UserRecords for those users // who are found to have Socialicast user ID's private HttpStatus parseGetUserResponse(ResponseEntity result, SocialcastRequestContext ctx) { MessageThread mt = ctx.getMessageThread(); Map userRecordMap = mt.allUsersByEmail(); for (Object queryResult : JsonPath.parse(result.getBody()).read("$.users", List.class)) { ReadContext userReadContext = JsonPath.parse(queryResult); String addr = userReadContext.read("$.contact_info.email"); String id = userReadContext.read("$.id", String.class); if (StringUtils.isNotBlank(id)) { UserRecord rec = userRecordMap.get(addr); if (rec != null) { rec.setScastId(id); } } } Set users = mt.allUsers(); for (UserRecord user : users) { String id = user.getScastId(); if (id == null) { logger.debug("Found no Socialcast ID for email <<{}>>", user.getEmailAddress()); ctx.addUserNotFound(user.getEmailAddress()); } else { logger.debug("Found Socialcast ID <<{}>> for email <<{}>>", id, user.getEmailAddress()); ctx.addUserFound(user.getEmailAddress(), id); } } ctx.addResponseCodeForStep("User query", result.getStatusCode().toString()); return result.getStatusCode(); } ////////////////////// // Step 2: create group // Socialcast API doc: https://socialcast.github.io/socialcast/apidoc/groups/create.html ////////////////////// private Single createGroup(SocialcastRequestContext ctx) { Map groupMap = new HashMap<>(); groupMap.put("name", formatter.makeGroupName(ctx.getMessageThread())); groupMap.put("description", formatter.makeGroupDescription(ctx.getMessageThread())); Map> bodyMap = Collections.singletonMap("group", groupMap); ListenableFuture> future = rest.exchange(ctx.getScBaseUrl() + "/api/groups.json", HttpMethod.POST, new HttpEntity<>(bodyMap, ctx.getHeaders()), String.class); return Async.toSingle(future) .map(entity -> parseGroupCreationResult(entity, ctx)); } // Get the ID and URI of the just-created Group and add them to the request context private HttpStatus parseGroupCreationResult(ResponseEntity entity, SocialcastRequestContext ctx) { ReadContext jsonContext = JsonPath.parse(entity.getBody()); String groupId = jsonContext.read("$.group.id").toString(); String groupUri = jsonContext.read("$.group.url").toString(); ctx.addResponseCodeForStep("Group creation", entity.getStatusCode().toString()); if (StringUtils.isBlank(groupId) || StringUtils.isEmpty(groupUri)) { throw new HttpServerErrorException(HttpStatus.UNPROCESSABLE_ENTITY); } else { ctx.setGroupUri(groupUri); ctx.setGroupId(groupId); } return entity.getStatusCode(); } ////////////////////// // Step 3: add users to group // Socialcast API doc: https://socialcast.github.io/socialcast/apidoc/group_memberships/add_members.html ////////////////////// private Single addUsersToGroup(SocialcastRequestContext ctx) { List> memberships = new ArrayList<>(); for (UserRecord user : ctx.getMessageThread().allUsers()) { Map userMap = new HashMap<>(); if (StringUtils.isNotBlank(user.getScastId())) { // If we have an ID for a user, they're already on Socialcast, so we just have to add them to the group String role = user.equals(ctx.getMessageThread().getFirstSender()) ? "admin" : "member"; userMap.put("user_id", user.getScastId()); userMap.put("role", role); } else { // If they don't have an ID, we tell Socialcast to invite them userMap.put("email", user.getEmailAddress()); userMap.put("invite", "true"); userMap.put("role", "member"); } memberships.add(userMap); } Map bodyMap = Collections.singletonMap("group_memberships", memberships); ListenableFuture> future = rest.exchange(ctx.getScBaseUrl() + "/api/groups/" + ctx.getGroupId() + "/memberships/add_members.json", HttpMethod.POST, new HttpEntity<>(bodyMap, ctx.getHeaders()), String.class); return Async.toSingle(future) .map(entity -> parseUserAdditionResponse(entity, ctx)); } // Write status report to the request context private HttpStatus parseUserAdditionResponse(ResponseEntity entity, SocialcastRequestContext ctx) { HttpStatus status = entity.getStatusCode(); ctx.addResponseCodeForStep("Adding users", status.toString()); return status; } ////////////////////// // Step 4: post messages // Socialcast API doc: https://socialcast.github.io/socialcast/apidoc/messages/create.html ////////////////////// private Single> postMessages(SocialcastRequestContext ctx) { List> postRequests = ctx.getMessageThread().getMessages().stream() .map(msg -> postMessage(msg, ctx)) .collect(Collectors.toList()); Observable concatenatedRequests = Observable.empty(); for (Observable req : postRequests) { concatenatedRequests = concatenatedRequests.concatWith(req); } return concatenatedRequests .all(httpStatus -> httpStatus == HttpStatus.CREATED) .map(success -> success ? new ResponseEntity(HttpStatus.CREATED) : new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)) .toSingle(); } // Post a single message private Observable postMessage(Message message, SocialcastRequestContext ctx) { Map msgMap = new HashMap<>(); msgMap.put("user", message.getSender().getScastId()); msgMap.put("group_id", ctx.getGroupId()); msgMap.put("body", formatter.formatMessageForDisplay(message)); // TODO: add attachment here Map bodyMap = Collections.singletonMap("message", msgMap); return Single.defer(() -> Async.toSingle( rest.exchange(ctx.getScBaseUrl() + "/api/messages.json", HttpMethod.POST, new HttpEntity<>(bodyMap, ctx.getHeaders()), String.class)) .map(entity -> reportStatus(entity, ctx, message))) .toObservable(); } // Write status and message permalink URL to the request context private HttpStatus reportStatus(ResponseEntity entity, SocialcastRequestContext ctx, Message message) { HttpStatus status = entity.getStatusCode(); ctx.addResponseCodeForStep("Posting message " + message.getId(), status.toString()); String msgUri = JsonPath.parse(entity.getBody()).read("$.message.permalink_url"); ctx.addMessagePosted(message.getId(), msgUri); return status; } private ResponseEntity respondWithSummary(ResponseEntity messagesResponse, SocialcastRequestContext ctx) { try { return new ResponseEntity<>(ctx.getResultJson(), messagesResponse.getHeaders(), messagesResponse.getStatusCode()); } catch (JsonProcessingException e) { logger.error("Exception serializing JSON response data", e); return new ResponseEntity<>("Exception serializing JSON response data", HttpStatus.INTERNAL_SERVER_ERROR); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy