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