Изменение подхода к уведомлениям

This commit is contained in:
Struchkov Mark 2024-08-26 17:56:18 +03:00
parent fb8a7e8a53
commit 57c787bf01
No known key found for this signature in database
GPG Key ID: A3F0AC3F0FA52F3C
19 changed files with 829 additions and 532 deletions

View File

@ -10,8 +10,12 @@ import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@ -25,7 +29,10 @@ import static dev.struchkov.haiti.utils.Checker.checkNotEmpty;
@Getter
@Setter
@Entity
@NoArgsConstructor
@Builder(toBuilder = true)
@Table(name = "discussion")
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Discussion {

View File

@ -17,8 +17,12 @@ import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@ -35,9 +39,12 @@ import java.util.Set;
@Getter
@Setter
@Entity
@FieldNames(mode = {Mode.TABLE, Mode.SIMPLE})
@NoArgsConstructor
@Builder(toBuilder = true)
@Table(name = "merge_request")
@FieldNames(mode = {Mode.TABLE, Mode.SIMPLE})
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MergeRequest {
@Id
@ -95,17 +102,6 @@ public class MergeRequest {
)
private List<Person> reviewers = new ArrayList<>();
@OneToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE}
)
@JoinTable(
name = "merge_request_approvals",
joinColumns = @JoinColumn(name = "merge_request_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "person_id", referencedColumnName = "id")
)
private List<Person> approvals = new ArrayList<>();
@Column(name = "target_branch")
private String targetBranch;

View File

@ -9,6 +9,7 @@ import jakarta.persistence.Table;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* @author upagge 14.01.2021
@ -16,9 +17,10 @@ import lombok.Setter;
@Entity
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@Table(name = "person")
@FieldNames(mode = {Mode.TABLE, Mode.SIMPLE})
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Person {
@Id

View File

@ -11,8 +11,12 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@ -25,8 +29,11 @@ import java.time.LocalDateTime;
@Getter
@Setter
@FieldNames
@NoArgsConstructor
@Table(name = "pipeline")
@Builder(toBuilder = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Pipeline {
@Id

View File

@ -0,0 +1,19 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.entity.Discussion;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "newDiscussion")
public class NewDiscussionEvent {
private Discussion discussion;
}

View File

@ -0,0 +1,19 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "newMergeRequest")
public class NewMergeRequestEvent {
private MergeRequest mergeRequest;
}

View File

@ -0,0 +1,19 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.entity.Pipeline;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "newPipeline")
public class NewPipelineEvent {
private Pipeline pipeline;
}

View File

@ -0,0 +1,19 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.entity.Project;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "newProject")
public class NewProjectEvent {
private Project project;
}

View File

@ -0,0 +1,20 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.entity.Discussion;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "updateDiscussion")
public class UpdateDiscussionEvent {
private Discussion oldDiscussion;
private Discussion newDiscussion;
}

View File

@ -0,0 +1,25 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.AssigneeChanged;
import dev.struchkov.bot.gitlab.context.domain.ReviewerChanged;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "updateMergeRequest")
public class UpdateMergeRequestEvent {
private MergeRequest oldMergeRequest;
private MergeRequest newMergeRequest;
private AssigneeChanged assigneeChanged;
private ReviewerChanged reviewerChanged;
}

View File

@ -0,0 +1,20 @@
package dev.struchkov.bot.gitlab.context.domain.event;
import dev.struchkov.bot.gitlab.context.domain.entity.Pipeline;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(staticName = "updatePipeline")
public class UpdatePipelineEvent {
private Pipeline oldPipeline;
private Pipeline newPipeline;
}

View File

@ -0,0 +1,265 @@
package dev.struchkov.bot.gitlab.core.handler;
import dev.struchkov.bot.gitlab.context.domain.PersonInformation;
import dev.struchkov.bot.gitlab.context.domain.entity.Discussion;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequestForDiscussion;
import dev.struchkov.bot.gitlab.context.domain.entity.Note;
import dev.struchkov.bot.gitlab.context.domain.event.NewDiscussionEvent;
import dev.struchkov.bot.gitlab.context.domain.event.UpdateDiscussionEvent;
import dev.struchkov.bot.gitlab.context.domain.notify.comment.NewCommentNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.level.DiscussionLevel;
import dev.struchkov.bot.gitlab.context.domain.notify.task.DiscussionNewNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.task.ThreadCloseNotify;
import dev.struchkov.bot.gitlab.context.service.AppSettingService;
import dev.struchkov.bot.gitlab.context.service.DiscussionService;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.haiti.utils.container.Pair;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.domain.notify.level.DiscussionLevel.NOTIFY_WITH_CONTEXT;
import static dev.struchkov.bot.gitlab.context.domain.notify.level.DiscussionLevel.WITHOUT_NOTIFY;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
import static dev.struchkov.haiti.utils.Checker.checkNull;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
@Component
@RequiredArgsConstructor
public class DiscussionHandler {
protected static final Pattern PATTERN = Pattern.compile("@[\\w]+");
private final PersonInformation personInformation;
private final AppSettingService settingService;
private final DiscussionService discussionService;
private final NotifyService notifyService;
@EventListener
public void newDiscussionEvent(NewDiscussionEvent event) {
final Discussion discussion = event.getDiscussion();
final List<Note> notes = discussion.getNotes();
if (isNeedNotifyNewNote(discussion)) {
notifyNewThread(discussion);
} else {
notes.forEach(note -> notifyAboutPersonalAnswer(discussion, note));
}
}
@EventListener
public void updateDiscussionHandle(UpdateDiscussionEvent event) {
final Discussion oldDiscussion = event.getOldDiscussion();
final Discussion newDiscussion = event.getNewDiscussion();
if (oldDiscussion.isNotification()) {
final Map<Long, Note> oldNoteMap = oldDiscussion
.getNotes().stream()
.collect(Collectors.toMap(Note::getId, n -> n));
// Пользователь участвовал в обсуждении
final boolean userParticipatedInDiscussion = oldDiscussion.getNotes().stream()
.anyMatch(note -> personInformation.getId().equals(note.getAuthor().getId()));
final Note threadFirstNote = newDiscussion.getFirstNote();
if (TRUE.equals(newDiscussion.getResolved())) {
notifyAboutCloseThread(threadFirstNote, oldNoteMap.get(threadFirstNote.getId()), newDiscussion.getLastNote());
}
for (Note newNote : newDiscussion.getNotes()) {
final Long newNoteId = newNote.getId();
if (!oldNoteMap.containsKey(newNoteId)) {
if (userParticipatedInDiscussion) {
notifyAboutNewAnswer(newDiscussion, newNote);
} else {
notifyAboutPersonalAnswer(newDiscussion, newNote);
}
}
}
}
}
/**
* <p>Уведомляет пользователя, если появился новый комментарий</p>
*/
private void notifyNewThread(Discussion discussion) {
final DiscussionLevel discussionLevel = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(discussionLevel)) {
final Note firstNote = discussion.getFirstNote();
final MergeRequestForDiscussion mergeRequest = discussion.getMergeRequest();
final DiscussionNewNotify.DiscussionNewNotifyBuilder messageBuilder = DiscussionNewNotify.builder()
.url(firstNote.getWebUrl())
.threadId(discussion.getId())
.mergeRequestName(mergeRequest.getTitle())
.authorName(firstNote.getAuthor().getName());
if (NOTIFY_WITH_CONTEXT.equals(discussionLevel)) {
final List<Note> notes = discussion.getNotes();
messageBuilder
.discussionMessage(firstNote.getBody());
if (notes.size() > 1) {
for (int i = 1; i < notes.size(); i++) {
final Note note = notes.get(i);
messageBuilder.note(
new Pair<>(note.getAuthor().getName(), note.getBody())
);
}
}
}
notifyService.send(messageBuilder.build());
}
}
private boolean isNeedNotifyNewNote(Discussion discussion) {
final Note firstNote = discussion.getFirstNote();
final Long gitlabUserId = personInformation.getId();
return firstNote.isResolvable() // Тип комментария требует решения (Задачи)
&& gitlabUserId.equals(discussion.getResponsible().getId()) // Ответственный за дискуссию пользователь
&& !gitlabUserId.equals(firstNote.getAuthor().getId()) // Создатель комментария не пользователь системы
&& FALSE.equals(firstNote.getResolved()); // Комментарий не отмечен как решенный
}
/**
* Уведомляет пользователя, если его никнейм упоминается в комментарии.
*/
private void notifyAboutPersonalAnswer(Discussion discussion, Note note) {
final DiscussionLevel discussionLevel = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(discussionLevel)) {
final Matcher matcher = PATTERN.matcher(note.getBody());
final Set<String> recipientsLogins = new HashSet<>();
while (matcher.find()) {
final String login = matcher.group(0).replace("@", "");
recipientsLogins.add(login);
}
if (recipientsLogins.contains(personInformation.getUsername())) {
final NewCommentNotify.NewCommentNotifyBuilder notifyBuilder = NewCommentNotify.builder()
.threadId(discussion.getId())
.mergeRequestName(discussion.getMergeRequest().getTitle())
.url(note.getWebUrl());
if (NOTIFY_WITH_CONTEXT.equals(discussionLevel)) {
final Optional<Note> prevLastNote = discussion.getPrevLastNote();
final Note firstNote = discussion.getFirstNote();
if (!firstNote.equals(note)) {
notifyBuilder.message(note.getBody())
.authorName(note.getAuthor().getName());
}
if (prevLastNote.isPresent()) {
final Note prevNote = prevLastNote.get();
notifyBuilder.previousMessage(prevNote.getBody());
notifyBuilder.previousAuthor(prevNote.getAuthor().getName());
}
notifyBuilder
.discussionMessage(firstNote.getBody())
.discussionAuthor(firstNote.getAuthor().getName());
}
notifyService.send(notifyBuilder.build());
}
}
}
private void notifyAboutNewAnswer(Discussion discussion, Note note) {
final DiscussionLevel discussionLevel = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(discussionLevel)
&& !personInformation.getId().equals(note.getAuthor().getId())) {
final Note firstNote = discussion.getFirstNote();
final NewCommentNotify.NewCommentNotifyBuilder notifyBuilder = NewCommentNotify.builder()
.threadId(discussion.getId())
.url(note.getWebUrl())
.mergeRequestName(discussion.getMergeRequest().getTitle());
if (NOTIFY_WITH_CONTEXT.equals(discussionLevel)) {
final Optional<Note> prevLastNote = discussion.getPrevLastNote();
if (prevLastNote.isPresent()) {
final Note prevNote = prevLastNote.get();
notifyBuilder.previousMessage(prevNote.getBody());
notifyBuilder.previousAuthor(prevNote.getAuthor().getName());
}
notifyBuilder
.discussionMessage(firstNote.getBody())
.discussionAuthor(firstNote.getAuthor().getName())
.message(note.getBody())
.authorName(note.getAuthor().getName());
}
notifyService.send(notifyBuilder.build());
}
}
private void notifyAboutCloseThread(Note newNote, Note oldNote, Optional<Note> lastNote) {
final DiscussionLevel level = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(level)) {
if (isResolved(newNote, oldNote)) {
final MergeRequestForDiscussion mergeRequest = oldNote.getDiscussion().getMergeRequest();
final List<Discussion> discussions = discussionService.getAllByMergeRequestId(mergeRequest.getId())
.stream()
.filter(discussion -> Objects.nonNull(discussion.getResponsible()))
.toList();
final long allYouTasks = discussions.stream()
.filter(discussion -> personInformation.getId().equals(discussion.getFirstNote().getAuthor().getId()))
.count();
final long resolvedYouTask = discussions.stream()
.filter(discussion -> personInformation.getId().equals(discussion.getFirstNote().getAuthor().getId()) && discussion.getResolved())
.count();
final ThreadCloseNotify.ThreadCloseNotifyBuilder notifyBuilder = ThreadCloseNotify.builder()
.mergeRequestName(mergeRequest.getTitle())
.url(oldNote.getWebUrl())
.personTasks(allYouTasks)
.personResolvedTasks(resolvedYouTask);
if (NOTIFY_WITH_CONTEXT.equals(level)) {
notifyBuilder
.authorName(oldNote.getAuthor().getName())
.messageTask(oldNote.getBody());
lastNote.ifPresent(
note -> {
notifyBuilder.authorLastNote(note.getAuthor().getName());
notifyBuilder.messageLastNote(note.getBody());
}
);
}
notifyService.send(notifyBuilder.build());
}
}
}
private boolean isResolved(Note note, Note oldNote) {
return checkNull(oldNote.getResolvedBy()) // В старом комментарии не было отметки о решении
&& checkNotNull(note.getResolvedBy()) // А в новом есть отметка
&& personInformation.getId().equals(oldNote.getAuthor().getId()) // и решающий не является пользователем бота
&& !note.getResolvedBy().getId().equals(oldNote.getAuthor().getId()); // и решающий не является автором треда
}
}

View File

@ -0,0 +1,273 @@
package dev.struchkov.bot.gitlab.core.handler;
import dev.struchkov.bot.gitlab.context.domain.AssigneeChanged;
import dev.struchkov.bot.gitlab.context.domain.MergeRequestState;
import dev.struchkov.bot.gitlab.context.domain.PersonInformation;
import dev.struchkov.bot.gitlab.context.domain.ReviewerChanged;
import dev.struchkov.bot.gitlab.context.domain.entity.Discussion;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequest;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.context.domain.entity.Project;
import dev.struchkov.bot.gitlab.context.domain.event.NewMergeRequestEvent;
import dev.struchkov.bot.gitlab.context.domain.event.UpdateMergeRequestEvent;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.ConflictMrNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.ConflictResolveMrNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.NewMrForAssignee;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.NewMrForReview;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.StatusMrNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.UpdateMrNotify;
import dev.struchkov.bot.gitlab.context.service.DiscussionService;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.bot.gitlab.context.service.ProjectService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
import static java.lang.Boolean.TRUE;
@Component
@RequiredArgsConstructor
public class MergeRequestHandler {
private final NotifyService notifyService;
private final PersonInformation personInformation;
private final ProjectService projectService;
private final DiscussionService discussionService;
@EventListener
public void newMrHandle(NewMergeRequestEvent event) {
final MergeRequest mergeRequest = event.getMergeRequest();
final String projectName = projectService.getByIdOrThrow(mergeRequest.getProjectId()).getName();
final boolean userReviewer = mergeRequest.isUserReviewer();
final boolean userAssignee = mergeRequest.isUserAssignee();
if (userReviewer || userAssignee) {
if (!mergeRequest.isConflict()) {
if (userReviewer) sendNotifyNewMrReview(mergeRequest, projectName);
if (userAssignee) sendNotifyNewAssignee(mergeRequest, projectName, null);
}
}
}
@EventListener
public void updateMrHandle(UpdateMergeRequestEvent event) {
final MergeRequest oldMergeRequest = event.getOldMergeRequest();
final MergeRequest newMergeRequest = event.getNewMergeRequest();
final AssigneeChanged assigneeChanged = event.getAssigneeChanged();
final ReviewerChanged reviewerChanged = event.getReviewerChanged();
final boolean isChangedMr = !oldMergeRequest.getUpdatedDate().equals(newMergeRequest.getUpdatedDate()) || oldMergeRequest.isConflict() != newMergeRequest.isConflict();
final boolean isChangedLinkedEntity = reviewerChanged.isChanged() || assigneeChanged.isChanged();
final boolean isMilestone = !Objects.equals(oldMergeRequest.getMilestone(), newMergeRequest.getMilestone());
if (isChangedMr || isChangedLinkedEntity || isMilestone) {
if (oldMergeRequest.isNotification()) {
final Project project = projectService.getByIdOrThrow(newMergeRequest.getProjectId());
if (isChangedMr) {
notifyAboutStatus(oldMergeRequest, newMergeRequest, project);
notifyAboutNewConflict(oldMergeRequest, newMergeRequest, project);
notifyAboutResolveConflict(oldMergeRequest, newMergeRequest, project);
notifyAboutUpdate(oldMergeRequest, newMergeRequest, project);
}
if (isChangedLinkedEntity) {
notifyReviewer(reviewerChanged, newMergeRequest, project);
notifyAssignee(assigneeChanged, oldMergeRequest, newMergeRequest, project);
}
}
}
}
private void sendNotifyNewMrReview(MergeRequest mergeRequest, String projectName) {
final NewMrForReview.NewMrForReviewBuilder builder = NewMrForReview.builder()
.mrId(mergeRequest.getId())
.projectName(projectName)
.labels(mergeRequest.getLabels())
.author(mergeRequest.getAuthor().getName())
.milestone(mergeRequest.getMilestone())
.description(mergeRequest.getDescription())
.title(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.targetBranch(mergeRequest.getTargetBranch())
.sourceBranch(mergeRequest.getSourceBranch());
getAssignee(mergeRequest)
.map(Person::getName)
.ifPresent(builder::assignee);
notifyService.send(builder.build());
}
private void sendNotifyNewAssignee(MergeRequest mergeRequest, String projectName, String oldAssigneeName) {
final NewMrForAssignee.NewMrForAssigneeBuilder builder = NewMrForAssignee.builder()
.mrId(mergeRequest.getId())
.projectName(projectName)
.labels(mergeRequest.getLabels())
.author(mergeRequest.getAuthor().getName())
.description(mergeRequest.getDescription())
.milestone(mergeRequest.getMilestone())
.title(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.targetBranch(mergeRequest.getTargetBranch())
.sourceBranch(mergeRequest.getSourceBranch())
.milestone(mergeRequest.getMilestone())
.reviewers(mergeRequest.getReviewers().stream().map(Person::getName).toList());
if (checkNotNull(oldAssigneeName)) {
builder.oldAssigneeName(oldAssigneeName);
getAssignee(mergeRequest)
.map(Person::getName)
.ifPresent(builder::newAssigneeName);
}
notifyService.send(builder.build());
}
private void notifyAboutUpdate(MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
final Long botUserGitlabId = personInformation.getId();
if (
!botUserGitlabId.equals(mergeRequest.getAuthor().getId()) // Автор MR не пользователь приложения
&& !oldMergeRequest.getDateLastCommit().equals(mergeRequest.getDateLastCommit()) // Изменилась дата последнего коммита
&& !mergeRequest.isConflict() // MR не находится в состоянии конфликта
&& !botUserGitlabId.equals(oldMergeRequest.getAuthor().getId()) // и MR создан НЕ пользователем бота
) {
long allTask = 0;
long resolvedTask = 0;
long allYouTasks = 0;
long resolvedYouTask = 0;
final List<Discussion> discussions = discussionService.getAllByMergeRequestId(oldMergeRequest.getId());
for (Discussion discussion : discussions) {
if (checkNotNull(discussion.getResponsible())) {
final boolean isBotUserAuthorDiscussion = botUserGitlabId.equals(discussion.getFirstNote().getAuthor().getId());
allTask += 1;
if (isBotUserAuthorDiscussion) {
allYouTasks += 1;
}
if (TRUE.equals(discussion.getResolved())) {
resolvedTask += 1;
if (isBotUserAuthorDiscussion) {
resolvedYouTask += 1;
}
}
}
}
final UpdateMrNotify.UpdateMrNotifyBuilder notifyBuilder = UpdateMrNotify.builder()
.mrId(oldMergeRequest.getId())
.author(oldMergeRequest.getAuthor().getName())
.name(oldMergeRequest.getTitle())
.projectName(project.getName())
.url(oldMergeRequest.getWebUrl())
.allTasks(allTask)
.milestone(mergeRequest.getMilestone())
.allResolvedTasks(resolvedTask)
.personTasks(allYouTasks)
.personResolvedTasks(resolvedYouTask);
if (oldMergeRequest.isConflict() && !mergeRequest.isConflict()) {
notifyBuilder.comment("The conflict has been resolved");
}
notifyService.send(notifyBuilder.build());
}
}
protected void notifyAboutNewConflict(MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
final Long gitlabUserId = personInformation.getId();
if (
!oldMergeRequest.isConflict() // У старого MR не было конфликта
&& mergeRequest.isConflict() // А у нового есть
&& gitlabUserId.equals(oldMergeRequest.getAuthor().getId()) // и MR создан пользователем бота
) {
notifyService.send(
ConflictMrNotify.builder()
.mrId(oldMergeRequest.getId())
.sourceBranch(oldMergeRequest.getSourceBranch())
.name(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.projectKey(project.getName())
.milestone(mergeRequest.getMilestone())
.build()
);
}
}
private void notifyAboutResolveConflict(MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
final Long gitlabUserId = personInformation.getId();
if (oldMergeRequest.isConflict() && !mergeRequest.isConflict()) {
// проверяем даты коммитов, так как при пуше в target ветку MR у которого есть конфликт, конфликт на время пропадает. Судя по всему GitLab после пуша заново проверяет вероятность конфликта. Чаще всего конфликт никуда не девается.
if (oldMergeRequest.getDateLastCommit().equals(mergeRequest.getDateLastCommit())) {
mergeRequest.setConflict(true);
} else {
if (gitlabUserId.equals(oldMergeRequest.getAuthor().getId())) {
notifyService.send(
ConflictResolveMrNotify.builder()
.mrId(oldMergeRequest.getId())
.sourceBranch(oldMergeRequest.getSourceBranch())
.name(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.projectKey(project.getName())
.milestone(mergeRequest.getMilestone())
.build()
);
}
}
}
}
protected void notifyAboutStatus(MergeRequest oldMergeRequest, MergeRequest newMergeRequest, Project project) {
final MergeRequestState oldStatus = oldMergeRequest.getState();
final MergeRequestState newStatus = newMergeRequest.getState();
final Long gitlabUserId = personInformation.getId();
if (
!oldStatus.equals(newStatus) // статус изменился
&& gitlabUserId.equals(oldMergeRequest.getAuthor().getId()) // создатель MR является пользователем бота
) {
notifyService.send(
StatusMrNotify.builder()
.mrId(oldMergeRequest.getId())
.name(newMergeRequest.getTitle())
.url(oldMergeRequest.getWebUrl())
.projectName(project.getName())
.newStatus(newStatus)
.oldStatus(oldStatus)
.milestone(newMergeRequest.getMilestone())
.build()
);
}
}
private Optional<Person> getAssignee(MergeRequest mergeRequest) {
return Optional.ofNullable(mergeRequest.getAssignee());
}
//TODO [05.12.2022|uPagge]: Добавить уведомление, если происходит удаление
private void notifyAssignee(AssigneeChanged assigneeChanged, MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
switch (assigneeChanged) {
case BECOME ->
sendNotifyNewAssignee(mergeRequest, project.getName(), getAssignee(oldMergeRequest).map(Person::getName).orElse(null));
}
}
//TODO [05.12.2022|uPagge]: Добавить уведомление, если происходит удаление ревьювера
private void notifyReviewer(ReviewerChanged reviewerChanged, MergeRequest mergeRequest, Project project) {
switch (reviewerChanged) {
case BECOME -> sendNotifyNewMrReview(mergeRequest, project.getName());
}
}
}

View File

@ -0,0 +1,59 @@
package dev.struchkov.bot.gitlab.core.handler;
import dev.struchkov.bot.gitlab.context.domain.PersonInformation;
import dev.struchkov.bot.gitlab.context.domain.PipelineStatus;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.context.domain.entity.Pipeline;
import dev.struchkov.bot.gitlab.context.domain.event.NewPipelineEvent;
import dev.struchkov.bot.gitlab.context.domain.notify.pipeline.PipelineNotify;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Set;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.CANCELED;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.FAILED;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.SKIPPED;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.SUCCESS;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
@Component
@RequiredArgsConstructor
public class PipelineHandler {
// Статусы пайплайнов, о которых нужно уведомить
private static final Set<PipelineStatus> notificationStatus = Set.of(FAILED, SUCCESS, CANCELED, SKIPPED);
private final PersonInformation personInformation;
private final NotifyService notifyService;
@EventListener
public void newPipelineHandle(NewPipelineEvent event) {
final Pipeline pipeline = event.getPipeline();
if (isNeedNotifyNewPipeline(pipeline)) {
notifyService.send(
PipelineNotify.builder()
.projectId(pipeline.getProjectId())
.newStatus(pipeline.getStatus())
.pipelineId(pipeline.getId())
.refName(pipeline.getRef())
.webUrl(pipeline.getWebUrl())
.oldStatus(PipelineStatus.NULL)
.build()
);
}
}
private boolean isNeedNotifyNewPipeline(@NonNull Pipeline pipeline) {
final Person personPipelineCreator = pipeline.getPerson();
return notificationStatus.contains(pipeline.getStatus()) // Пайплайн имеет статус необходимый для уведомления
&& checkNotNull(personPipelineCreator) // Создатель пайплайна не null
&& personInformation.getId().equals(personPipelineCreator.getId()) // Пользователь приложения является инициатором пайплайна
&& LocalDateTime.now().minusDays(1).isBefore(pipeline.getCreated()); // Пайплан был создан не более 24 часов назад
}
}

View File

@ -0,0 +1,36 @@
package dev.struchkov.bot.gitlab.core.handler;
import dev.struchkov.bot.gitlab.context.domain.entity.Project;
import dev.struchkov.bot.gitlab.context.domain.event.NewProjectEvent;
import dev.struchkov.bot.gitlab.context.domain.notify.project.NewProjectNotify;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.bot.gitlab.context.service.PersonService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class ProjectHandler {
private final NotifyService notifyService;
private final PersonService personService;
@EventListener
public void handleNewProjectEvent(NewProjectEvent event) {
final Project newProject = event.getProject();
final String authorName = personService.getByIdOrThrown(newProject.getCreatorId()).getName();
notifyService.send(
NewProjectNotify.builder()
.projectId(newProject.getId())
.projectDescription(newProject.getDescription())
.projectName(newProject.getName())
.projectUrl(newProject.getWebUrl())
.sshUrlToRepo(newProject.getSshUrlToRepo())
.httpUrlToRepo(newProject.getHttpUrlToRepo())
.authorName(authorName)
.build()
);
}
}

View File

@ -1,44 +1,31 @@
package dev.struchkov.bot.gitlab.core.service;
import dev.struchkov.bot.gitlab.context.domain.ExistContainer;
import dev.struchkov.bot.gitlab.context.domain.PersonInformation;
import dev.struchkov.bot.gitlab.context.domain.entity.Discussion;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequestForDiscussion;
import dev.struchkov.bot.gitlab.context.domain.entity.Note;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.context.domain.notify.comment.NewCommentNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.level.DiscussionLevel;
import dev.struchkov.bot.gitlab.context.domain.notify.task.DiscussionNewNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.task.ThreadCloseNotify;
import dev.struchkov.bot.gitlab.context.repository.DiscussionRepository;
import dev.struchkov.bot.gitlab.context.service.AppSettingService;
import dev.struchkov.bot.gitlab.context.service.DiscussionService;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.haiti.utils.container.Pair;
import dev.struchkov.sdk.gitlab.core.GitlabSdkManager;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.domain.notify.level.DiscussionLevel.NOTIFY_WITH_CONTEXT;
import static dev.struchkov.bot.gitlab.context.domain.event.NewDiscussionEvent.newDiscussion;
import static dev.struchkov.bot.gitlab.context.domain.event.UpdateDiscussionEvent.updateDiscussion;
import static dev.struchkov.bot.gitlab.context.domain.notify.level.DiscussionLevel.WITHOUT_NOTIFY;
import static dev.struchkov.haiti.context.exception.NotFoundException.notFoundException;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
import static dev.struchkov.haiti.utils.Checker.checkNull;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
/**
* Сервис для работы с дискуссиями.
@ -50,31 +37,21 @@ import static java.lang.Boolean.TRUE;
@RequiredArgsConstructor
public class DiscussionServiceImpl implements DiscussionService {
protected static final Pattern PATTERN = Pattern.compile("@[\\w]+");
private final DiscussionRepository repository;
private final NotifyService notifyService;
private final AppSettingService settingService;
private final PersonInformation personInformation;
private final GitlabSdkManager gitlabSdkManager;
private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
public Discussion create(@NonNull Discussion discussion) {
final List<Note> notes = discussion.getNotes();
final DiscussionLevel levelDiscussionNotify = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(levelDiscussionNotify)) {
discussion.setNotification(true);
if (isNeedNotifyNewNote(discussion)) {
notifyNewThread(discussion);
} else {
notes.forEach(note -> notifyAboutPersonalAnswer(discussion, note));
}
eventPublisher.publishEvent(newDiscussion(discussion));
} else {
discussion.setNotification(false);
}
@ -116,23 +93,11 @@ public class DiscussionServiceImpl implements DiscussionService {
.allMatch(note -> note.isResolvable() && note.getResolved());
discussion.setResolved(resolved);
if (oldDiscussion.isNotification()) {
notifyUpdateNote(oldDiscussion, discussion);
}
eventPublisher.publishEvent(updateDiscussion(oldDiscussion, discussion));
return repository.save(discussion);
}
private boolean isNeedNotifyNewNote(Discussion discussion) {
final Note firstNote = discussion.getFirstNote();
final Long gitlabUserId = personInformation.getId();
return firstNote.isResolvable() // Тип комментария требует решения (Задачи)
&& gitlabUserId.equals(discussion.getResponsible().getId()) // Ответственный за дискуссию пользователь
&& !gitlabUserId.equals(firstNote.getAuthor().getId()) // Создатель комментария не пользователь системы
&& FALSE.equals(firstNote.getResolved()); // Комментарий не отмечен как решенный
}
@Override
public List<Discussion> updateAll(@NonNull List<Discussion> discussions) {
return discussions.stream()
@ -140,85 +105,6 @@ public class DiscussionServiceImpl implements DiscussionService {
.collect(Collectors.toList());
}
private void notifyUpdateNote(Discussion oldDiscussion, Discussion discussion) {
final Map<Long, Note> oldNoteMap = oldDiscussion
.getNotes().stream()
.collect(Collectors.toMap(Note::getId, n -> n));
// Пользователь участвовал в обсуждении
final boolean userParticipatedInDiscussion = oldDiscussion.getNotes().stream()
.anyMatch(note -> personInformation.getId().equals(note.getAuthor().getId()));
final Note threadFirstNote = discussion.getFirstNote();
if (TRUE.equals(discussion.getResolved())) {
notifyAboutCloseThread(threadFirstNote, oldNoteMap.get(threadFirstNote.getId()), discussion.getLastNote());
}
for (Note newNote : discussion.getNotes()) {
final Long newNoteId = newNote.getId();
if (!oldNoteMap.containsKey(newNoteId)) {
if (userParticipatedInDiscussion) {
notifyAboutNewAnswer(discussion, newNote);
} else {
notifyAboutPersonalAnswer(discussion, newNote);
}
}
}
}
private void notifyAboutCloseThread(Note newNote, Note oldNote, Optional<Note> lastNote) {
final DiscussionLevel level = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(level)) {
if (isResolved(newNote, oldNote)) {
final MergeRequestForDiscussion mergeRequest = oldNote.getDiscussion().getMergeRequest();
final List<Discussion> discussions = getAllByMergeRequestId(mergeRequest.getId())
.stream()
.filter(discussion -> Objects.nonNull(discussion.getResponsible()))
.toList();
final long allYouTasks = discussions.stream()
.filter(discussion -> personInformation.getId().equals(discussion.getFirstNote().getAuthor().getId()))
.count();
final long resolvedYouTask = discussions.stream()
.filter(discussion -> personInformation.getId().equals(discussion.getFirstNote().getAuthor().getId()) && discussion.getResolved())
.count();
final ThreadCloseNotify.ThreadCloseNotifyBuilder notifyBuilder = ThreadCloseNotify.builder()
.mergeRequestName(mergeRequest.getTitle())
.url(oldNote.getWebUrl())
.personTasks(allYouTasks)
.personResolvedTasks(resolvedYouTask);
if (NOTIFY_WITH_CONTEXT.equals(level)) {
notifyBuilder
.authorName(oldNote.getAuthor().getName())
.messageTask(oldNote.getBody());
lastNote.ifPresent(
note -> {
notifyBuilder.authorLastNote(note.getAuthor().getName());
notifyBuilder.messageLastNote(note.getBody());
}
);
}
notifyService.send(notifyBuilder.build());
}
}
}
private boolean isResolved(Note note, Note oldNote) {
return checkNull(oldNote.getResolvedBy()) // В старом комментарии не было отметки о решении
&& checkNotNull(note.getResolvedBy()) // А в новом есть отметка
&& personInformation.getId().equals(oldNote.getAuthor().getId()) // и решающий не является пользователем бота
&& !note.getResolvedBy().getId().equals(oldNote.getAuthor().getId()); // и решающий не является автором треда
}
@Override
public void answer(@NonNull String discussionId, @NonNull String text) {
final Discussion discussion = repository.findById(discussionId)
@ -285,115 +171,4 @@ public class DiscussionServiceImpl implements DiscussionService {
repository.notification(enable, discussionId);
}
private void notifyAboutNewAnswer(Discussion discussion, Note note) {
final DiscussionLevel discussionLevel = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(discussionLevel)
&& !personInformation.getId().equals(note.getAuthor().getId())) {
final Note firstNote = discussion.getFirstNote();
final NewCommentNotify.NewCommentNotifyBuilder notifyBuilder = NewCommentNotify.builder()
.threadId(discussion.getId())
.url(note.getWebUrl())
.mergeRequestName(discussion.getMergeRequest().getTitle());
if (NOTIFY_WITH_CONTEXT.equals(discussionLevel)) {
final Optional<Note> prevLastNote = discussion.getPrevLastNote();
if (prevLastNote.isPresent()) {
final Note prevNote = prevLastNote.get();
notifyBuilder.previousMessage(prevNote.getBody());
notifyBuilder.previousAuthor(prevNote.getAuthor().getName());
}
notifyBuilder
.discussionMessage(firstNote.getBody())
.discussionAuthor(firstNote.getAuthor().getName())
.message(note.getBody())
.authorName(note.getAuthor().getName());
}
notifyService.send(notifyBuilder.build());
}
}
/**
* Уведомляет пользователя, если его никнейм упоминается в комментарии.
*/
private void notifyAboutPersonalAnswer(Discussion discussion, Note note) {
final DiscussionLevel discussionLevel = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(discussionLevel)) {
final Matcher matcher = PATTERN.matcher(note.getBody());
final Set<String> recipientsLogins = new HashSet<>();
while (matcher.find()) {
final String login = matcher.group(0).replace("@", "");
recipientsLogins.add(login);
}
if (recipientsLogins.contains(personInformation.getUsername())) {
final NewCommentNotify.NewCommentNotifyBuilder notifyBuilder = NewCommentNotify.builder()
.threadId(discussion.getId())
.mergeRequestName(discussion.getMergeRequest().getTitle())
.url(note.getWebUrl());
if (NOTIFY_WITH_CONTEXT.equals(discussionLevel)) {
final Optional<Note> prevLastNote = discussion.getPrevLastNote();
final Note firstNote = discussion.getFirstNote();
if (!firstNote.equals(note)) {
notifyBuilder.message(note.getBody())
.authorName(note.getAuthor().getName());
}
if (prevLastNote.isPresent()) {
final Note prevNote = prevLastNote.get();
notifyBuilder.previousMessage(prevNote.getBody());
notifyBuilder.previousAuthor(prevNote.getAuthor().getName());
}
notifyBuilder
.discussionMessage(firstNote.getBody())
.discussionAuthor(firstNote.getAuthor().getName());
}
notifyService.send(notifyBuilder.build());
}
}
}
/**
* <p>Уведомляет пользователя, если появился новый комментарий</p>
*/
private void notifyNewThread(Discussion discussion) {
final DiscussionLevel discussionLevel = settingService.getLevelDiscussionNotify();
if (!WITHOUT_NOTIFY.equals(discussionLevel)) {
final Note firstNote = discussion.getFirstNote();
final MergeRequestForDiscussion mergeRequest = discussion.getMergeRequest();
final DiscussionNewNotify.DiscussionNewNotifyBuilder messageBuilder = DiscussionNewNotify.builder()
.url(firstNote.getWebUrl())
.threadId(discussion.getId())
.mergeRequestName(mergeRequest.getTitle())
.authorName(firstNote.getAuthor().getName());
if (NOTIFY_WITH_CONTEXT.equals(discussionLevel)) {
final List<Note> notes = discussion.getNotes();
messageBuilder
.discussionMessage(firstNote.getBody());
if (notes.size() > 1) {
for (int i = 1; i < notes.size(); i++) {
final Note note = notes.get(i);
messageBuilder.note(
new Pair<>(note.getAuthor().getName(), note.getBody())
);
}
}
}
notifyService.send(messageBuilder.build());
}
}
}

View File

@ -6,40 +6,29 @@ import dev.struchkov.bot.gitlab.context.domain.IdAndStatusPr;
import dev.struchkov.bot.gitlab.context.domain.MergeRequestState;
import dev.struchkov.bot.gitlab.context.domain.PersonInformation;
import dev.struchkov.bot.gitlab.context.domain.ReviewerChanged;
import dev.struchkov.bot.gitlab.context.domain.entity.Discussion;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequest;
import dev.struchkov.bot.gitlab.context.domain.entity.MergeRequestForDiscussion;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.context.domain.entity.Project;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.ConflictMrNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.ConflictResolveMrNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.NewMrForAssignee;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.NewMrForReview;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.StatusMrNotify;
import dev.struchkov.bot.gitlab.context.domain.notify.mergerequest.UpdateMrNotify;
import dev.struchkov.bot.gitlab.context.repository.MergeRequestRepository;
import dev.struchkov.bot.gitlab.context.service.DiscussionService;
import dev.struchkov.bot.gitlab.context.service.MergeRequestsService;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.bot.gitlab.context.service.ProjectService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.domain.MergeRequestState.CLOSED;
import static dev.struchkov.bot.gitlab.context.domain.MergeRequestState.MERGED;
import static dev.struchkov.bot.gitlab.context.domain.event.NewMergeRequestEvent.newMergeRequest;
import static dev.struchkov.bot.gitlab.context.domain.event.UpdateMergeRequestEvent.updateMergeRequest;
import static dev.struchkov.haiti.context.exception.NotFoundException.notFoundException;
import static dev.struchkov.haiti.utils.Checker.checkNotEmpty;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
import static java.lang.Boolean.TRUE;
@Slf4j
@Service
@ -48,11 +37,9 @@ public class MergeRequestsServiceImpl implements MergeRequestsService {
public static final Set<MergeRequestState> DELETE_STATES = Set.of(MERGED, CLOSED);
private final MergeRequestRepository repository;
private final ApplicationEventPublisher eventPublisher;
private final NotifyService notifyService;
private final ProjectService projectService;
private final DiscussionService discussionService;
private final MergeRequestRepository repository;
private final PersonInformation personInformation;
@ -69,13 +56,7 @@ public class MergeRequestsServiceImpl implements MergeRequestsService {
final MergeRequest savedMergeRequest = repository.save(mergeRequest);
if (botUserReviewer || botUserAssignee) {
if (!mergeRequest.isConflict()) {
final String projectName = projectService.getByIdOrThrow(savedMergeRequest.getProjectId()).getName();
if (botUserReviewer) sendNotifyNewMrReview(savedMergeRequest, projectName);
if (botUserAssignee) sendNotifyNewAssignee(mergeRequest, projectName, null);
}
}
eventPublisher.publishEvent(newMergeRequest(savedMergeRequest));
return savedMergeRequest;
}
@ -117,62 +98,14 @@ public class MergeRequestsServiceImpl implements MergeRequestsService {
return false;
}
private void sendNotifyNewMrReview(MergeRequest mergeRequest, String projectName) {
final NewMrForReview.NewMrForReviewBuilder builder = NewMrForReview.builder()
.mrId(mergeRequest.getId())
.projectName(projectName)
.labels(mergeRequest.getLabels())
.author(mergeRequest.getAuthor().getName())
.milestone(mergeRequest.getMilestone())
.description(mergeRequest.getDescription())
.title(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.targetBranch(mergeRequest.getTargetBranch())
.sourceBranch(mergeRequest.getSourceBranch());
getAssignee(mergeRequest)
.map(Person::getName)
.ifPresent(builder::assignee);
notifyService.send(builder.build());
}
private Optional<Person> getAssignee(MergeRequest mergeRequest) {
return Optional.ofNullable(mergeRequest.getAssignee());
}
private void sendNotifyNewAssignee(MergeRequest mergeRequest, String projectName, String oldAssigneeName) {
final NewMrForAssignee.NewMrForAssigneeBuilder builder = NewMrForAssignee.builder()
.mrId(mergeRequest.getId())
.projectName(projectName)
.labels(mergeRequest.getLabels())
.author(mergeRequest.getAuthor().getName())
.description(mergeRequest.getDescription())
.milestone(mergeRequest.getMilestone())
.title(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.targetBranch(mergeRequest.getTargetBranch())
.sourceBranch(mergeRequest.getSourceBranch())
.milestone(mergeRequest.getMilestone())
.reviewers(mergeRequest.getReviewers().stream().map(Person::getName).toList());
if (checkNotNull(oldAssigneeName)) {
builder.oldAssigneeName(oldAssigneeName);
getAssignee(mergeRequest)
.map(Person::getName)
.ifPresent(builder::newAssigneeName);
}
notifyService.send(builder.build());
}
@Override
@Transactional
public MergeRequest update(@NonNull MergeRequest mergeRequest) {
final MergeRequest oldMergeRequest = repository.findById(mergeRequest.getId())
.orElseThrow(notFoundException("MergeRequest не найден"));
final MergeRequest cloneOldMergeRequest = oldMergeRequest.toBuilder().build();
mergeRequest.setNotification(oldMergeRequest.isNotification());
final Long gitlabUserId = personInformation.getId();
@ -182,49 +115,13 @@ public class MergeRequestsServiceImpl implements MergeRequestsService {
mergeRequest.setUserAssignee(assigneeChanged.getNewStatus(oldMergeRequest.isUserAssignee()));
mergeRequest.setUserReviewer(reviewerChanged.getNewStatus(oldMergeRequest.isUserReviewer()));
final boolean isChangedMr = !oldMergeRequest.getUpdatedDate().equals(mergeRequest.getUpdatedDate()) || oldMergeRequest.isConflict() != mergeRequest.isConflict();
final boolean isChangedLinkedEntity = reviewerChanged.isChanged() || assigneeChanged.isChanged();
final boolean isMilestone = !Objects.equals(oldMergeRequest.getMilestone(), mergeRequest.getMilestone());
eventPublisher.publishEvent(
updateMergeRequest(cloneOldMergeRequest, mergeRequest, assigneeChanged, reviewerChanged)
);
if (isChangedMr || isChangedLinkedEntity || isMilestone) {
if (oldMergeRequest.isNotification()) {
final Project project = projectService.getByIdOrThrow(mergeRequest.getProjectId());
if (isChangedMr) {
notifyAboutStatus(oldMergeRequest, mergeRequest, project);
notifyAboutNewConflict(oldMergeRequest, mergeRequest, project);
notifyAboutResolveConflict(oldMergeRequest, mergeRequest, project);
notifyAboutUpdate(oldMergeRequest, mergeRequest, project);
}
if (isChangedLinkedEntity) {
notifyReviewer(reviewerChanged, mergeRequest, project);
notifyAssignee(assigneeChanged, oldMergeRequest, mergeRequest, project);
}
}
return repository.save(mergeRequest);
}
return oldMergeRequest;
return repository.save(mergeRequest);
}
//TODO [05.12.2022|uPagge]: Добавить уведомление, если происходит удаление
private void notifyAssignee(AssigneeChanged assigneeChanged, MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
switch (assigneeChanged) {
case BECOME ->
sendNotifyNewAssignee(mergeRequest, project.getName(), getAssignee(oldMergeRequest).map(Person::getName).orElse(null));
}
}
//TODO [05.12.2022|uPagge]: Добавить уведомление, если происходит удаление ревьювера
private void notifyReviewer(ReviewerChanged reviewerChanged, MergeRequest mergeRequest, Project project) {
switch (reviewerChanged) {
case BECOME -> sendNotifyNewMrReview(mergeRequest, project.getName());
}
}
@Override
@Transactional
@ -295,119 +192,8 @@ public class MergeRequestsServiceImpl implements MergeRequestsService {
repository.notificationByProjectId(enable, projectIds);
}
private void notifyAboutUpdate(MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
final Long botUserGitlabId = personInformation.getId();
if (
!botUserGitlabId.equals(mergeRequest.getAuthor().getId()) // Автор MR не пользователь приложения
&& !oldMergeRequest.getDateLastCommit().equals(mergeRequest.getDateLastCommit()) // Изменилась дата последнего коммита
&& !mergeRequest.isConflict() // MR не находится в состоянии конфликта
&& !botUserGitlabId.equals(oldMergeRequest.getAuthor().getId()) // и MR создан НЕ пользователем бота
) {
long allTask = 0;
long resolvedTask = 0;
long allYouTasks = 0;
long resolvedYouTask = 0;
final List<Discussion> discussions = discussionService.getAllByMergeRequestId(oldMergeRequest.getId());
for (Discussion discussion : discussions) {
if (checkNotNull(discussion.getResponsible())) {
final boolean isBotUserAuthorDiscussion = botUserGitlabId.equals(discussion.getFirstNote().getAuthor().getId());
allTask += 1;
if (isBotUserAuthorDiscussion) {
allYouTasks += 1;
}
if (TRUE.equals(discussion.getResolved())) {
resolvedTask += 1;
if (isBotUserAuthorDiscussion) {
resolvedYouTask += 1;
}
}
}
}
final UpdateMrNotify.UpdateMrNotifyBuilder notifyBuilder = UpdateMrNotify.builder()
.mrId(oldMergeRequest.getId())
.author(oldMergeRequest.getAuthor().getName())
.name(oldMergeRequest.getTitle())
.projectName(project.getName())
.url(oldMergeRequest.getWebUrl())
.allTasks(allTask)
.milestone(mergeRequest.getMilestone())
.allResolvedTasks(resolvedTask)
.personTasks(allYouTasks)
.personResolvedTasks(resolvedYouTask);
if (oldMergeRequest.isConflict() && !mergeRequest.isConflict()) {
notifyBuilder.comment("The conflict has been resolved");
}
notifyService.send(notifyBuilder.build());
}
}
protected void notifyAboutNewConflict(MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
final Long gitlabUserId = personInformation.getId();
if (
!oldMergeRequest.isConflict() // У старого MR не было конфликта
&& mergeRequest.isConflict() // А у нового есть
&& gitlabUserId.equals(oldMergeRequest.getAuthor().getId()) // и MR создан пользователем бота
) {
notifyService.send(
ConflictMrNotify.builder()
.mrId(oldMergeRequest.getId())
.sourceBranch(oldMergeRequest.getSourceBranch())
.name(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.projectKey(project.getName())
.milestone(mergeRequest.getMilestone())
.build()
);
}
}
private void notifyAboutResolveConflict(MergeRequest oldMergeRequest, MergeRequest mergeRequest, Project project) {
final Long gitlabUserId = personInformation.getId();
if (oldMergeRequest.isConflict() && !mergeRequest.isConflict()) {
// проверяем даты коммитов, так как при пуше в target ветку MR у которого есть конфликт, конфликт на время пропадает. Судя по всему GitLab после пуша заново проверяет вероятность конфликта. Чаще всего конфликт никуда не девается.
if (oldMergeRequest.getDateLastCommit().equals(mergeRequest.getDateLastCommit())) {
mergeRequest.setConflict(true);
} else {
if (gitlabUserId.equals(oldMergeRequest.getAuthor().getId())) {
notifyService.send(
ConflictResolveMrNotify.builder()
.mrId(oldMergeRequest.getId())
.sourceBranch(oldMergeRequest.getSourceBranch())
.name(mergeRequest.getTitle())
.url(mergeRequest.getWebUrl())
.projectKey(project.getName())
.milestone(mergeRequest.getMilestone())
.build()
);
}
}
}
}
protected void notifyAboutStatus(MergeRequest oldMergeRequest, MergeRequest newMergeRequest, Project project) {
final MergeRequestState oldStatus = oldMergeRequest.getState();
final MergeRequestState newStatus = newMergeRequest.getState();
final Long gitlabUserId = personInformation.getId();
if (
!oldStatus.equals(newStatus) // статус изменился
&& gitlabUserId.equals(oldMergeRequest.getAuthor().getId()) // создатель MR является пользователем бота
) {
notifyService.send(
StatusMrNotify.builder()
.mrId(oldMergeRequest.getId())
.name(newMergeRequest.getTitle())
.url(oldMergeRequest.getWebUrl())
.projectName(project.getName())
.newStatus(newStatus)
.oldStatus(oldStatus)
.milestone(newMergeRequest.getMilestone())
.build()
);
}
private Optional<Person> getAssignee(MergeRequest mergeRequest) {
return Optional.ofNullable(mergeRequest.getAssignee());
}
}

View File

@ -1,17 +1,14 @@
package dev.struchkov.bot.gitlab.core.service;
import dev.struchkov.bot.gitlab.context.domain.ExistContainer;
import dev.struchkov.bot.gitlab.context.domain.PersonInformation;
import dev.struchkov.bot.gitlab.context.domain.PipelineStatus;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.context.domain.entity.Pipeline;
import dev.struchkov.bot.gitlab.context.domain.notify.pipeline.PipelineNotify;
import dev.struchkov.bot.gitlab.context.repository.PipelineRepository;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.bot.gitlab.context.service.PipelineService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -20,12 +17,9 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.CANCELED;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.FAILED;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.SKIPPED;
import static dev.struchkov.bot.gitlab.context.domain.PipelineStatus.SUCCESS;
import static dev.struchkov.bot.gitlab.context.domain.event.NewPipelineEvent.newPipeline;
import static dev.struchkov.bot.gitlab.context.domain.event.UpdatePipelineEvent.updatePipeline;
import static dev.struchkov.haiti.context.exception.NotFoundException.notFoundException;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
/**
* Реализация сервиса для работы с пайплайнами.
@ -37,19 +31,15 @@ import static dev.struchkov.haiti.utils.Checker.checkNotNull;
@RequiredArgsConstructor
public class PipelineServiceImpl implements PipelineService {
// Статусы пайплайнов, о которых нужно уведомить
private static final Set<PipelineStatus> notificationStatus = Set.of(FAILED, SUCCESS, CANCELED, SKIPPED);
private final ApplicationEventPublisher eventPublisher;
private final NotifyService notifyService;
private final PipelineRepository repository;
private final PersonInformation personInformation;
@Override
@Transactional
public Pipeline create(@NonNull Pipeline pipeline) {
final Pipeline newPipeline = repository.save(pipeline);
notifyNewPipeline(pipeline, PipelineStatus.NULL);
eventPublisher.publishEvent(newPipeline(newPipeline));
return newPipeline;
}
@ -61,21 +51,6 @@ public class PipelineServiceImpl implements PipelineService {
.collect(Collectors.toList());
}
private void notifyNewPipeline(Pipeline pipeline, PipelineStatus oldStatus) {
if (isNeedNotifyNewPipeline(pipeline)) {
notifyService.send(
PipelineNotify.builder()
.projectId(pipeline.getProjectId())
.newStatus(pipeline.getStatus())
.pipelineId(pipeline.getId())
.refName(pipeline.getRef())
.webUrl(pipeline.getWebUrl())
.oldStatus(oldStatus)
.build()
);
}
}
@Override
public Pipeline update(@NonNull Pipeline pipeline) {
final Pipeline oldPipeline = repository.findById(pipeline.getId())
@ -84,7 +59,7 @@ public class PipelineServiceImpl implements PipelineService {
pipeline.setProjectId(oldPipeline.getProjectId());
if (!oldPipeline.getUpdated().equals(pipeline.getUpdated())) {
notifyNewPipeline(pipeline, oldPipeline.getStatus());
eventPublisher.publishEvent(updatePipeline(oldPipeline.toBuilder().build(), pipeline));
return repository.save(pipeline);
}
@ -98,14 +73,6 @@ public class PipelineServiceImpl implements PipelineService {
.collect(Collectors.toList());
}
private boolean isNeedNotifyNewPipeline(@NonNull Pipeline pipeline) {
final Person personPipelineCreator = pipeline.getPerson();
return notificationStatus.contains(pipeline.getStatus()) // Пайплайн имеет статус необходимый для уведомления
&& checkNotNull(personPipelineCreator) // Создатель пайплайна не null
&& personInformation.getId().equals(personPipelineCreator.getId()) // Пользователь приложения является инициатором пайплайна
&& LocalDateTime.now().minusDays(1).isBefore(pipeline.getCreated()); // Пайплан был создан не более 24 часов назад
}
@Override
public List<Pipeline> getAllByStatuses(@NonNull Set<PipelineStatus> statuses) {
return repository.findAllByStatuses(statuses);

View File

@ -2,13 +2,11 @@ package dev.struchkov.bot.gitlab.core.service;
import dev.struchkov.bot.gitlab.context.domain.ExistContainer;
import dev.struchkov.bot.gitlab.context.domain.entity.Project;
import dev.struchkov.bot.gitlab.context.domain.notify.project.NewProjectNotify;
import dev.struchkov.bot.gitlab.context.repository.ProjectRepository;
import dev.struchkov.bot.gitlab.context.service.NotifyService;
import dev.struchkov.bot.gitlab.context.service.PersonService;
import dev.struchkov.bot.gitlab.context.service.ProjectService;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -17,6 +15,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.domain.event.NewProjectEvent.newProject;
import static dev.struchkov.haiti.context.exception.NotFoundException.notFoundException;
/**
@ -28,8 +27,7 @@ public class ProjectServiceImpl implements ProjectService {
private final ProjectRepository repository;
private final NotifyService notifyService;
private final PersonService personService;
private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
@ -37,8 +35,7 @@ public class ProjectServiceImpl implements ProjectService {
final Project newProject = repository.save(project);
if (sendNotify) {
final String authorName = personService.getByIdOrThrown(newProject.getCreatorId()).getName();
notifyAboutNewProject(newProject, authorName);
eventPublisher.publishEvent(newProject(project));
}
return newProject;
@ -111,18 +108,4 @@ public class ProjectServiceImpl implements ProjectService {
repository.processing(enable, projectIds);
}
private void notifyAboutNewProject(Project newProject, String authorName) {
notifyService.send(
NewProjectNotify.builder()
.projectId(newProject.getId())
.projectDescription(newProject.getDescription())
.projectName(newProject.getName())
.projectUrl(newProject.getWebUrl())
.sshUrlToRepo(newProject.getSshUrlToRepo())
.httpUrlToRepo(newProject.getHttpUrlToRepo())
.authorName(authorName)
.build()
);
}
}