/** * Yobi, Project Hosting SW * * Copyright 2013 NAVER Corp. * http://yobi.io * * @author Yi EungJun * * 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 models; import com.avaje.ebean.RawSqlBuilder; import controllers.UserApp; import controllers.routes; import notification.INotificationEvent; import models.enumeration.*; import models.resource.GlobalResource; import models.resource.Resource; import models.resource.ResourceConvertible; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Predicate; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.revwalk.RevCommit; import org.joda.time.DateTime; import org.tmatesoft.svn.core.SVNException; import play.api.i18n.Lang; import play.db.ebean.Model; import play.i18n.Messages; import play.libs.Akka; import playRepository.*; import scala.concurrent.duration.Duration; import utils.AccessControl; import utils.EventConstants; import utils.RouteUtil; import javax.naming.LimitExceededException; import javax.persistence.*; import javax.servlet.ServletException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import static models.enumeration.EventType.*; @Entity public class NotificationEvent extends Model implements INotificationEvent { private static final long serialVersionUID = 1L; @Id public Long id; public static final Finder<Long, NotificationEvent> find = new Finder<>(Long.class, NotificationEvent.class); public String title; public Long senderId; @ManyToMany(cascade = CascadeType.ALL) public Set<User> receivers; @Temporal(TemporalType.TIMESTAMP) public Date created; @Enumerated(EnumType.STRING) public ResourceType resourceType; public String resourceId; @Enumerated(EnumType.STRING) public EventType eventType; @Lob public String oldValue; @Lob public String newValue; @OneToOne(mappedBy="notificationEvent", cascade = CascadeType.ALL) public NotificationMail notificationMail; /** * Returns receivers. * * This is much faster than field access to {@link #receivers}. * * @return receivers */ public Set<User> findReceivers() { String sql = "select n4user.id from n4user where id in (select n4user_id " + "from notification_event_n4user where " + "notification_event_id = '" + id + "')"; return User.find.setRawSql(RawSqlBuilder.parse(sql).create()).findSet(); } @Override public void setReceivers(Set<User> receivers) { throw new UnsupportedOperationException(); } public String getOldValue() { return oldValue; } @Transient public String getMessage() { return getMessage(Lang.defaultLang()); } @Transient public String getMessage(Lang lang) { switch (eventType) { case ISSUE_STATE_CHANGED: if (newValue.equals(State.CLOSED.state())) { return Messages.get(lang, "notification.issue.closed"); } else { return Messages.get(lang, "notification.issue.reopened"); } case ISSUE_ASSIGNEE_CHANGED: if (newValue == null) { return Messages.get(lang, "notification.issue.unassigned"); } else { return Messages.get(lang, "notification.issue.assigned", newValue); } case NEW_ISSUE: case NEW_POSTING: case NEW_COMMENT: case NEW_PULL_REQUEST: case NEW_COMMIT: case ISSUE_BODY_CHANGED: case COMMENT_UPDATED: return newValue; case NEW_REVIEW_COMMENT: try { ReviewComment reviewComment = ReviewComment.find.byId(Long.valueOf(this.resourceId)); if (reviewComment != null) { return buildCommentedCodeMessage(reviewComment, lang); } } catch (Exception e) { play.Logger.error( "Failed to generate a notification " + "message for a review comment", e); } return newValue; case PULL_REQUEST_STATE_CHANGED: if (State.OPEN.state().equals(newValue)) { return Messages.get(lang, "notification.pullrequest.reopened"); } else { return Messages.get(lang, "notification.pullrequest." + newValue); } case PULL_REQUEST_COMMIT_CHANGED: return newValue; case PULL_REQUEST_MERGED: return Messages.get(lang, "notification.type.pullrequest.merged." + newValue) + "\n" + StringUtils.defaultString(oldValue, StringUtils.EMPTY); case MEMBER_ENROLL_REQUEST: if (RequestState.REQUEST.name().equals(newValue)) { return Messages.get(lang, "notification.member.enroll.request"); } else if (RequestState.ACCEPT.name().equals(newValue)) { return Messages.get(lang, "notification.member.enroll.accept"); } else { return Messages.get(lang, "notification.member.enroll.cancel"); } case ORGANIZATION_MEMBER_ENROLL_REQUEST: if (RequestState.REQUEST.name().equals(newValue)) { return Messages.get(lang, "notification.organization.member.enroll.request"); } else if (RequestState.ACCEPT.name().equals(newValue)) { return Messages.get(lang, "notification.organization.member.enroll.accept"); } else { return Messages.get(lang, "notification.organization.member.enroll.cancel"); } case PULL_REQUEST_REVIEW_STATE_CHANGED: if (PullRequestReviewAction.DONE.name().equals(newValue)) { return Messages.get(lang, "notification.pullrequest.reviewed", User.find.byId(senderId).loginId); } else { return Messages.get(lang, "notification.pullrequest.unreviewed", User.find.byId(senderId).loginId); } case REVIEW_THREAD_STATE_CHANGED: if (newValue.equals(CommentThread.ThreadState.CLOSED.name())) { return Messages.get(lang, "notification.reviewthread.closed"); } else { return Messages.get(lang, "notification.reviewthread.reopened"); } default: return null; } } /** * Builds a notification message for a comment on code. * * The message contains the commented hunk of the code as below: * * In foo.c: * * > @@ -1,5 +1,5 @@ * > int bar(void) * > { * > - printf("bad"); * > + printf("good"); * * Looks good to me * * > return 0; * > } * * Note: This method has a performance issue. See the comment in the method * body for the details. * * @param reviewComment * @param lang * @return * @throws IOException */ private static String buildCommentedCodeMessage(ReviewComment reviewComment, Lang lang) throws IOException { if (reviewComment.thread == null || !reviewComment.thread.getFirstReviewComment().equals(reviewComment) || !(reviewComment.thread instanceof CodeCommentThread)) { return reviewComment.getContents(); } CodeCommentThread thread = (CodeCommentThread) reviewComment.thread; PlayRepository repo; try { repo = RepositoryService.getRepository(thread.project); } catch (Exception e) { play.Logger.error("Failed to get the repository", e); return reviewComment.getContents(); } CodeRange codeRange = thread.codeRange; List<FileDiff> diffs; if (thread.prevCommitId == null) { diffs = repo.getDiff(thread.commitId); } else { diffs = repo.getDiff(thread.prevCommitId, thread.commitId); } for(FileDiff diff : diffs) { if (!codeRange.isFor(diff)) continue; StringBuilder message = new StringBuilder(); message.append(Messages.get(lang, "notification.reviewthread.inTheFile", codeRange.path)); message.append("\n"); diff.setInterestLine(codeRange.endLine); diff.setInterestSide(codeRange.endSide); // FIXME: Performance Issue: The hunks of this diffs were // already computed but it was not necessary because they will // and should be recomputed here. FileDiff.Hunks hunks = diff.getHunks(); if (hunks != null) { message.append("```diff\n"); for (Hunk hunk : hunks) { message.append( String.format("> @@ -%d, %d +%d, %d @@\n", hunk.beginA + 1, (hunk.endA - hunk.beginA), hunk.beginB + 1, (hunk.endB - hunk.beginB))); for (DiffLine line : hunk.lines) { message.append("> "); switch (line.kind) { case CONTEXT: message.append(" "); break; case ADD: message.append("+"); break; case REMOVE: message.append("-"); break; } message.append(line.content + "\n"); if (codeRange.endsWith(line)) { message.append("```\n"); message.append("\n" + reviewComment.getContents() + "\n\n"); message.append("```diff\n"); } } } message.append("```\n"); } else { message.append(reviewComment.getContents()); } return message.toString(); } return reviewComment.getContents(); } public User getSender() { return User.find.byId(this.senderId); } public Resource getResource() { return Resource.get(resourceType, resourceId); } public Project getProject() { switch(resourceType) { case ISSUE_ASSIGNEE: return Assignee.finder.byId(Long.valueOf(resourceId)).project; case PROJECT: return Project.find.byId(Long.valueOf(resourceId)); default: Resource resource = getResource(); if (resource != null) { if (resource instanceof GlobalResource) { return null; } else { return resource.getProject(); } } else { return null; } } } public Organization getOrganization() { switch (resourceType) { case ORGANIZATION: return Organization.find.byId(Long.valueOf(resourceId)); default: return null; } } public boolean resourceExists() { return Resource.exists(resourceType, resourceId); } public static void add(NotificationEvent event) { if (event.notificationMail == null) { event.notificationMail = new NotificationMail(); event.notificationMail.notificationEvent = event; } Date draftDate = DateTime.now().minusMillis(EventConstants.DRAFT_TIME_IN_MILLIS).toDate(); NotificationEvent lastEvent = NotificationEvent.find.where() .eq("resourceId", event.resourceId) .eq("resourceType", event.resourceType) .gt("created", draftDate) .orderBy("id desc").setMaxRows(1).findUnique(); if (lastEvent != null) { if (lastEvent.eventType == event.eventType && event.senderId.equals(lastEvent.senderId)) { // If the last event is A -> B and the current event is B -> C, // they are merged into the new event A -> C. event.oldValue = lastEvent.getOldValue(); lastEvent.delete(); // If the last event is A -> B and the current event is B -> A, // they are removed. if (StringUtils.equals(event.oldValue, event.newValue)) { return; } } } filterReceivers(event); if (event.receivers.isEmpty()) { return; } event.save(); event.saveManyToManyAssociations("receivers"); } private static void filterReceivers(final NotificationEvent event) { final Project project = event.getProject(); if (project == null) { return; } final Resource resource = project.asResource(); CollectionUtils.filter(event.receivers, new Predicate() { @Override public boolean evaluate(Object obj) { User receiver = (User) obj; if(receiver.loginId == null) { return false; } if (!AccessControl.isAllowed(receiver, event.getResource(), Operation.READ)) { return false; } if (!Watch.isWatching(receiver, resource)) { return true; } return UserProjectNotification.isEnabledNotiType(receiver, project, event.eventType); } }); } public static void deleteBy(Resource resource) { for (NotificationEvent event : NotificationEvent.find.where().where().eq("resourceType", resource.getType()).eq("resourceId", resource.getId()).findList()) { event.delete(); } } /** * @see {@link controllers.PullRequestApp#newPullRequest(String, String)} */ public static NotificationEvent afterNewPullRequest(User sender, PullRequest pullRequest) { NotificationEvent notiEvent = createFrom(sender, pullRequest); notiEvent.title = formatNewTitle(pullRequest); notiEvent.receivers = getReceiversWithRelatedAuthors(sender, pullRequest); notiEvent.eventType = NEW_PULL_REQUEST; notiEvent.oldValue = null; notiEvent.newValue = pullRequest.body; NotificationEvent.add(notiEvent); return notiEvent; } public String getUrlToView() { switch(eventType) { case MEMBER_ENROLL_REQUEST: if (getProject() == null) { return null; } else { return routes.ProjectApp.members( getProject().owner, getProject().name).url(); } case ORGANIZATION_MEMBER_ENROLL_REQUEST: Organization organization = getOrganization(); if (organization == null) { return null; } return routes.OrganizationApp.members(organization.name).url(); case NEW_COMMIT: if (getProject() == null) { return null; } else { return routes.CodeHistoryApp.historyUntilHead( getProject().owner, getProject().name).url(); } default: return RouteUtil.getUrl(resourceType, resourceId); } } @Override public Date getCreatedDate() { return created; } @Override public String getTitle() { return title; } @Override public EventType getType() { return eventType; } @Override public ResourceType getResourceType() { return resourceType; } @Override public String getResourceId() { return resourceId; } /** * @see {@link models.PullRequest#merge(models.PullRequestEventMessage)} * @see {@link controllers.PullRequestApp#addNotification(models.PullRequest, models.enumeration.State, models.enumeration.State)} */ public static NotificationEvent afterPullRequestUpdated(User sender, PullRequest pullRequest, State oldState, State newState) { NotificationEvent notiEvent = createFrom(sender, pullRequest); notiEvent.title = formatReplyTitle(pullRequest); notiEvent.receivers = getReceivers(sender, pullRequest); notiEvent.eventType = PULL_REQUEST_STATE_CHANGED; notiEvent.oldValue = oldState.state(); notiEvent.newValue = newState.state(); NotificationEvent.add(notiEvent); return notiEvent; } public static NotificationEvent afterPullRequestCommitChanged(User sender, PullRequest pullRequest) { NotificationEvent notiEvent = createFrom(sender, pullRequest); notiEvent.title = formatReplyTitle(pullRequest); notiEvent.receivers = getReceivers(sender, pullRequest); notiEvent.eventType = PULL_REQUEST_COMMIT_CHANGED; notiEvent.oldValue = null; notiEvent.newValue = newPullRequestCommitChangedMessage(pullRequest); NotificationEvent.add(notiEvent); return notiEvent; } private static String newPullRequestCommitChangedMessage(PullRequest pullRequest) { List<PullRequestCommit> commits = PullRequestCommit.find.where().eq("pullRequest", pullRequest).orderBy().desc("authorDate").findList(); StringBuilder builder = new StringBuilder(); builder.append("### "); builder.append(Messages.get("notification.pullrequest.current.commits")); builder.append("\n"); for (PullRequestCommit commit : commits) { if (commit.state == PullRequestCommit.State.CURRENT) { builder.append(commit.getCommitShortId()); builder.append(" "); builder.append(commit.getCommitShortMessage()); builder.append("\n"); } } return builder.toString(); } /** * @see {@link actors.PullRequestActor#processPullRequestMerging(models.PullRequestEventMessage, models.PullRequest)} */ public static NotificationEvent afterMerge(User sender, PullRequest pullRequest, State state) { NotificationEvent notiEvent = createFrom(sender, pullRequest); notiEvent.title = formatReplyTitle(pullRequest); notiEvent.receivers = state == State.MERGED ? getReceiversWithRelatedAuthors(sender, pullRequest) : getReceivers(sender, pullRequest); notiEvent.eventType = PULL_REQUEST_MERGED; notiEvent.newValue = state.state(); NotificationEvent.add(notiEvent); return notiEvent; } /** * @see {@link controllers.PullRequestApp#newComment(String, String, Long, String)} */ public static void afterNewComment(User sender, PullRequest pullRequest, ReviewComment newComment, String urlToView) { NotificationEvent.add(forNewComment(sender, pullRequest, newComment)); } public static NotificationEvent forNewComment(User sender, PullRequest pullRequest, ReviewComment newComment) { NotificationEvent notiEvent = createFrom(sender, newComment); notiEvent.title = formatReplyTitle(pullRequest); Set<User> receivers = getMentionedUsers(newComment.getContents()); receivers.addAll(getReceivers(sender, pullRequest)); receivers.remove(User.findByLoginId(newComment.author.loginId)); notiEvent.receivers = receivers; notiEvent.eventType = NEW_REVIEW_COMMENT; notiEvent.oldValue = null; notiEvent.newValue = newComment.getContents(); return notiEvent; } public static NotificationEvent afterNewPullRequest(PullRequest pullRequest) { return afterNewPullRequest(UserApp.currentUser(), pullRequest); } public static NotificationEvent afterPullRequestUpdated(PullRequest pullRequest, State oldState, State newState) { return afterPullRequestUpdated(UserApp.currentUser(), pullRequest, oldState, newState); } public static void afterNewComment(Comment comment) { NotificationEvent.add(forNewComment(comment, UserApp.currentUser())); } public static NotificationEvent forComment(Comment comment, User author, EventType eventType) { AbstractPosting post = comment.getParent(); NotificationEvent notiEvent = createFrom(author, comment); notiEvent.title = formatReplyTitle(post); notiEvent.eventType = eventType; Set<User> receivers = getReceivers(post, author); receivers.addAll(getMentionedUsers(comment.contents)); receivers.remove(author); notiEvent.receivers = receivers; notiEvent.oldValue = null; notiEvent.newValue = comment.contents; notiEvent.resourceType = comment.asResource().getType(); notiEvent.resourceId = comment.asResource().getId(); return notiEvent; } public static NotificationEvent forUpdatedComment(Comment comment, User author) { return forComment(comment, author, COMMENT_UPDATED); } public static NotificationEvent forNewComment(Comment comment, User author) { return forComment(comment, author, NEW_COMMENT); } public static void afterNewCommentWithState(Comment comment, State state) { AbstractPosting post = comment.getParent(); NotificationEvent notiEvent = createFromCurrentUser(comment); notiEvent.title = formatReplyTitle(post); Set<User> receivers = getReceivers(post); receivers.addAll(getMentionedUsers(comment.contents)); receivers.remove(UserApp.currentUser()); notiEvent.receivers = receivers; notiEvent.eventType = NEW_COMMENT; notiEvent.oldValue = null; notiEvent.newValue = comment.contents + "\n" + state.state(); notiEvent.resourceType = comment.asResource().getType(); notiEvent.resourceId = comment.asResource().getId(); NotificationEvent.add(notiEvent); } public static NotificationEvent afterStateChanged(State oldState, Issue issue) { NotificationEvent notiEvent = createFromCurrentUser(issue); notiEvent.title = formatReplyTitle(issue); notiEvent.receivers = getReceivers(issue); notiEvent.eventType = ISSUE_STATE_CHANGED; notiEvent.oldValue = oldState != null ? oldState.state() : null; notiEvent.newValue = issue.state.state(); NotificationEvent.add(notiEvent); return notiEvent; } public static NotificationEvent afterStateChanged( CommentThread.ThreadState oldState, CommentThread thread) throws IOException, SVNException, ServletException { NotificationEvent notiEvent = createFromCurrentUser(thread); notiEvent.eventType = REVIEW_THREAD_STATE_CHANGED; notiEvent.oldValue = oldState.name() != null ? oldState.name() : null; notiEvent.newValue = thread.state.name(); // Set receivers Set<User> receivers; if (thread.isOnPullRequest()) { PullRequest pullRequest = thread.pullRequest; notiEvent.title = formatReplyTitle(pullRequest); receivers = pullRequest.getWatchers(); } else { String commitId; if (thread instanceof CodeCommentThread) { commitId = ((CodeCommentThread)thread).commitId; } else { commitId = ((NonRangedCodeCommentThread)thread).commitId; } Project project = thread.project; Commit commit = RepositoryService.getRepository(project).getCommit(commitId); notiEvent.title = formatReplyTitle(project, commit); receivers = commit.getWatchers(project); } receivers.remove(UserApp.currentUser()); notiEvent.receivers = receivers; NotificationEvent.add(notiEvent); return notiEvent; } public static NotificationEvent afterAssigneeChanged(User oldAssignee, Issue issue) { NotificationEvent notiEvent = createFromCurrentUser(issue); Set<User> receivers = getReceivers(issue); if(oldAssignee != null) { notiEvent.oldValue = oldAssignee.loginId; if(!oldAssignee.loginId.equals(UserApp.currentUser().loginId)) { receivers.add(oldAssignee); } } if (issue.assignee != null) { notiEvent.newValue = User.find.byId(issue.assignee.user.id).loginId; } notiEvent.title = formatReplyTitle(issue); notiEvent.receivers = receivers; notiEvent.eventType = ISSUE_ASSIGNEE_CHANGED; NotificationEvent.add(notiEvent); return notiEvent; } public static void afterNewIssue(Issue issue) { NotificationEvent.add(forNewIssue(issue, UserApp.currentUser())); } public static NotificationEvent forNewIssue(Issue issue, User author) { NotificationEvent notiEvent = createFrom(author, issue); notiEvent.title = formatNewTitle(issue); notiEvent.receivers = getReceivers(issue, author); notiEvent.eventType = NEW_ISSUE; notiEvent.oldValue = null; notiEvent.newValue = issue.body; return notiEvent; } public static NotificationEvent afterIssueBodyChanged(String oldBody, Issue issue) { NotificationEvent notiEvent = createFromCurrentUser(issue); notiEvent.title = formatReplyTitle(issue); notiEvent.receivers = getReceiversForIssueBodyChanged(oldBody, issue); notiEvent.eventType = EventType.ISSUE_BODY_CHANGED; notiEvent.oldValue = oldBody; notiEvent.newValue = issue.body; NotificationEvent.add(notiEvent); return notiEvent; } private static Set<User> getReceiversForIssueBodyChanged(String oldBody, Issue issue) { Set<User> receivers = issue.getWatchers(); receivers.addAll(getNewMentionedUsers(oldBody, issue.body)); receivers.remove(UserApp.currentUser()); return receivers; } public static void afterNewPost(Posting post) { NotificationEvent.add(forNewPosting(post, UserApp.currentUser())); } public static NotificationEvent forNewPosting(Posting post, User author) { NotificationEvent notiEvent = createFrom(author, post); notiEvent.title = formatNewTitle(post); notiEvent.receivers = getReceivers(post); notiEvent.eventType = NEW_POSTING; notiEvent.oldValue = null; notiEvent.newValue = post.body; return notiEvent; } public static void afterNewCommitComment(Project project, ReviewComment comment, String commitId) throws IOException, SVNException, ServletException { NotificationEvent.add( forNewCommitComment(project, comment, commitId, UserApp.currentUser())); } public static NotificationEvent forNewCommitComment( Project project, ReviewComment comment, String commitId, User author) throws IOException, SVNException, ServletException { Commit commit = RepositoryService.getRepository(project).getCommit(commitId); Set<User> watchers = commit.getWatchers(project); watchers.addAll(getMentionedUsers(comment.getContents())); watchers.remove(author); NotificationEvent notiEvent = createFrom(author, comment); notiEvent.title = formatReplyTitle(project, commit); notiEvent.receivers = watchers; notiEvent.eventType = NEW_REVIEW_COMMENT; notiEvent.oldValue = null; notiEvent.newValue = comment.getContents(); return notiEvent; } public static void afterNewSVNCommitComment(Project project, CommitComment codeComment) throws IOException, SVNException, ServletException { NotificationEvent.add(forNewSVNCommitComment(project, codeComment, UserApp.currentUser())); } private static NotificationEvent forNewSVNCommitComment( Project project, CommitComment codeComment, User author) throws IOException, SVNException, ServletException { Commit commit = RepositoryService.getRepository(project).getCommit(codeComment.commitId); Set<User> watchers = commit.getWatchers(project); watchers.addAll(getMentionedUsers(codeComment.contents)); watchers.remove(author); NotificationEvent notiEvent = createFromCurrentUser(codeComment); notiEvent.title = formatReplyTitle(project, commit); notiEvent.receivers = watchers; notiEvent.eventType = NEW_COMMENT; notiEvent.oldValue = null; notiEvent.newValue = codeComment.contents; return notiEvent; } public static void afterMemberRequest(Project project, User user, RequestState state) { NotificationEvent notiEvent = createFromCurrentUser(project); notiEvent.eventType = MEMBER_ENROLL_REQUEST; notiEvent.receivers = getReceivers(project); notiEvent.newValue = state.name(); if (state == RequestState.ACCEPT || state == RequestState.REJECT) { notiEvent.receivers.remove(UserApp.currentUser()); notiEvent.receivers.add(user); } switch (state) { case REQUEST: notiEvent.title = formatMemberRequestTitle(project, user); notiEvent.oldValue = RequestState.CANCEL.name(); break; case CANCEL: notiEvent.title = formatMemberRequestCancelTitle(project, user); notiEvent.oldValue = RequestState.REQUEST.name(); break; case ACCEPT: notiEvent.title = formatMemberAcceptTitle(project, user); notiEvent.oldValue = RequestState.REQUEST.name(); break; } notiEvent.resourceType = project.asResource().getType(); notiEvent.resourceId = project.asResource().getId(); NotificationEvent.add(notiEvent); } public static void afterOrganizationMemberRequest(Organization organization, User user, RequestState state) { NotificationEvent notiEvent = createFromCurrentUser(organization); notiEvent.eventType = ORGANIZATION_MEMBER_ENROLL_REQUEST; notiEvent.receivers = getReceivers(organization); notiEvent.newValue = state.name(); if (state == RequestState.ACCEPT || state == RequestState.REJECT) { notiEvent.receivers.remove(UserApp.currentUser()); notiEvent.receivers.add(user); } switch (state) { case REQUEST: notiEvent.title = formatMemberRequestTitle(organization, user); notiEvent.oldValue = RequestState.CANCEL.name(); break; case CANCEL: notiEvent.title = formatMemberRequestCancelTitle(organization, user); notiEvent.oldValue = RequestState.REQUEST.name(); break; case ACCEPT: notiEvent.title = formatMemberAcceptTitle(organization, user); notiEvent.oldValue = RequestState.REQUEST.name(); break; } notiEvent.resourceType = organization.asResource().getType(); notiEvent.resourceId = organization.asResource().getId(); NotificationEvent.add(notiEvent); } public static void afterNewCommits(List<RevCommit> commits, List<String> refNames, Project project, User sender, String title, Set<User> watchers) { NotificationEvent notiEvent = createFrom(sender, project); notiEvent.title = title; notiEvent.receivers = watchers; notiEvent.eventType = NEW_COMMIT; notiEvent.oldValue = null; notiEvent.newValue = newCommitsMessage(commits, refNames, project); notiEvent.resourceType = project.asResource().getType(); notiEvent.resourceId = project.asResource().getId(); NotificationEvent.add(notiEvent); } public static NotificationEvent afterReviewed(PullRequest pullRequest, PullRequestReviewAction reviewAction) { String title = formatReplyTitle(pullRequest); Resource resource = pullRequest.asResource(); Set<User> receivers = pullRequest.getWatchers(); receivers.add(pullRequest.contributor); User reviewer = UserApp.currentUser(); receivers.remove(reviewer); NotificationEvent notiEvent = new NotificationEvent(); notiEvent.created = new Date(); notiEvent.title = title; notiEvent.senderId = reviewer.id; notiEvent.receivers = receivers; notiEvent.resourceId = resource.getId(); notiEvent.resourceType = resource.getType(); notiEvent.eventType = EventType.PULL_REQUEST_REVIEW_STATE_CHANGED; notiEvent.oldValue = reviewAction.getOppositAction().name(); notiEvent.newValue = reviewAction.name(); add(notiEvent); return notiEvent; } private static String newCommitsMessage(List<RevCommit> commits, List<String> refNames, Project project) { StringBuilder result = new StringBuilder(); if(commits.size() > 0) { result.append("### " + Messages.get("notification.pushed.newcommits") + "\n"); result.append("```\n"); for(RevCommit commit : commits) { GitCommit gitCommit = new GitCommit(commit); result.append(gitCommit.getShortId()); result.append(" "); result.append(gitCommit.getShortMessage()); result.append("\n"); } result.append("```\n\n"); } if(refNames.size() > 0) { result.append("### " + Messages.get("notification.pushed.branches") + "\n"); for(String refName: refNames) { try { result.append("[" + refName + "](" + routes.CodeHistoryApp.history(project.owner, project.name, URLEncoder.encode(refName, "UTF-8"), "") + ")"); } catch(UnsupportedEncodingException e){ result.append(refName); } result.append("\n"); } } return result.toString(); } private static NotificationEvent createFrom(User sender, ResourceConvertible rc) { NotificationEvent notiEvent = new NotificationEvent(); notiEvent.senderId = sender.id; notiEvent.created = new Date(); Resource resource = rc.asResource(); notiEvent.resourceId = resource.getId(); notiEvent.resourceType = resource.getType(); return notiEvent; } /** * @see {@link #createFrom(models.User, models.resource.ResourceConvertible)} */ private static NotificationEvent createFromCurrentUser(ResourceConvertible rc) { return createFrom(UserApp.currentUser(), rc); } private static Set<User> getReceivers(AbstractPosting abstractPosting) { return getReceivers(abstractPosting, UserApp.currentUser()); } private static Set<User> getReceivers(AbstractPosting abstractPosting, User except) { Set<User> receivers = abstractPosting.getWatchers(); receivers.addAll(getMentionedUsers(abstractPosting.body)); receivers.remove(except); return receivers; } private static String getPrefixedNumber(AbstractPosting posting) { if (posting instanceof Issue) { return "#" + posting.getNumber(); } else { return posting.getNumber().toString(); } } private static String formatReplyTitle(AbstractPosting posting) { return String.format("Re: [%s] %s (%s)", posting.project.name, posting.title, getPrefixedNumber(posting)); } private static String formatNewTitle(AbstractPosting posting) { return String.format("[%s] %s (%s)", posting.project.name, posting.title, getPrefixedNumber(posting)); } private static String formatReplyTitle(Project project, Commit commit) { return String.format("Re: [%s] %s (%s)", project.name, commit.getShortMessage(), commit.getShortId()); } private static Set<User> getReceivers(User sender, PullRequest pullRequest) { Set<User> watchers = getDefaultReceivers(pullRequest); watchers.remove(sender); return watchers; } private static Set<User> getDefaultReceivers(PullRequest pullRequest) { Set<User> watchers = pullRequest.getWatchers(); watchers.addAll(getMentionedUsers(pullRequest.body)); return watchers; } private static Set<User> getReceiversWithRelatedAuthors(User sender, PullRequest pullRequest) { Set<User> receivers = getDefaultReceivers(pullRequest); String failureMessage = "Failed to get authors related to the pullrequest " + pullRequest; try { if (pullRequest.mergedCommitIdFrom != null && pullRequest.mergedCommitIdTo != null) { receivers.addAll(GitRepository.getRelatedAuthors( new GitRepository(pullRequest.toProject).getRepository(), pullRequest.mergedCommitIdFrom, pullRequest.mergedCommitIdTo)); } } catch (LimitExceededException e) { for (ProjectUser member : pullRequest.toProject.members()) { receivers.add(member.user); } play.Logger.info(failureMessage + ": Get all project members instead", e); } catch (GitAPIException e) { play.Logger.warn(failureMessage, e); } catch (IOException e) { play.Logger.warn(failureMessage, e); } receivers.remove(sender); return receivers; } private static String formatNewTitle(PullRequest pullRequest) { return String.format("[%s] %s (#%d)", pullRequest.toProject.name, pullRequest.title, pullRequest.number); } private static String formatReplyTitle(PullRequest pullRequest) { return String.format("Re: [%s] %s (#%s)", pullRequest.toProject.name, pullRequest.title, pullRequest.number); } private static Set<User> getReceivers(Project project) { Set<User> receivers = new HashSet<>(); List<User> managers = User.findUsersByProject(project.id, RoleType.MANAGER); for (User manager : managers) { if (Watch.isWatching(manager, project.asResource())) { receivers.add(manager); } } return receivers; } private static Set<User> getReceivers(Organization organization) { Set<User> receivers = new HashSet<>(); List<User> managers = User.findUsersByOrganization(organization.id, RoleType.ORG_ADMIN); receivers.addAll(managers); return receivers; } private static String formatMemberRequestTitle(Project project, User user) { return Messages.get("notification.member.request.title", project.name, user.loginId); } private static String formatMemberRequestCancelTitle(Project project, User user) { return Messages.get("notification.member.request.cancel.title", project.name, user.loginId); } private static String formatMemberRequestCancelTitle(Organization organization, User user) { return Messages.get("notification.member.request.cancel.title", organization.name, user.loginId); } private static String formatMemberRequestTitle(Organization organization, User user) { return Messages.get("notification.organization.member.request.title", organization.name, user.loginId); } private static String formatMemberAcceptTitle(Project project, User user) { return Messages.get("notification.member.request.accept.title", project.name, user.loginId); } private static String formatMemberAcceptTitle(Organization organization, User user) { return Messages.get("notification.member.request.accept.title", organization.name, user.loginId); } /** * Get mentioned users in {@code body}. * * @param body * @return */ public static Set<User> getMentionedUsers(String body) { Matcher matcher = Pattern.compile("@" + User.LOGIN_ID_PATTERN_ALLOW_FORWARD_SLASH).matcher(body); Set<User> users = new HashSet<>(); while(matcher.find()) { String mentionWord = matcher.group().substring(1); users.addAll(findOrganizationMembers(mentionWord)); users.addAll(findProjectMembers(mentionWord)); users.add(User.findByLoginId(mentionWord)); } users.remove(User.anonymous); return users; } /** * Get new mentioned users. * * It gets mentioned users from {@code oldBody} and {@code newBody}, * subtracts old from new and returns it. * * @param oldBody * @param newBody * @return */ public static Set<User> getNewMentionedUsers(String oldBody, String newBody) { Set<User> oldBodyMentionedUsers = getMentionedUsers(oldBody); Set<User> newBodyMentionedUsers = getMentionedUsers(newBody); newBodyMentionedUsers.removeAll(oldBodyMentionedUsers); return newBodyMentionedUsers; } private static Set<User> findOrganizationMembers(String mentionWord) { Set<User> users = new HashSet<>(); Organization org = Organization.findByName(mentionWord); if (org != null) { for (OrganizationUser orgUser : org.users) { users.add(orgUser.user); } } return users; } private static Set<User> findProjectMembers(String mentionWord) { Set<User> users = new HashSet<>(); if(mentionWord.contains("/")){ String projectName = mentionWord.substring(mentionWord.lastIndexOf("/")+1); String loginId = mentionWord.substring(0, mentionWord.lastIndexOf("/")); Project mentionedProject = Project.findByOwnerAndProjectName(loginId, projectName); if(mentionedProject == null) { return users; } for(ProjectUser projectUser: mentionedProject.members() ){ users.add(projectUser.user); } } return users; } public static void scheduleDeleteOldNotifications() { if (EventConstants.KEEP_TIME_IN_DAYS > 0) { Akka.system().scheduler().schedule( Duration.create(1, TimeUnit.MINUTES), Duration.create(1, TimeUnit.DAYS), new Runnable() { @Override public void run() { Date threshold = DateTime.now() .minusDays(EventConstants.KEEP_TIME_IN_DAYS).toDate(); List<NotificationEvent> olds = find.where().lt("created", threshold).findList(); for (NotificationEvent old : olds) { old.delete(); } } }, Akka.system().dispatcher() ); } } public static void onStart() { scheduleDeleteOldNotifications(); } /** * Finds NotificationEvents that are supposed to be shown to the {@code user}. * Paginates it with {@code from} and {@code size}. * * @param user * @param from * @param size * @return */ public static List<NotificationEvent> findByReceiver(User user, int from, int size) { String sql = "select t1.id, t1.title, t1.sender_id, t1.created, t1.resource_type, t1.resource_id, t1.event_type, " + "t1.old_value, t1.new_value " + "from n4user t0 " + "left outer join notification_event_n4user t1z_ on t1z_.n4user_id = t0.id " + "left outer join notification_event t1 on t1.id = t1z_.notification_event_id " + "left outer join notification_mail t2 on t2.notification_event_id = t1.id " + "where t0.id = " + user.id + " and t1.id IS NOT NULL " + "order by t1.created DESC"; return find.setRawSql(RawSqlBuilder.parse(sql).create()) .setFirstRow(from) .setMaxRows(size) .findList(); } public static int getNotificationsCount(User user) { String sql = "select t1.id " + "from n4user t0 " + "left outer join notification_event_n4user t1z_ on t1z_.n4user_id = t0.id " + "left outer join notification_event t1 on t1.id = t1z_.notification_event_id " + "left outer join notification_mail t2 on t2.notification_event_id = t1.id " + "where t0.id = " + user.id + " and t1.id IS NOT NULL "; return find.setRawSql(RawSqlBuilder.parse(sql).create()).findList().size(); } public static void afterCommentUpdated(Comment comment) { NotificationEvent.add(forUpdatedComment(comment, UserApp.currentUser())); } }