package com.robonobo.midas; import static com.robonobo.common.util.TextUtil.*; import static com.robonobo.common.util.TimeUtil.*; import java.io.IOException; import java.util.*; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.robonobo.core.api.model.Playlist; import com.robonobo.midas.dao.*; import com.robonobo.midas.model.*; @Service("notification") public class NotificationServiceImpl implements NotificationService { private static final int ONE_HOUR_IN_MS = 60 * 60 * 1000; private static final int ONE_MIN_IN_MS = 60 * 1000; private static final int TWO_MINS_IN_MS = 2 * 60 * 1000; private static final int FIVE_MINS_IN_MS = 5 * 60 * 1000; static final String WEEKLY = "weekly"; static final String DAILY = "daily"; static final String IMMEDIATE = "immediate"; static final String NONE = "none"; static final Pattern LIBRARY_ITEM_PAT = Pattern.compile("^library:(\\d+)$"); static final Pattern PLAYLIST_ITEM_PAT = Pattern.compile("^playlist:(\\d+)$"); static final Pattern LOVE_ITEM_PAT = Pattern.compile("^love:(\\S+)$"); @Autowired MessageService message; @Autowired NotificationDao notifDao; @Autowired UserDao userDao; @Autowired PlaylistDao playlistDao; @Autowired UserConfigDao userCfgDao; @Autowired CommentDao commentDao; Log log = LogFactory.getLog(getClass()); @Override public void newComment(MidasComment c) throws IOException { Set<Long> sentUids = new HashSet<Long>(); MidasUser commentUser = userDao.getById(c.getUserId()); // Send notification to owner of parent comment if (c.getParentId() > 0) { MidasComment par = commentDao.getComment(c.getParentId()); if (par.getUserId() != c.getUserId()) { MidasUserConfig muc = userCfgDao.getUserConfig(par.getUserId()); String creStr = (muc == null) ? null : muc.getItem("commentReplyEmails"); boolean cre = (creStr == null) ? true : Boolean.valueOf(creStr); if (cre) { MidasUser origUser = userDao.getById(par.getUserId()); Matcher pm = PLAYLIST_ITEM_PAT.matcher(c.getResourceId()); if (pm.matches()) { MidasPlaylist p = playlistDao.getPlaylistById(Long.parseLong(pm.group(1))); message.sendReplyNotificationForPlaylist(origUser, commentUser, p); sentUids.add(origUser.getUserId()); } else { Matcher lm = LIBRARY_ITEM_PAT.matcher(c.getResourceId()); if (lm.matches()) { message.sendReplyNotificationForLibrary(origUser, commentUser, Long.parseLong(lm.group(1))); sentUids.add(origUser.getUserId()); } } } } } // Send notification to owners of resource Matcher pm = PLAYLIST_ITEM_PAT.matcher(c.getResourceId()); if (pm.matches()) { MidasPlaylist p = playlistDao.getPlaylistById(Long.parseLong(pm.group(1))); for (Long ownerId : p.getOwnerIds()) { if (sentUids.contains(ownerId)) continue; MidasUserConfig muc = userCfgDao.getUserConfig(ownerId); String pceStr = (muc == null) ? null : muc.getItem("playlistCommentEmails"); boolean pce = (pceStr == null) ? true : Boolean.valueOf(pceStr); if (pce) { MidasUser owner = userDao.getById(ownerId); message.sendCommentNotificationForPlaylist(owner, commentUser, p); sentUids.add(ownerId); } } } else { Matcher lm = LIBRARY_ITEM_PAT.matcher(c.getResourceId()); if (!lm.matches()) { log.error("No match for resource id " + c.getResourceId()); return; } long ownerId = Long.parseLong(lm.group(1)); if (!sentUids.contains(ownerId)) { MidasUserConfig muc = userCfgDao.getUserConfig(ownerId); String pceStr = (muc == null) ? null : muc.getItem("playlistCommentEmails"); boolean pce = (pceStr == null) ? true : Boolean.valueOf(pceStr); if (pce) { MidasUser owner = userDao.getById(ownerId); message.sendCommentNotificationForLibrary(owner, commentUser); sentUids.add(ownerId); } } } } @Override public void playlistUpdated(MidasUser owner, MidasPlaylist p) { if (p.getVisibility().equals(Playlist.VIS_ME)) return; for (long friendId : owner.getFriendIds()) { String pref = getUpdateFreq(friendId); if (pref.equals(IMMEDIATE)) { MidasUser friend = userDao.getById(friendId); try { message.sendPlaylistNotification(owner, friend, p); } catch (IOException e) { log.error("Caught exception sending playlist notification", e); } } else if (!pref.equals(NONE)) notifDao.saveNotification(new MidasNotification(owner.getUserId(), friendId, "playlist:" + p.getPlaylistId())); } } @Override public void lovesAdded(MidasUser user, List<String> artists) throws IOException { for (long friendId : user.getFriendIds()) { String pref = getUpdateFreq(friendId); if (pref.equals(IMMEDIATE)) { MidasUser friend = userDao.getById(friendId); message.sendLovesNotification(user, friend, artists); } else if (!pref.equals(NONE)) { for (String artist : artists) { notifDao.saveNotification(new MidasNotification(user.getUserId(), friendId, "love:" + urlEncode(artist))); } } } } @Override public void addedToLibrary(MidasUser user, int numTrax) { // We don't actually send library notifications immediately as there might well be more coming in down the pipe // - instead we send them every hour for (long friendId : user.getFriendIds()) { if (!getUpdateFreq(friendId).equals(NONE)) notifDao.saveNotification(new MidasNotification(user.getUserId(), friendId, "library:" + numTrax)); } } /** Send our collected library updates every hour */ @Scheduled(fixedRate = ONE_HOUR_IN_MS) @Transactional public void sendPseudoImmediateNotifications() { Map<Long, List<MidasNotification>> nMap = getAllNotificationsByNotifUser(); int numSent = 0; for (Long notifUid : nMap.keySet()) { if (!getUpdateFreq(notifUid).equals(IMMEDIATE)) continue; Map<Long, List<MidasNotification>> notsBySource = mapNotsByUpdateUser(nMap.get(notifUid)); updateUser: for (Long updateUid : notsBySource.keySet()) { // Only send if they're all over an hour old, otherwise they're still adding to their library int numTrax = 0; for (MidasNotification n : notsBySource.get(updateUid)) { if (msElapsedSince(n.getDate()) < ONE_HOUR_IN_MS) continue updateUser; Matcher m = LIBRARY_ITEM_PAT.matcher(n.getItem()); if (!m.matches()) continue; numTrax += Integer.parseInt(m.group(1)); } MidasUser updateUser = userDao.getById(updateUid); if (updateUser != null) { MidasUser notifyUser = userDao.getById(notifUid); if (notifyUser != null) { try { message.sendLibraryNotification(updateUser, notifyUser, numTrax); } catch (IOException e) { log.error("Caught exception when sending library notification", e); continue; } numSent++; } } notifDao.deleteNotifications(notsBySource.get(updateUid)); } } if (numSent > 0) log.debug("Sent " + numSent + " 'immediate' notifications"); } /** Send daily notifications at 0900 GMT */ @Scheduled(cron = "0 0 9 * * *") @Transactional public void sendDailyNotifications() { Map<Long, List<MidasNotification>> nMap = getAllNotificationsByNotifUser(); Iterator<Entry<Long, List<MidasNotification>>> it = nMap.entrySet().iterator(); while (it.hasNext()) { Entry<Long, List<MidasNotification>> en = it.next(); if (!getUpdateFreq(en.getKey()).equals(DAILY)) it.remove(); } log.info("Sending daily notifications to " + nMap.size() + " users"); sendCombinedNotifications(nMap); } /** Send weekly notifications at 0915 GMT on Friday */ @Scheduled(cron = "0 15 9 * * 5") @Transactional public void sendWeeklyNotifications() { Map<Long, List<MidasNotification>> nMap = getAllNotificationsByNotifUser(); Iterator<Entry<Long, List<MidasNotification>>> it = nMap.entrySet().iterator(); while (it.hasNext()) { Entry<Long, List<MidasNotification>> en = it.next(); if (!getUpdateFreq(en.getKey()).equals(WEEKLY)) it.remove(); } log.info("Sending weekly notifications to " + nMap.size() + " users"); sendCombinedNotifications(nMap); } private void sendCombinedNotifications(Map<Long, List<MidasNotification>> nots) { for (Long notUid : nots.keySet()) { MidasUser notUser = userDao.getById(notUid); if (notUser == null) { log.info("Deleting notifications to deleted user id " + notUid); notifDao.deleteAllNotificationsTo(notUid); continue; } // For each user we're notifying, group the nots together by the updating user Map<Long, List<MidasNotification>> notsByUpdateUid = mapNotsByUpdateUser(nots.get(notUid)); Map<MidasUser, Integer> libTraxAdded = new HashMap<MidasUser, Integer>(); Map<Long, List<Playlist>> playlists = new HashMap<Long, List<Playlist>>(); Map<Long, List<String>> loveArtists = new HashMap<Long, List<String>>(); for (Long updateUid : notsByUpdateUid.keySet()) { MidasUser updateUser = userDao.getById(updateUid); int added = 0; Map<Long, Playlist> pm = new HashMap<Long, Playlist>(); Set<String> as = new HashSet<String>(); for (MidasNotification n : notsByUpdateUid.get(updateUid)) { Matcher m = LIBRARY_ITEM_PAT.matcher(n.getItem()); if (m.matches()) added += Integer.parseInt(m.group(1)); else { m = PLAYLIST_ITEM_PAT.matcher(n.getItem()); if (m.matches()) { Long plId = Long.parseLong(m.group(1)); if (pm.containsKey(plId)) continue; Playlist p = playlistDao.getPlaylistById(plId); if (p != null) { // Don't send notifications for radio playlists if (p.getTitle().equalsIgnoreCase("radio")) continue; pm.put(plId, p); } } else { m = LOVE_ITEM_PAT.matcher(n.getItem()); if (m.matches()) as.add(urlDecode(m.group(1))); } } } if (added > 0 || pm.size() > 0 || as.size() > 0) { libTraxAdded.put(updateUser, added); List<Playlist> pll = new ArrayList<Playlist>(pm.values()); Collections.sort(pll); List<String> al = new ArrayList<String>(as); playlists.put(updateUid, pll); Collections.sort(al); loveArtists.put(updateUid, al); } } if (libTraxAdded.size() > 0 || playlists.size() > 0 || loveArtists.size() > 0) { try { message.sendCombinedNotification(notUser, libTraxAdded, playlists, loveArtists); } catch (IOException e) { log.error("Caught exception sending combined notification to " + notUser.getEmail(), e); continue; } } notifDao.deleteAllNotificationsTo(notUid); } } private Map<Long, List<MidasNotification>> mapNotsByUpdateUser(List<MidasNotification> nList) { Map<Long, List<MidasNotification>> result = new HashMap<Long, List<MidasNotification>>(); for (MidasNotification n : nList) { long updateUid = n.getUpdateUserId(); if (!result.containsKey(updateUid)) result.put(updateUid, new ArrayList<MidasNotification>()); result.get(updateUid).add(n); } return result; } private Map<Long, List<MidasNotification>> getAllNotificationsByNotifUser() { List<MidasNotification> nList = notifDao.getAllNotifications(); Map<Long, List<MidasNotification>> result = new HashMap<Long, List<MidasNotification>>(); for (MidasNotification n : nList) { if (!result.containsKey(n.getNotifUserId())) result.put(n.getNotifUserId(), new ArrayList<MidasNotification>()); result.get(n.getNotifUserId()).add(n); } return result; } private String getUpdateFreq(long userId) { // By default, we update weekly - if they haven't set their preference yet, use that MidasUserConfig muc = userCfgDao.getUserConfig(userId); if (muc == null) return WEEKLY; String result = muc.getItem("playlistUpdateEmails"); if (result == null) return WEEKLY; return result; } }