/* * Symphony - A modern community (forum/SNS/blog) platform written in Java. * Copyright (C) 2012-2017, b3log.org & hacpai.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.b3log.symphony.service; import org.apache.commons.lang.StringUtils; import org.b3log.latke.Keys; import org.b3log.latke.event.Event; import org.b3log.latke.event.EventException; import org.b3log.latke.event.EventManager; import org.b3log.latke.ioc.inject.Inject; import org.b3log.latke.logging.Level; import org.b3log.latke.logging.Logger; import org.b3log.latke.model.User; import org.b3log.latke.repository.RepositoryException; import org.b3log.latke.repository.Transaction; import org.b3log.latke.repository.annotation.Transactional; import org.b3log.latke.service.LangPropsService; import org.b3log.latke.service.ServiceException; import org.b3log.latke.service.annotation.Service; import org.b3log.latke.util.Ids; import org.b3log.symphony.event.EventTypes; import org.b3log.symphony.model.*; import org.b3log.symphony.repository.*; import org.b3log.symphony.util.Emotions; import org.b3log.symphony.util.Symphonys; import org.json.JSONObject; import java.util.List; import java.util.Locale; /** * Comment management service. * * @author <a href="http://88250.b3log.org">Liang Ding</a> * @version 2.13.11.20, May 8, 2017 * @since 0.2.0 */ @Service public class CommentMgmtService { /** * Logger. */ private static final Logger LOGGER = Logger.getLogger(CommentMgmtService.class); /** * Revision repository. */ @Inject private RevisionRepository revisionRepository; /** * Comment repository. */ @Inject private CommentRepository commentRepository; /** * Article repository. */ @Inject private ArticleRepository articleRepository; /** * Option repository. */ @Inject private OptionRepository optionRepository; /** * Tag repository. */ @Inject private TagRepository tagRepository; /** * Tag-Article repository. */ @Inject private TagArticleRepository tagArticleRepository; /** * User repository. */ @Inject private UserRepository userRepository; /** * Notification repository. */ @Inject private NotificationRepository notificationRepository; /** * Event manager. */ @Inject private EventManager eventManager; /** * Language service. */ @Inject private LangPropsService langPropsService; /** * Pointtransfer management service. */ @Inject private PointtransferMgmtService pointtransferMgmtService; /** * Reward management service. */ @Inject private RewardMgmtService rewardMgmtService; /** * Reward query service. */ @Inject private RewardQueryService rewardQueryService; /** * Notification management service. */ @Inject private NotificationMgmtService notificationMgmtService; /** * Liveness management service. */ @Inject private LivenessMgmtService livenessMgmtService; /** * Removes a comment specified with the given comemnt id. A comemnt is removable if: * <ul> * <li>No replies</li> * <li>No ups, downs</li> * <li>No thanks</li> * </ul> * Sees https://github.com/b3log/symphony/issues/451 for more details. * * @param commentId the given commentId id * @throws ServiceException service exception */ public void removeComment(final String commentId) throws ServiceException { JSONObject comment = null; try { comment = commentRepository.get(commentId); } catch (final Exception e) { LOGGER.log(Level.ERROR, "Gets a comment [id=" + commentId + "] failed", e); } if (null == comment) { return; } final int replyCnt = comment.optInt(Comment.COMMENT_REPLY_CNT); if (replyCnt > 0) { throw new ServiceException(langPropsService.get("removeCommentFoundReplyLabel")); } final int ups = comment.optInt(Comment.COMMENT_GOOD_CNT); final int downs = comment.optInt(Comment.COMMENT_BAD_CNT); if (ups > 0 || downs > 0) { throw new ServiceException("removeCommentFoundWatchEtcLabel"); } final int thankCnt = (int) rewardQueryService.rewardedCount(commentId, Reward.TYPE_C_COMMENT); if (thankCnt > 0) { throw new ServiceException("removeCommentFoundThankLabel"); } // Perform removal removeCommentByAdmin(commentId); } /** * Removes a comment specified with the given comment id. Calls this method will remove all existed data related * with the specified comment forcibly. * * @param commentId the given comment id */ @Transactional public void removeCommentByAdmin(final String commentId) { try { commentRepository.removeComment(commentId); } catch (final Exception e) { LOGGER.log(Level.ERROR, "Removes a comment error [id=" + commentId + "]", e); } } /** * A user specified by the given sender id thanks the author of a comment specified by the given comment id. * * @param commentId the given comment id * @param senderId the given sender id * @throws ServiceException service exception */ public void thankComment(final String commentId, final String senderId) throws ServiceException { try { final JSONObject comment = commentRepository.get(commentId); if (null == comment) { return; } if (Comment.COMMENT_STATUS_C_INVALID == comment.optInt(Comment.COMMENT_STATUS)) { return; } final JSONObject sender = userRepository.get(senderId); if (null == sender) { return; } if (UserExt.USER_STATUS_C_VALID != sender.optInt(UserExt.USER_STATUS)) { return; } final String receiverId = comment.optString(Comment.COMMENT_AUTHOR_ID); final JSONObject receiver = userRepository.get(receiverId); if (null == receiver) { return; } if (UserExt.USER_STATUS_C_VALID != receiver.optInt(UserExt.USER_STATUS)) { return; } if (receiverId.equals(senderId)) { throw new ServiceException(langPropsService.get("thankSelfLabel")); } final int rewardPoint = Symphonys.getInt("pointThankComment"); if (rewardQueryService.isRewarded(senderId, commentId, Reward.TYPE_C_COMMENT)) { return; } final String rewardId = Ids.genTimeMillisId(); if (Comment.COMMENT_ANONYMOUS_C_PUBLIC == comment.optInt(Comment.COMMENT_ANONYMOUS)) { final boolean succ = null != pointtransferMgmtService.transfer(senderId, receiverId, Pointtransfer.TRANSFER_TYPE_C_COMMENT_REWARD, rewardPoint, rewardId, System.currentTimeMillis()); if (!succ) { throw new ServiceException(langPropsService.get("transferFailLabel")); } } final JSONObject reward = new JSONObject(); reward.put(Keys.OBJECT_ID, rewardId); reward.put(Reward.SENDER_ID, senderId); reward.put(Reward.DATA_ID, commentId); reward.put(Reward.TYPE, Reward.TYPE_C_COMMENT); rewardMgmtService.addReward(reward); final JSONObject notification = new JSONObject(); notification.put(Notification.NOTIFICATION_USER_ID, receiverId); notification.put(Notification.NOTIFICATION_DATA_ID, rewardId); notificationMgmtService.addCommentThankNotification(notification); livenessMgmtService.incLiveness(senderId, Liveness.LIVENESS_THANK); } catch (final RepositoryException e) { LOGGER.log(Level.ERROR, "Thanks a comment[id=" + commentId + "] failed", e); throw new ServiceException(e); } } /** * Adds a comment with the specified request json object. * * @param requestJSONObject the specified request json object, for example, * "commentContent": "", * "commentAuthorId": "", * "commentOnArticleId": "", * "commentOriginalCommentId": "", // optional * "clientCommentId": "" // optional, * "commentAuthorName": "" // If from client * "commenter": { * // User model * }, * "commentIP": "", // optional, default to "" * "commentUA": "", // optional, default to "" * "commentAnonymous": int, // optional, default to 0 (public) * "userCommentViewMode": int * , see {@link Comment} for more details * @return generated comment id * @throws ServiceException service exception */ public synchronized String addComment(final JSONObject requestJSONObject) throws ServiceException { final long currentTimeMillis = System.currentTimeMillis(); final JSONObject commenter = requestJSONObject.optJSONObject(Comment.COMMENT_T_COMMENTER); final String commentAuthorId = requestJSONObject.optString(Comment.COMMENT_AUTHOR_ID); final boolean fromClient = requestJSONObject.has(Comment.COMMENT_CLIENT_COMMENT_ID); final String articleId = requestJSONObject.optString(Comment.COMMENT_ON_ARTICLE_ID); final String ip = requestJSONObject.optString(Comment.COMMENT_IP); String ua = requestJSONObject.optString(Comment.COMMENT_UA); final int commentAnonymous = requestJSONObject.optInt(Comment.COMMENT_ANONYMOUS); final int commentViewMode = requestJSONObject.optInt(UserExt.USER_COMMENT_VIEW_MODE); if (currentTimeMillis - commenter.optLong(UserExt.USER_LATEST_CMT_TIME) < Symphonys.getLong("minStepCmtTime") && !Role.ROLE_ID_C_ADMIN.equals(commenter.optString(User.USER_ROLE)) && !UserExt.DEFAULT_CMTER_ROLE.equals(commenter.optString(User.USER_ROLE))) { LOGGER.log(Level.WARN, "Adds comment too frequent [userName={0}]", commenter.optString(User.USER_NAME)); throw new ServiceException(langPropsService.get("tooFrequentCmtLabel")); } final String commenterName = commenter.optString(User.USER_NAME); JSONObject article; try { // check if admin allow to add comment final JSONObject option = optionRepository.get(Option.ID_C_MISC_ALLOW_ADD_COMMENT); if (!"0".equals(option.optString(Option.OPTION_VALUE))) { throw new ServiceException(langPropsService.get("notAllowAddCommentLabel")); } final int balance = commenter.optInt(UserExt.USER_POINT); if (Comment.COMMENT_ANONYMOUS_C_ANONYMOUS == commentAnonymous) { final int anonymousPoint = Symphonys.getInt("anonymous.point"); if (balance < anonymousPoint) { String anonymousEnabelPointLabel = langPropsService.get("anonymousEnabelPointLabel"); anonymousEnabelPointLabel = anonymousEnabelPointLabel.replace("${point}", String.valueOf(anonymousPoint)); throw new ServiceException(anonymousEnabelPointLabel); } } article = articleRepository.get(articleId); if (!fromClient && !TuringQueryService.ROBOT_NAME.equals(commenterName)) { int pointSum = Pointtransfer.TRANSFER_SUM_C_ADD_COMMENT; // Point final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID); if (articleAuthorId.equals(commentAuthorId)) { pointSum = Pointtransfer.TRANSFER_SUM_C_ADD_SELF_ARTICLE_COMMENT; } if (balance - pointSum < 0) { throw new ServiceException(langPropsService.get("insufficientBalanceLabel")); } } } catch (final RepositoryException e) { throw new ServiceException(e); } final int articleAnonymous = article.optInt(Article.ARTICLE_ANONYMOUS); final Transaction transaction = commentRepository.beginTransaction(); try { article.put(Article.ARTICLE_COMMENT_CNT, article.optInt(Article.ARTICLE_COMMENT_CNT) + 1); article.put(Article.ARTICLE_LATEST_CMTER_NAME, commenter.optString(User.USER_NAME)); if (Comment.COMMENT_ANONYMOUS_C_ANONYMOUS == commentAnonymous) { article.put(Article.ARTICLE_LATEST_CMTER_NAME, UserExt.ANONYMOUS_USER_NAME); } article.put(Article.ARTICLE_LATEST_CMT_TIME, currentTimeMillis); final String ret = Ids.genTimeMillisId(); final JSONObject comment = new JSONObject(); comment.put(Keys.OBJECT_ID, ret); String content = requestJSONObject.optString(Comment.COMMENT_CONTENT). replace("_esc_enter_88250_", "<br/>"); // Solo client escape comment.put(Comment.COMMENT_AUTHOR_ID, commentAuthorId); comment.put(Comment.COMMENT_ON_ARTICLE_ID, articleId); if (fromClient) { comment.put(Comment.COMMENT_CLIENT_COMMENT_ID, requestJSONObject.optString(Comment.COMMENT_CLIENT_COMMENT_ID)); // Appends original commenter name final String authorName = requestJSONObject.optString(Comment.COMMENT_T_AUTHOR_NAME); content += " <i class='ft-small'>by " + authorName + "</i>"; } final String originalCmtId = requestJSONObject.optString(Comment.COMMENT_ORIGINAL_COMMENT_ID); comment.put(Comment.COMMENT_ORIGINAL_COMMENT_ID, originalCmtId); if (StringUtils.isNotBlank(originalCmtId)) { final JSONObject originalCmt = commentRepository.get(originalCmtId); final int originalCmtReplyCnt = originalCmt.optInt(Comment.COMMENT_REPLY_CNT); originalCmt.put(Comment.COMMENT_REPLY_CNT, originalCmtReplyCnt + 1); commentRepository.update(originalCmtId, originalCmt); } content = Emotions.toAliases(content); content = content.replaceAll("\\s+$", ""); // https://github.com/b3log/symphony/issues/389 content += " "; // in case of tailing @user content = content.replace(langPropsService.get("uploadingLabel", Locale.SIMPLIFIED_CHINESE), ""); content = content.replace(langPropsService.get("uploadingLabel", Locale.US), ""); comment.put(Comment.COMMENT_CONTENT, content); comment.put(Comment.COMMENT_CREATE_TIME, System.currentTimeMillis()); comment.put(Comment.COMMENT_SHARP_URL, "/article/" + articleId + "#" + ret); comment.put(Comment.COMMENT_STATUS, Comment.COMMENT_STATUS_C_VALID); comment.put(Comment.COMMENT_IP, ip); if (StringUtils.length(ua) > Common.MAX_LENGTH_UA) { LOGGER.log(Level.WARN, "UA is too long [" + ua + "]"); ua = StringUtils.substring(ua, 0, Common.MAX_LENGTH_UA); } comment.put(Comment.COMMENT_UA, ua); comment.put(Comment.COMMENT_ANONYMOUS, commentAnonymous); final JSONObject cmtCntOption = optionRepository.get(Option.ID_C_STATISTIC_CMT_COUNT); final int cmtCnt = cmtCntOption.optInt(Option.OPTION_VALUE); cmtCntOption.put(Option.OPTION_VALUE, String.valueOf(cmtCnt + 1)); articleRepository.update(articleId, article); // Updates article comment count, latest commenter name and time optionRepository.update(Option.ID_C_STATISTIC_CMT_COUNT, cmtCntOption); // Updates global comment count // Updates tag comment count and User-Tag relation final String tagsString = article.optString(Article.ARTICLE_TAGS); final String[] tagStrings = tagsString.split(","); for (int i = 0; i < tagStrings.length; i++) { final String tagTitle = tagStrings[i].trim(); final JSONObject tag = tagRepository.getByTitle(tagTitle); tag.put(Tag.TAG_COMMENT_CNT, tag.optInt(Tag.TAG_COMMENT_CNT) + 1); tag.put(Tag.TAG_RANDOM_DOUBLE, Math.random()); tagRepository.update(tag.optString(Keys.OBJECT_ID), tag); } // Updates user comment count, latest comment time commenter.put(UserExt.USER_COMMENT_COUNT, commenter.optInt(UserExt.USER_COMMENT_COUNT) + 1); commenter.put(UserExt.USER_LATEST_CMT_TIME, currentTimeMillis); userRepository.update(commenter.optString(Keys.OBJECT_ID), commenter); comment.put(Comment.COMMENT_GOOD_CNT, 0); comment.put(Comment.COMMENT_BAD_CNT, 0); comment.put(Comment.COMMENT_SCORE, 0D); comment.put(Comment.COMMENT_REPLY_CNT, 0); comment.put(Comment.COMMENT_AUDIO_URL, ""); // Adds the comment final String commentId = commentRepository.add(comment); // Updates tag-article relation stat. final List<JSONObject> tagArticleRels = tagArticleRepository.getByArticleId(articleId); for (final JSONObject tagArticleRel : tagArticleRels) { tagArticleRel.put(Article.ARTICLE_LATEST_CMT_TIME, currentTimeMillis); tagArticleRel.put(Article.ARTICLE_COMMENT_CNT, article.optInt(Article.ARTICLE_COMMENT_CNT)); tagArticleRepository.update(tagArticleRel.optString(Keys.OBJECT_ID), tagArticleRel); } // Revision final JSONObject revision = new JSONObject(); revision.put(Revision.REVISION_AUTHOR_ID, comment.optString(Comment.COMMENT_AUTHOR_ID)); final JSONObject revisionData = new JSONObject(); revisionData.put(Comment.COMMENT_CONTENT, content); revision.put(Revision.REVISION_DATA, revisionData.toString()); revision.put(Revision.REVISION_DATA_ID, commentId); revision.put(Revision.REVISION_DATA_TYPE, Revision.DATA_TYPE_C_COMMENT); revisionRepository.add(revision); transaction.commit(); if (!fromClient && Comment.COMMENT_ANONYMOUS_C_PUBLIC == commentAnonymous && Article.ARTICLE_ANONYMOUS_C_PUBLIC == articleAnonymous && !TuringQueryService.ROBOT_NAME.equals(commenterName)) { // Point final String articleAuthorId = article.optString(Article.ARTICLE_AUTHOR_ID); if (articleAuthorId.equals(commentAuthorId)) { pointtransferMgmtService.transfer(commentAuthorId, Pointtransfer.ID_C_SYS, Pointtransfer.TRANSFER_TYPE_C_ADD_COMMENT, Pointtransfer.TRANSFER_SUM_C_ADD_SELF_ARTICLE_COMMENT, commentId, System.currentTimeMillis()); } else { pointtransferMgmtService.transfer(commentAuthorId, articleAuthorId, Pointtransfer.TRANSFER_TYPE_C_ADD_COMMENT, Pointtransfer.TRANSFER_SUM_C_ADD_COMMENT, commentId, System.currentTimeMillis()); } livenessMgmtService.incLiveness(commentAuthorId, Liveness.LIVENESS_COMMENT); } // Event final JSONObject eventData = new JSONObject(); eventData.put(Comment.COMMENT, comment); eventData.put(Common.FROM_CLIENT, fromClient); eventData.put(Article.ARTICLE, article); eventData.put(UserExt.USER_COMMENT_VIEW_MODE, commentViewMode); try { eventManager.fireEventAsynchronously(new Event<JSONObject>(EventTypes.ADD_COMMENT_TO_ARTICLE, eventData)); } catch (final EventException e) { LOGGER.log(Level.ERROR, e.getMessage(), e); } return ret; } catch (final RepositoryException e) { if (transaction.isActive()) { transaction.rollback(); } LOGGER.log(Level.ERROR, "Adds a comment failed", e); throw new ServiceException(e); } } /** * Updates the specified comment by the given comment id. * * @param commentId the given comment id * @param comment the specified comment * @throws ServiceException service exception */ public void updateComment(final String commentId, final JSONObject comment) throws ServiceException { final Transaction transaction = commentRepository.beginTransaction(); try { final JSONObject oldComment = commentRepository.get(commentId); final String oldContent = oldComment.optString(Comment.COMMENT_CONTENT); String content = comment.optString(Comment.COMMENT_CONTENT); content = Emotions.toAliases(content); content = content.replaceAll("\\s+$", ""); // https://github.com/b3log/symphony/issues/389 content += " "; // in case of tailing @user content = content.replace(langPropsService.get("uploadingLabel", Locale.SIMPLIFIED_CHINESE), ""); content = content.replace(langPropsService.get("uploadingLabel", Locale.US), ""); comment.put(Comment.COMMENT_CONTENT, content); commentRepository.update(commentId, comment); final String commentAuthorId = comment.optString(Comment.COMMENT_AUTHOR_ID); if (!oldContent.equals(content)) { // Revision final JSONObject revision = new JSONObject(); revision.put(Revision.REVISION_AUTHOR_ID, commentAuthorId); final JSONObject revisionData = new JSONObject(); revisionData.put(Comment.COMMENT_CONTENT, content); revision.put(Revision.REVISION_DATA, revisionData.toString()); revision.put(Revision.REVISION_DATA_ID, commentId); revision.put(Revision.REVISION_DATA_TYPE, Revision.DATA_TYPE_C_COMMENT); revisionRepository.add(revision); } transaction.commit(); final JSONObject article = articleRepository.get(comment.optString(Comment.COMMENT_ON_ARTICLE_ID)); final int articleAnonymous = article.optInt(Article.ARTICLE_ANONYMOUS); final int commentAnonymous = comment.optInt(Comment.COMMENT_ANONYMOUS); if (Comment.COMMENT_ANONYMOUS_C_PUBLIC == commentAnonymous && Article.ARTICLE_ANONYMOUS_C_PUBLIC == articleAnonymous) { // Point final long now = System.currentTimeMillis(); final long createTime = comment.optLong(Keys.OBJECT_ID); if (now - createTime > 1000 * 60 * 5) { pointtransferMgmtService.transfer(commentAuthorId, Pointtransfer.ID_C_SYS, Pointtransfer.TRANSFER_TYPE_C_UPDATE_COMMENT, Pointtransfer.TRANSFER_SUM_C_UPDATE_COMMENT, commentId, now); } } final boolean fromClient = comment.has(Comment.COMMENT_CLIENT_COMMENT_ID); // Event final JSONObject eventData = new JSONObject(); eventData.put(Common.FROM_CLIENT, fromClient); eventData.put(Article.ARTICLE, article); eventData.put(Comment.COMMENT, comment); try { eventManager.fireEventAsynchronously(new Event<>(EventTypes.UPDATE_COMMENT, eventData)); } catch (final EventException e) { LOGGER.log(Level.ERROR, e.getMessage(), e); } } catch (final RepositoryException e) { if (transaction.isActive()) { transaction.rollback(); } LOGGER.log(Level.ERROR, "Updates a comment[id=" + commentId + "] failed", e); throw new ServiceException(e); } } }