/* * Copyright 2012 Nodeable Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.streamreduce.core.service; import com.streamreduce.ConnectionNotFoundException; import com.streamreduce.OutboundStorageException; import com.streamreduce.connections.ConnectionProvider; import com.streamreduce.connections.ConnectionProviderFactory; import com.streamreduce.core.dao.SobaMessageDAO; import com.streamreduce.core.event.EventId; import com.streamreduce.core.model.Account; import com.streamreduce.core.model.Connection; import com.streamreduce.core.model.Event; import com.streamreduce.core.model.InventoryItem; import com.streamreduce.core.model.SobaObject; import com.streamreduce.core.model.User; import com.streamreduce.core.model.messages.MessageComment; import com.streamreduce.core.model.messages.MessageType; import com.streamreduce.core.model.messages.SobaMessage; import com.streamreduce.core.model.messages.details.SobaMessageDetails; import com.streamreduce.core.service.exception.AccountNotFoundException; import com.streamreduce.core.service.exception.MessageNotFoundException; import com.streamreduce.core.service.exception.TargetNotFoundException; import com.streamreduce.core.transformer.MessageTransformerResult; import com.streamreduce.core.transformer.SobaMessageTransformerFactory; import com.streamreduce.util.HashtagUtil; import com.streamreduce.util.MessageUtils; import java.text.MessageFormat; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.annotation.Nullable; import javax.annotation.Resource; import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service("messageService") public class MessageServiceImpl extends AbstractService implements MessageService { @Autowired private SobaMessageDAO sobaMessageDAO; @Autowired private ConnectionProviderFactory connectionProviderFactory; @Autowired private OutboundStorageService outboundStorageService; @Autowired private UserService userService; @Autowired private ConnectionService connectionService; @Autowired private EventService eventService; @Autowired private EmailService emailService; @Resource(name = "messageProperties") public Properties messageProperties; @Override public List<SobaMessage> getAllMessages(User currentUser, Long after, Long before, int limit, boolean ascending, String search, List<String> hashTags, String sender, boolean excludeNodebellies) { return sobaMessageDAO.getMessagesFromInbox(currentUser, after, before, limit, ascending, search, HashtagUtil.normalizeTags(hashTags), sender, excludeNodebellies); } @Override public SobaMessage getMessage(Account account, ObjectId messageId) throws MessageNotFoundException { SobaMessage message = sobaMessageDAO.getFromInbox(account, messageId); if (message == null) { throw new MessageNotFoundException(messageId.toString()); } return message; } // for all inboxes, will create mult @SuppressWarnings("rawtypes") @Override public void sendNodebellyGlobalMessage(Event event, SobaObject sender, Connection connection, Long dateGenerated, MessageType type, Set<String> hashtags) { this.createMessage(event, sender, connection, dateGenerated, type, hashtags, null); } // for a certain account @SuppressWarnings({"rawtypes", "unchecked"}) @Override public SobaMessage sendNodeableAccountMessage(Event event, Account account, Set<String> hashtags) { User sender = userService.getSystemUser(); // this enables us to save in a target account rather than use the senders account sender.setAccount(account); return sendAccountMessage( event, sender, // sent from nodebelly null, // no connectionId right now new Date().getTime(), MessageType.SYSTEM, hashtags, null ); } @Override public SobaMessage sendConnectionMessage(Event event, Connection connection) { return sendAccountMessage( event, connection, connection, // happens to be the same as sender new Date().getTime(), MessageType.CONNECTION, connection.getHashtags(), null ); } @SuppressWarnings({"rawtypes", "unchecked"}) @Override public SobaMessage sendInventoryMessage(Event event, InventoryItem inventoryItem) { return sendAccountMessage(event, inventoryItem, inventoryItem.getConnection(), new Date().getTime(), MessageType.INVENTORY_ITEM, inventoryItem.getHashtags(), null ); } @Override public SobaMessage sendActivityMessage(Event event, Connection connection, Long dateGenerated, SobaMessageDetails details) { return sendAccountMessage( event, connection, connection, // happens to be the same as sender dateGenerated, MessageType.ACTIVITY, connection.getHashtags(), details ); } @Override public SobaMessage sendGatewayMessage(Event event, Connection connection, Long dateGenerated) { return sendAccountMessage( event, connection, connection, // happens to be the same as sender dateGenerated, MessageType.GATEWAY, connection.getHashtags(), null ); } @Override public SobaMessage sendGatewayMessage(Event event, InventoryItem inventoryItem, Long dateGenerated) { return sendAccountMessage( event, inventoryItem, inventoryItem.getConnection(), dateGenerated, MessageType.GATEWAY, inventoryItem.getHashtags(), null ); } @SuppressWarnings("rawtypes") @Override public SobaMessage sendAccountMessage(Event event, SobaObject sender, Connection connection, Long dateGenerated, MessageType type, Set<String> hashtags, SobaMessageDetails details) { return createMessage(event, sender, connection, dateGenerated, type, hashtags, details); } @Override public SobaMessage sendNodebellyInsightMessage(Event event, Long dateGenerated, Set<String> hashtags) { if (event == null || dateGenerated == null) { throw new IllegalArgumentException("event and dateGenerated must be non-null"); } hashtags = hashtags == null ? new HashSet<String>() : hashtags; SobaMessage sobaMessage = null; try { // if this is the sender, should we override the ownerId? how? User nodebelly = userService.getSuperUser(); // if this is null, it's a global metric so only persist the message to the Nodeable account if (event.getAccountId() != null) { Account account = userService.getAccount(event.getAccountId()); // send an email if these are your first insights // SOBA-1600 if (!account.getConfigValue(Account.ConfigKey.RECIEVED_INSIGHTS)) { userService.handleInitialInsightForAccount(account); } // trickery to send from Nodebelly to a certain account // DO NOT PERSIST THIS!!!!! // also note how this is set last so the account references above are still valid to the "real" account nodebelly.setAccount(account); } Map<String, Object> meta = event.getMetadata(); String objectType = (String) meta.get("targetType"); String providerTypeId = (String) meta.get("targetProviderId"); // filter out inventory count messages, since they are redundant with connection counts String metricName = (String) meta.get("name"); if (metricName.startsWith("INVENTORY_ITEM_COUNT.")) { return null; } // get the connection where applicable Connection connection = null; try { ObjectId connectionId = null; // ugh if (objectType.contains("Connection")) { connectionId = event.getTargetId(); } else if (objectType.contains("InventoryItem")) { connectionId = new ObjectId(meta.get("targetConnectionId").toString()); } connection = connectionService.getConnection(connectionId); } catch (ConnectionNotFoundException e) { logger.error("Invalid connection Id, can not send Nodebelly Message" + e.getMessage()); } // custom tags hashtags.add("#insight"); if (providerTypeId != null) { hashtags.add(HashtagUtil.normalizeTag(providerTypeId)); // ie, aws, github, jira (this should be what we currently aggregate on } hashtags.add(HashtagUtil.toNodebellyTag(event.getEventId())); // based suffix of eventId sobaMessage = this.createMessage(event, nodebelly, connection, dateGenerated, MessageType.NODEBELLY, hashtags, null); } catch (AccountNotFoundException e) { logger.error(e.getMessage()); } return sobaMessage; } @SuppressWarnings("rawtypes") @Override public SobaMessage sendUserMessage(Event event, User sender, String message) throws TargetNotFoundException { // Not sure if this is possible but since the EventService can return null for created events and we want to // avoid NPEs, let's be safe. if (event == null) { return null; } MessageUtils.ParsedMessage parsedMessage = MessageUtils.parseMessage(message); Set<String> hashtags = parsedMessage.getTags(); // this is now a conversation (even if with no one), so add that tag hashtags.add("#conversation"); SobaMessage sobaMessage = createMessage(event, sender, null, new Date().getTime(), MessageType.USER, hashtags, null); emailService.sendUserMessageAddedEmail(sender, sobaMessage); return sobaMessage; } @SuppressWarnings({"rawtypes", "unchecked"}) protected SobaMessage createMessage(Event event, SobaObject sender, Connection connection, Long dateGenerated, MessageType type, Set<String> hashtags, @Nullable SobaMessageDetails details) { // Anytime the event is null, we do not want to send the requested message if (event == null) { return null; } // convert to a more user friendly string // accept messageDetails (if any) so we can append info if needed MessageTransformerResult messageTransformerResult = SobaMessageTransformerFactory.transformMessage(event, type, details, messageProperties); // plain text String transformedMessage = messageTransformerResult.getTransformedMessage(); // embed the rich formatted message in the soba object SobaMessageDetails sobaMessageDetails = messageTransformerResult.getMessageDetails(); SobaMessage message = new SobaMessage.Builder() .sender(sender) .dateGenerated(dateGenerated) .type(type) .connection(connection) .transformedMessage(transformedMessage) .hashtags(hashtags) .details(sobaMessageDetails) .build(); // save to inbox(es) saveMessage(sender, message); // Fire the event that a message has been created Map<String, Object> eventMetadata = new HashMap<>(); // Message event metadata eventMetadata.put("messageEventId", event.getId()); eventMetadata.put("messageEventAccountId", event.getAccountId()); eventMetadata.put("messageEventTargetId", event.getTargetId()); eventMetadata.put("messageEventTargetType", event.getMetadata().get("targetType")); eventMetadata.put("messageEventUserId", event.getUserId()); eventMetadata.put("messageEventEventId", event.getEventId()); // General metadata eventMetadata.put("messageDateGenerated", dateGenerated); eventMetadata.put("messageType", type); eventMetadata.put("messageHashtags", hashtags); eventMetadata.put("messagePlainContent", transformedMessage); // Sender metadata eventMetadata.put("messageSenderId", sender.getId()); eventMetadata.put("messageSenderType", sender.getClass().getSimpleName()); eventMetadata.put("messageSenderUserId", sender.getUser() != null ? sender.getUser().getId() : null); eventMetadata.put("messageSenderAccountId", sender.getAccount() != null ? sender.getAccount().getId() : null); eventMetadata.put("messageSenderAlias", sender.getAlias()); eventMetadata.put("messageSenderHashtags", sender.getHashtags()); eventMetadata.put("messageSenderVisibility", sender.getVisibility()); eventMetadata.put("messageSenderVersion", sender.getVersion()); // Connection metadata eventMetadata.put("messageConnectionId", connection != null ? connection.getId() : null); eventMetadata.put("messageConnectionAlias", connection != null ? connection.getAlias() : null); eventMetadata.put("messageConnectionHashtags", connection != null ? connection.getHashtags() : null); eventMetadata.put("messageConnectionVersion", connection != null ? connection.getVersion() : null); // Connection provider metadata ConnectionProvider cProvider = connection != null ? connectionProviderFactory.connectionProviderFromId(connection.getProviderId()) : null; eventMetadata.put("messageProviderId", cProvider != null ? cProvider.getId() : null); eventMetadata.put("messageProviderDisplayName", cProvider != null ? cProvider.getDisplayName() : null); eventMetadata.put("messageProviderType", cProvider != null ? cProvider.getType() : null); eventService.createEvent(EventId.CREATE, message, eventMetadata); // the the message and creation event, attempt to persist to outbound. try { outboundStorageService.sendSobaMessage(message); } catch (OutboundStorageException e) { logger.error("Unable to save processed message to outbound connections", e); } return message; } @SuppressWarnings("rawtypes") protected void saveMessage(SobaObject sender, SobaMessage message) { SobaObject.Visibility visibility = message.getVisibility(); switch (visibility) { case PUBLIC: // give a copy to everyone sobaMessageDAO.saveToInboxes(userService.getAccounts(), message); // save in public archive repo sobaMessageDAO.save(message); break; default: // all other types are just in the one account inbox sobaMessageDAO.saveToInbox(sender.getAccount(), message); break; } } @Override public void updateMessage(Account account, SobaMessage sobaMessage) throws MessageNotFoundException { // verify it exists first SobaMessage message = sobaMessageDAO.getFromInbox(account, sobaMessage.getId()); if (message == null) { throw new MessageNotFoundException(sobaMessage.getId().toString()); } sobaMessageDAO.saveToInbox(account, sobaMessage); } @Override public void addCommentToMessage(Account account, ObjectId messageId, MessageComment comment, Set<String> hashtags) throws MessageNotFoundException { SobaMessage message = getMessage(account, messageId); message.addComment(comment); if (hashtags != null && !hashtags.isEmpty()) { message.addHashtags(hashtags); } updateMessage(account, message); emailService.sendCommentAddedEmail(account, message, comment); } @Override public void addHashtagToMessage(Account account, ObjectId messageId, String hashtag) throws MessageNotFoundException { SobaMessage message = getMessage(account, messageId); message.addHashtag(hashtag); updateMessage(account, message); } @Override public void removeHashtagFromMessage(Account account, ObjectId messageId, String hashtag) throws MessageNotFoundException { SobaMessage message = getMessage(account, messageId); message.removeHashtag(hashtag); updateMessage(account, message); } @Override public void copyArchivedMessagesToInbox(Account account) { // get all messages from the archive. List<SobaMessage> messages = sobaMessageDAO.getPublicArchivedMessages(); logger.info("Copying " + messages.size() + " archived messages to account inbox " + account.getFuid()); for (SobaMessage message : messages) { sobaMessageDAO.saveToInbox(account, message); } } @Override public void removeSampleMessages(Account account, ObjectId connectionId) { sobaMessageDAO.removeMessagesFromConnection(account, connectionId); } /** * Remove all messages and the collection for this account. * * @param account - the account to kill */ @Override public void removeAllMessages(Account account) { sobaMessageDAO.removeInbox(account); } @Override public void nullifyMessage(User user, SobaMessage sobaMessage) { try { String nullify = MessageFormat.format(messageProperties.getProperty("message.removed.by"), user.getAlias()); sobaMessage.setDetails(null); sobaMessage.setTransformedMessage(nullify); updateMessage(user.getAccount(), sobaMessage); } catch (MessageNotFoundException e) { logger.error(e.getMessage(), e); } } @Override public void nullifyMessageComment(User user, SobaMessage sobaMessage, MessageComment messageComment) { try { String nullify = MessageFormat.format(messageProperties.getProperty("message.comment.removed.by"), user.getAlias()); for (MessageComment comment : sobaMessage.getComments()) { if (comment.equals(messageComment)) { comment.setComment(nullify); comment.setCreated(new Date().getTime()); updateMessage(user.getAccount(), sobaMessage); break; } } } catch (MessageNotFoundException e) { logger.error(e.getMessage(), e); } } }