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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.robonobo.common.concurrent.CatchingRunnable; import com.robonobo.common.exceptions.SeekInnerCalmException; import com.robonobo.common.util.TimeUtil; import com.robonobo.core.api.model.*; import com.robonobo.midas.dao.*; import com.robonobo.midas.model.*; import com.robonobo.remote.service.MidasService; import com.twmacinta.util.MD5; @Service("midas") public class LocalMidasService implements MidasService { @Autowired private AppConfig appConfig; @Autowired private FacebookService facebook; @Autowired private TwitterService twitter; @Autowired private MessageService message; @Autowired private EventService event; @Autowired private NotificationService notification; @Autowired private FriendRequestDao friendRequestDao; @Autowired private InviteDao inviteDao; @Autowired private LibraryDao libraryDao; @Autowired private PlaylistDao playlistDao; @Autowired private StreamDao streamDao; @Autowired private UserConfigDao userConfigDao; @Autowired private UserDao userDao; @Autowired private CommentDao commentDao; @Autowired ThreadPoolTaskScheduler scheduler; Log log = LogFactory.getLog(getClass()); private long lastPlaylistId = -1; private long lastCommentId = -1; @Transactional(readOnly = true) public List<MidasUser> getAllUsers() { return userDao.getAll(); } public MidasUser getUserByEmail(String email) { return populateDefault(userDao.getByEmail(email)); } public MidasUser getUserById(long userId) { return populateDefault(userDao.getById(userId)); } @Transactional(rollbackFor = Exception.class) public MidasUser createUser(MidasUser user) { log.info("Creating user " + user.getEmail()); user.setVerified(true); user.setUpdated(now()); preventUserXSS(user); // This will have its user id set MidasUser createdUser = userDao.create(user); try { message.sendWelcome(createdUser); message.sendNewUserNotification(createdUser); } catch (IOException e) { log.error("Error sending mails when creating user " + createdUser.getEmail(), e); } event.newUser(createdUser); return populateDefault(createdUser); } private MidasUser populateDefault(MidasUser u) { if (u == null) return u; if (isEmpty(u.getImgUrl())) u.setImgUrl(appConfig.getInitParam("defaultUserImgUrl")); return u; } public MidasUser getUserAsVisibleBy(MidasUser targetU, MidasUser requestor) { // If this the user asking for themselves, give them everything. If // they're a friend, they get public and friend-visible playlists, but no friends. // Otherwise, they just get friendly name and image url MidasUser result; if (targetU.equals(requestor)) { result = new MidasUser(targetU); } else if (targetU.getFriendIds().contains(requestor.getUserId())) { result = new MidasUser(targetU); result.setPassword(null); result.getFriendIds().clear(); result.setInvitesLeft(0); Iterator<Long> iter = result.getPlaylistIds().iterator(); while (iter.hasNext()) { Playlist p = playlistDao.getPlaylistById(iter.next()); if (p.getVisibility().equals(Playlist.VIS_ME)) iter.remove(); } } else { result = new MidasUser(); result.setUserId(targetU.getUserId()); result.setFriendlyName(targetU.getFriendlyName()); result.setImgUrl(targetU.getImgUrl()); } return populateDefault(result); } @Transactional(rollbackFor = Exception.class) public void saveUser(MidasUser user) { preventUserXSS(user); user.setUpdated(now()); userDao.save(user); } private void preventUserXSS(MidasUser user) { user.setFriendlyName(escapeHtml(user.getFriendlyName())); user.setDescription(escapeHtml(user.getDescription())); } @Transactional(rollbackFor = Exception.class) public void deleteUser(long userId) { MidasUser u = userDao.getById(userId); // Go through all their playlists - if they are the only owner, delete it, otherwise remove them from the owners // list for (Long plId : u.getPlaylistIds()) { MidasPlaylist p = playlistDao.getPlaylistById(plId); p.getOwnerIds().remove(userId); if (p.getOwnerIds().size() == 0) playlistDao.deletePlaylist(p); else playlistDao.savePlaylist(p); } // Go through all their friends, delete this user from their friendids for (long friendId : u.getFriendIds()) { MidasUser friend = userDao.getById(friendId); friend.getFriendIds().remove(userId); userDao.save(friend); } // Delete any pending friend requests to this user List<MidasFriendRequest> frs = friendRequestDao.retrieveByRequestee(userId); for (MidasFriendRequest fr : frs) { friendRequestDao.delete(fr); } // Delete their userconfig userConfigDao.deleteUserConfig(userId); // Finally, delete the user itself userDao.delete(u); } @Transactional(rollbackFor = Exception.class) public MidasPlaylist getPlaylistById(long playlistId) { return playlistDao.getPlaylistById(playlistId); } @Transactional(rollbackFor = Exception.class) public MidasPlaylist getPlaylistByUserIdAndTitle(long uid, String title) { return playlistDao.getPlaylistByUserIdAndTitle(uid, title); } @Override @Transactional(rollbackFor = Exception.class) public List<MidasPlaylist> getRecentPlaylists(long maxAgeMs) { return playlistDao.getRecentPlaylists(maxAgeMs); } @Transactional(rollbackFor = Exception.class) @Override public MidasPlaylist newPlaylist(MidasPlaylist playlist) { if (playlist.getPlaylistId() > 0) throw new SeekInnerCalmException("newPlaylist called with non-new playlist!"); long newPlaylistId; synchronized (this) { if (lastPlaylistId <= 0) lastPlaylistId = playlistDao.getHighestPlaylistId(); if (lastPlaylistId == Long.MAX_VALUE) throw new SeekInnerCalmException("playlist ids wrapped!"); // Unlikely else lastPlaylistId++; newPlaylistId = lastPlaylistId; } playlist.setPlaylistId(newPlaylistId); savePlaylist(playlist); return playlist; } @Override @Transactional(rollbackFor = Exception.class) public List<MidasComment> getCommentsForPlaylist(long plId, Date since) { String resourceId = "playlist:" + plId; if (since == null) return commentDao.getAllComments(resourceId); else return commentDao.getCommentsSince(resourceId, since); } @Override @Transactional(rollbackFor = Exception.class) public List<MidasComment> getCommentsForLibrary(long uid, Date since) { String resourceId = "library:" + uid; if (since == null) return commentDao.getAllComments(resourceId); else return commentDao.getCommentsSince(resourceId, since); } @Override public MidasComment newCommentForPlaylist(MidasComment comment, long playlistId) { return newComment(comment, "playlist:" + playlistId); } @Override public MidasComment newCommentForLibrary(MidasComment comment, long userId) { return newComment(comment, "library:" + userId); } @Transactional(rollbackFor = Exception.class) private MidasComment newComment(MidasComment comment, String resourceId) { if (comment.getCommentId() > 0) throw new SeekInnerCalmException("newComment called with non-new comment"); long newCommentId; synchronized (this) { if (lastCommentId <= 0) lastCommentId = commentDao.getHighestCommentId(); if (lastCommentId == Long.MAX_VALUE) throw new SeekInnerCalmException("comment ids wrapped"); else lastCommentId++; newCommentId = lastCommentId; } comment.setCommentId(newCommentId); comment.setResourceId(resourceId); saveComment(comment); return comment; } @Override @Transactional(rollbackFor = Exception.class) public void saveComment(MidasComment c) { if (c.getCommentId() <= 0) throw new SeekInnerCalmException("comment id is not set"); preventCommentXSS(c); commentDao.saveComment(c); } @Override @Transactional(rollbackFor = Exception.class) public MidasComment getComment(long commentId) { return commentDao.getComment(commentId); } @Override @Transactional(rollbackFor = Exception.class) public void deleteComment(MidasComment c) { commentDao.deleteComment(c); } @Transactional(rollbackFor = Exception.class) public void savePlaylist(MidasPlaylist p) { if (p.getPlaylistId() <= 0) throw new SeekInnerCalmException("playlist id is not set"); preventPlaylistXSS(p); playlistDao.savePlaylist(p); } private void preventPlaylistXSS(MidasPlaylist p) { p.setTitle(escapeHtml(p.getTitle())); p.setDescription(escapeHtml(p.getDescription())); } private void preventCommentXSS(MidasComment c) { c.setText(escapeHtml(c.getText())); } @Transactional(rollbackFor = Exception.class) public void deletePlaylist(MidasPlaylist playlist) { playlistDao.deletePlaylist(playlist); commentDao.deleteAllComments("playlist:" + playlist.getPlaylistId()); } @Transactional(rollbackFor = Exception.class) public MidasStream getStreamById(String streamId) { return streamDao.getStream(streamId); } @Transactional(rollbackFor = Exception.class) public void saveStream(MidasStream stream) { streamDao.putStream(stream); } @Transactional(rollbackFor = Exception.class) public void deleteStream(MidasStream stream) { streamDao.deleteStream(stream); } @Transactional(rollbackFor = Exception.class) public Long countUsers() { return userDao.getUserCount(); } @Transactional(rollbackFor = Exception.class) public MidasInvite createOrUpdateInvite(String email, MidasUser friend, MidasPlaylist pl) { MidasInvite result = inviteDao.retrieveByEmail(email); if (result == null) { // New invite result = new MidasInvite(); result.setEmail(email); result.setInviteCode(generateEmailCode(email)); } result.getFriendIds().add(friend.getUserId()); if (pl != null) result.getPlaylistIds().add(pl.getPlaylistId()); result.setUpdated(now()); inviteDao.save(result); return result; } @Transactional(rollbackFor = Exception.class) public MidasFriendRequest createOrUpdateFriendRequest(MidasUser requestor, MidasUser requestee, MidasPlaylist pl) { MidasFriendRequest result = friendRequestDao.retrieveByUsers(requestor.getUserId(), requestee.getUserId()); if (result == null) { // New friend request result = new MidasFriendRequest(); result.setRequestorId(requestor.getUserId()); result.setRequesteeId(requestee.getUserId()); result.setRequestCode(generateEmailCode(requestee.getEmail())); } if (pl != null) result.getPlaylistIds().add(pl.getPlaylistId()); result.setUpdated(now()); friendRequestDao.save(result); return result; } public MidasFriendRequest getFriendRequest(String requestCode) { return friendRequestDao.retrieveByRequestCode(requestCode); } @Transactional(rollbackFor = Exception.class) public String acceptFriendRequest(MidasFriendRequest req) { MidasUser requestor = userDao.getById(req.getRequestorId()); if (requestor == null) return "Requesting user " + req.getRequestorId() + " not found"; MidasUser requestee = userDao.getById(req.getRequesteeId()); if (requestee == null) return "Requested user " + req.getRequesteeId() + " not found"; for (Long plId : req.getPlaylistIds()) { MidasPlaylist p = playlistDao.getPlaylistById(plId); p.getOwnerIds().add(requestee.getUserId()); playlistDao.savePlaylist(p); requestee.getPlaylistIds().add(plId); } requestor.getFriendIds().add(requestee.getUserId()); userDao.save(requestor); requestee.getFriendIds().add(requestor.getUserId()); userDao.save(requestee); friendRequestDao.delete(req); try { message.sendFriendConfirmation(requestor, requestee); } catch (IOException e) { log.error("Error sending friend confirmation", e); } event.friendRequestAccepted(requestor, requestee); return null; } @Transactional(rollbackFor = Exception.class) public void ignoreFriendRequest(MidasFriendRequest request) { friendRequestDao.delete(request); } public List<MidasFriendRequest> getPendingFriendRequests(long userId) { return friendRequestDao.retrieveByRequestee(userId); } @Transactional(rollbackFor = Exception.class) public void inviteAccepted(long acceptedUserId, String inviteCode) { MidasInvite invite = inviteDao.retrieveByInviteCode(inviteCode); if (invite != null) { MidasUser acceptedUser = getUserById(acceptedUserId); // Tell them about their new buddy for (Long friendId : invite.getFriendIds()) { MidasUser friend = getUserById(friendId); if (friend != null) { try { message.sendFriendConfirmation(friend, acceptedUser); } catch (IOException e) { log.error("Error sending friend confirmation", e); } } } event.inviteAccepted(acceptedUser, invite); inviteDao.delete(invite); } } public MidasInvite getInvite(String inviteCode) { return inviteDao.retrieveByInviteCode(inviteCode); } @Override public MidasInvite getInviteByEmail(String email) { return inviteDao.retrieveByEmail(email); } @Override public Library getLibrary(MidasUser u, Date since) { Library lib = libraryDao.getLibrary(u.getUserId()); if (lib == null) return null; lib.setUserId(u.getUserId()); if (since != null) { Iterator<Entry<String, Date>> it = lib.getTracks().entrySet().iterator(); while (it.hasNext()) { Entry<String, Date> e = it.next(); if (since.after(e.getValue())) it.remove(); } } return lib; } @Transactional(rollbackFor = Exception.class) @Override public void putLibrary(Library lib) { libraryDao.saveLibrary(lib); } @Override public MidasUserConfig getUserConfig(MidasUser u) { MidasUserConfig result = userConfigDao.getUserConfig(u.getUserId()); if (result == null) { result = new MidasUserConfig(); result.setUserId(u.getUserId()); } return result; } @Override @Transactional(rollbackFor = Exception.class) public void putUserConfig(MidasUserConfig newCfg) { // Update friends based on facebook & twitter deets long uid = newCfg.getUserId(); final MidasUser mu = userDao.getById(uid); MidasUserConfig oldCfg = userConfigDao.getUserConfig(uid); // Check to see if they have added facebook/twitter details boolean newFb = (oldCfg == null || oldCfg.getItem("facebookId") == null) && newCfg.getItem("facebookId") != null; boolean newTwit = (oldCfg == null || oldCfg.getItem("twitterScreenName") == null) && newCfg.getItem("twitterScreenName") != null; if (newFb || newTwit) { // Post their loves - but wait 30 mins to give them a chance to disable it final Playlist lovesPl = playlistDao.getPlaylistByUserIdAndTitle(uid, "loves"); if (lovesPl != null && lovesPl.getStreamIds().size() > 0) { scheduler.schedule(new CatchingRunnable() { public void doRun() throws Exception { Playlist newLovesPl = playlistDao.getPlaylistByUserIdAndTitle(mu.getUserId(), "loves"); // We want to exclude any new loves that have been posted, so we pass the new playlist as the // old one and vice versa lovesChanged(mu, newLovesPl, lovesPl); } }, TimeUtil.timeInFuture(30 * 60 * 1000)); } } if (newFb) facebook.updateFriends(mu, newCfg); userConfigDao.saveUserConfig(newCfg); } @Override @Transactional(rollbackFor = Exception.class) public void lovesChanged(MidasUser u, Playlist oldP, Playlist newP) throws IOException { long uid = u.getUserId(); // Which artists have just been added? Set<String> curArtists = new HashSet<String>(); for (String sid : oldP.getStreamIds()) { Stream s = streamDao.getStream(sid); curArtists.add(s.getArtist()); } Set<String> newArtists = new HashSet<String>(); for (String sid : newP.getStreamIds()) { if (!oldP.getStreamIds().contains(sid)) { Stream s = streamDao.getStream(sid); String artist = s.getArtist(); if (!curArtists.contains(artist)) newArtists.add(artist); } } if (newArtists.size() > 0) { List<String> al = new ArrayList<String>(newArtists); Collections.sort(al); // Figure out our message to post - use the twitter limit int msgSizeLimit = 140; String okMsg = "I love " + numItems(newArtists, "artist") + ": "; String url = appConfig.getInitParam("shortUrlBase") + "sp/" + Long.toHexString(uid) + "/loves"; for (int i = 0; i < newArtists.size(); i++) { StringBuffer sb = new StringBuffer("I love "); for (int j = 0; j <= i; j++) { if (j != 0) sb.append(", "); sb.append(al.get(j)); } if (i < (newArtists.size() - 1)) { int numOtherArtists = newArtists.size() - (i + 1); sb.append(" and ").append(numItems(numOtherArtists, "other artist")); } sb.append(": "); String msg = sb.toString(); if ((msg.length() + url.length()) <= msgSizeLimit) okMsg = msg; else break; } // Post our loves to fb/twitter MidasUserConfig muc = userConfigDao.getUserConfig(u.getUserId()); if (muc != null) { if (muc.getItem("facebookId") != null) { String fbStr = muc.getItem("postLovesToFacebook"); if (fbStr == null || Boolean.valueOf(fbStr)) facebook.postSpecialPlaylistToFacebook(muc, uid, "loves", okMsg); } if (muc.getItem("twitterScreenName") != null) { String twitStr = muc.getItem("postLovesToTwitter"); if (twitStr == null || Boolean.valueOf(twitStr)) twitter.postSpecialPlaylistToTwitter(muc, uid, "loves", okMsg); } } event.specialPlaylistPosted(u, uid, "loves"); // Send notifications to friends notification.lovesAdded(u, al); } } @Override public void addFriends(long userId, List<Long> friendIds, List<String> friendEmails) { MidasUser fromUser = getUserById(userId); if (fromUser == null) { log.error("addFriends failed: No user with id " + userId); return; } try { for (Long friendId : friendIds) { MidasUser friend = getUserById(friendId); if (friend == null) { log.error("addFriends failed for user " + fromUser.getEmail() + " and non existent user id " + friendId); continue; } message.sendFriendRequest(fromUser, friend, null); event.friendRequestSent(fromUser, friend); } for (String email : friendEmails) { MidasInvite invite = message.sendInvite(fromUser, email, null); event.inviteSent(fromUser, email, invite); } } catch (IOException e) { log.error("addFriends failed", e); } } @Override public String requestAccountTopUp(long userId) { MidasUser user = userDao.getById(userId); if (user == null) return "Error: no such user"; try { message.sendTopUpRequest(user); } catch (IOException e) { log.error("Error requesting topup", e); return "Error processing request, please contact help@robonobo.com"; } return "TopUp request received - please check your account in a few hours."; } private String generateEmailCode(String email) { MD5 hash = new MD5(); hash.Update(email); hash.Update(getDateFormat().format(now())); return hash.asHex(); } }