Feat: завершен основной функционал issue, выгрузка, удаление, уведомления

This commit is contained in:
Dmitry Sheyko 2023-02-03 20:15:46 +03:00
parent 30ff49eb75
commit 7e1c6c1f44
34 changed files with 1489 additions and 16 deletions

View File

@ -0,0 +1,50 @@
package dev.struchkov.bot.gitlab.context.domain;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author Dmitry Sheyko [25.01.2023]
*/
@Getter
@RequiredArgsConstructor
public enum AssigneesChanged {
BECOME(true),
DELETED(true),
NOT_AFFECT_USER(true),
NOT_CHANGED(false);
private final boolean changed;
public static AssigneesChanged valueOf(Long gitlabUserId, List<Person> oldAssignees, List<Person> newAssignees) {
final Map<Long, Person> oldMap = oldAssignees.stream().collect(Collectors.toMap(Person::getId, p -> p));
final Map<Long, Person> newMap = newAssignees.stream().collect(Collectors.toMap(Person::getId, p -> p));
if (!oldMap.keySet().equals(newMap.keySet())) {
if (oldMap.containsKey(gitlabUserId) && !newMap.containsKey(gitlabUserId)) {
return AssigneesChanged.DELETED;
}
if (!oldMap.containsKey(gitlabUserId) && newMap.containsKey(gitlabUserId)) {
return AssigneesChanged.BECOME;
}
return AssigneesChanged.NOT_AFFECT_USER;
}
return AssigneesChanged.NOT_CHANGED;
}
public boolean getNewStatus(boolean oldStatus) {
return switch (this) {
case BECOME -> true;
case DELETED -> false;
case NOT_AFFECT_USER, NOT_CHANGED -> oldStatus;
};
}
}

View File

@ -0,0 +1,20 @@
package dev.struchkov.bot.gitlab.context.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @author Dmotry Sheyko [25.01.2023]
*/
@Getter
@Setter
@AllArgsConstructor
public class IdAndStatusIssue {
private long id;
private long twoId;
private long projectId;
private IssueState status;
}

View File

@ -22,6 +22,7 @@ import javax.persistence.JoinTable;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@ -76,11 +77,10 @@ public class Issue {
private Person closedBy; private Person closedBy;
@ElementCollection @ElementCollection
@CollectionTable(name = "issue_labels", joinColumns = @JoinColumn(name = "label_id")) @CollectionTable(name = "issue_label", joinColumns = @JoinColumn(name = "issue_id"))
@Column(name = "labels") @Column(name = "label")
private Set<String> labels = new HashSet<>(); private Set<String> labels = new HashSet<>();
@Column(name = "assignees")
@OneToMany( @OneToMany(
fetch = FetchType.LAZY, fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE} cascade = {CascadeType.PERSIST, CascadeType.MERGE}
@ -113,7 +113,7 @@ public class Issue {
private Integer downVotes; private Integer downVotes;
@Column(name = "due_date") @Column(name = "due_date")
private LocalDateTime dueDate; private LocalDate dueDate;
@Column(name = "confidential") @Column(name = "confidential")
private Boolean confidential; private Boolean confidential;
@ -136,4 +136,10 @@ public class Issue {
@Column(name = "has_tasks") @Column(name = "has_tasks")
private Boolean hasTasks; private Boolean hasTasks;
@Column(name = "notification")
private boolean notification;
@Column(name = "is_assignee")
private boolean userAssignee;
} }

View File

@ -0,0 +1,33 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import lombok.Builder;
import lombok.Getter;
/**
* @author Dmitry Sheyko 25.01.2021
*/
@Getter
public class DeleteFromAssigneesNotify extends IssueNotify {
public static final String TYPE = "DeleteFromAssigneesOfIssueNotify";
private final String updateDate;
@Builder
public DeleteFromAssigneesNotify(
String projectName,
String title,
String url,
String issueType,
String updateDate
) {
super(projectName, title, url, issueType);
this.updateDate = updateDate;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,33 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import lombok.Builder;
import lombok.Getter;
/**
* @author Dmitry Sheyko 25.01.2021
*/
@Getter
public class DescriptionIssueNotify extends IssueNotify {
public static final String TYPE = "DescriptionIssueNotify";
private final String newDescription;
@Builder
public DescriptionIssueNotify(
String projectName,
String title,
String url,
String issueType,
String newDescription
) {
super(projectName, title, url, issueType);
this.newDescription = newDescription;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,36 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import lombok.Builder;
import lombok.Getter;
/**
* @author Dmitry Sheyko 25.01.2021
*/
@Getter
public class DueDateIssueNotify extends IssueNotify {
public static final String TYPE = "DueDateIssueNotify";
private final String oldDueDate;
private final String newDueDate;
@Builder
public DueDateIssueNotify(
String projectName,
String title,
String url,
String issueType,
String oldDueDate,
String newDueDate
) {
super(projectName, title, url, issueType);
this.oldDueDate = oldDueDate;
this.newDueDate = newDueDate;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,29 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import dev.struchkov.bot.gitlab.context.domain.notify.Notify;
import lombok.Getter;
/**
* @author Dmitry Sheyko 23.01.2021
*/
@Getter
public abstract class IssueNotify implements Notify {
protected final String projectName;
protected final String title;
protected final String url;
protected final String issueType;
public IssueNotify(
String projectName,
String title,
String url,
String issueType
) {
this.projectName = projectName;
this.title = title;
this.url = url;
this.issueType = issueType;
}
}

View File

@ -0,0 +1,47 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import lombok.Builder;
import lombok.Getter;
import java.util.Set;
/**
* @author Dmitry Sheyko 23.01.2021
*/
@Getter
public class NewIssueNotify extends IssueNotify {
public static final String TYPE = "NewIssueNotify";
private final String author;
private final String description;
private final String dueDate;
private final Set<String> labels;
private final String confidential;
@Builder
public NewIssueNotify(
String projectName,
String title,
String url,
String issueType,
String author,
String description,
String dueDate,
Set<String> labels,
String confidential
) {
super(projectName, title, url, issueType);
this.author = author;
this.description = description;
this.dueDate = dueDate;
this.labels = labels;
this.confidential = confidential;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,37 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import dev.struchkov.bot.gitlab.context.domain.IssueState;
import lombok.Builder;
import lombok.Getter;
/**
* @author Dmitry Sheyko 23.01.2021
*/
@Getter
public class StatusIssueNotify extends IssueNotify{
public static final String TYPE = "StatusIssueNotify";
private final IssueState oldStatus;
private final IssueState newStatus;
@Builder
private StatusIssueNotify(
String name,
String url,
String projectName,
String issueType,
IssueState oldStatus,
IssueState newStatus
) {
super(projectName, name, url, issueType);
this.oldStatus = oldStatus;
this.newStatus = newStatus;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,33 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import lombok.Builder;
import lombok.Getter;
/**
* @author Dmitry Sheyko 25.01.2021
*/
@Getter
public class TitleIssueNotify extends IssueNotify {
public static final String TYPE = "TitleIssueNotify";
private final String newTitle;
@Builder
public TitleIssueNotify(
String projectName,
String title,
String url,
String issueType,
String newTitle
) {
super(projectName, title, url, issueType);
this.newTitle = newTitle;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,37 @@
package dev.struchkov.bot.gitlab.context.domain.notify.issue;
import dev.struchkov.bot.gitlab.context.domain.IssueType;
import lombok.Builder;
import lombok.Getter;
/**
* @author Dmitry Sheyko 25.01.2021
*/
@Getter
public class TypeIssueNotify extends IssueNotify {
public static final String TYPE = "TypeIssueNotify";
private final IssueType oldType;
private final IssueType newType;
@Builder
public TypeIssueNotify(
String projectName,
String title,
String url,
String issueType,
IssueType oldType,
IssueType newType
) {
super(projectName, title, url, issueType);
this.oldType = oldType;
this.newType = newType;
}
@Override
public String getType() {
return TYPE;
}
}

View File

@ -0,0 +1,27 @@
package dev.struchkov.bot.gitlab.context.repository;
import dev.struchkov.bot.gitlab.context.domain.IdAndStatusIssue;
import dev.struchkov.bot.gitlab.context.domain.IssueState;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
import lombok.NonNull;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* @author Dmitry Sheyko [24.01.2023]
*/
public interface IssueRepository {
Set<IdAndStatusIssue> findAllIdByStateIn(@NonNull Set<IssueState> states);
Issue save(Issue issue);
Optional<Issue> findById(Long issueId);
List<Issue> findAllById(Set<Long> mergeRequestIds);
void deleteByStates(Set<IssueState> states);
}

View File

@ -0,0 +1,29 @@
package dev.struchkov.bot.gitlab.context.service;
import dev.struchkov.bot.gitlab.context.domain.*;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
import lombok.NonNull;
import java.util.List;
import java.util.Set;
/**
* @author Dmitry Sheyko [24.01.2023]
*/
public interface IssueService {
Issue create(@NonNull Issue issue);
Issue update(@NonNull Issue issue);
List<Issue> updateAll(@NonNull List<Issue> issues);
ExistContainer<Issue, Long> existsById(@NonNull Set<Long> issueIds);
List<Issue> createAll(List<Issue> issues);
Set<IdAndStatusIssue> getAllId(Set<IssueState> statuses);
void cleanOld();
}

View File

@ -19,6 +19,7 @@ public class Icons {
public static final String PEN = "✏️"; public static final String PEN = "✏️";
public static final String BUILD = "\uD83D\uDEE0"; public static final String BUILD = "\uD83D\uDEE0";
public static final String LINK = "\uD83D\uDD17"; public static final String LINK = "\uD83D\uDD17";
public static final String BELL ="\uD83D\uDD14";
private Icons() { private Icons() {
utilityClass(); utilityClass();

View File

@ -68,4 +68,11 @@ public class GitlabProperty {
private String discussionUrl; private String discussionUrl;
/**
* Адрес, по которому можно получить ISSUE
*/
private String issueUrl;
private String openIssueUrl;
} }

View File

@ -0,0 +1,97 @@
package dev.struchkov.bot.gitlab.core.service.convert;
import dev.struchkov.bot.gitlab.context.domain.IssueState;
import dev.struchkov.bot.gitlab.context.domain.IssueType;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.sdk.domain.*;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static dev.struchkov.haiti.utils.Checker.checkNotEmpty;
import static dev.struchkov.haiti.utils.Checker.checkNotNull;
/**
* @author Dmitry Sheyko [22.01.2023]
*/
@Component
@RequiredArgsConstructor
public class IssueJsonConverter implements Converter<IssueJson, Issue> {
private final PersonJsonConverter convertPerson;
@Override
public Issue convert(IssueJson source) {
final Issue issue = new Issue();
issue.setId(source.getId());
issue.setTwoId(source.getTwoId());
issue.setProjectId(source.getProjectId());
issue.setTitle(source.getTitle());
issue.setDescription(source.getDescription());
issue.setState(convertState(source.getState()));
issue.setCreatedDate(source.getCreatedDate());
issue.setUpdatedDate(source.getUpdatedDate());
issue.setCloseDate(source.getClosedDate());
issue.setType(convertType(source.getType()));
issue.setUserNotesCount(source.getUserNotesCount());
issue.setMergeRequestsCount(source.getMergeRequestsCount());
issue.setUpVotes(source.getUpVotes());
issue.setDownVotes(source.getDownVotes());
issue.setDueDate(source.getDueDate());
issue.setConfidential(source.getConfidential());
issue.setDiscussionLocked(source.getDiscussionLocked());
issue.setTaskCount(source.getTaskCompletionStatus().getCount());
issue.setTaskCompletedCount(source.getTaskCompletionStatus().getCompletedCount());
issue.setWebUrl(source.getWebUrl());
issue.setBlockingIssuesCount(source.getBlockingIssuesCount());
issue.setHasTasks(source.getHasTasks());
convertAssignees(issue, source.getAssignees());
convertLabels(issue, source.getLabels());
if (checkNotNull(source.getClosedBy())) {
issue.setClosedBy(convertPerson.convert(source.getClosedBy()));
}
issue.setAuthor(convertPerson.convert(source.getAuthor()));
return issue;
}
private void convertAssignees(Issue issue, List<PersonJson> jsonAssignees) {
if (checkNotEmpty(jsonAssignees)) {
final List<Person> assignees = jsonAssignees.stream()
.map(convertPerson::convert)
.toList();
issue.setAssignees(assignees);
}
}
private void convertLabels(Issue issue, Set<String> source) {
if (checkNotEmpty(source)) {
final Set<String> labels = source.stream()
.map(label -> label.replace("-", "_"))
.collect(Collectors.toSet());
issue.setLabels(labels);
}
}
private IssueState convertState(IssueStateJson state) {
return switch (state) {
case CLOSED -> IssueState.CLOSED;
case OPENED -> IssueState.OPENED;
};
}
private IssueType convertType(IssueTypeJson type) {
return switch (type) {
case ISSUE -> IssueType.ISSUE;
case INCIDENT -> IssueType.INCIDENT;
};
}
}

View File

@ -0,0 +1,313 @@
package dev.struchkov.bot.gitlab.core.service.impl;
import dev.struchkov.bot.gitlab.context.domain.*;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
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.issue.*;
import dev.struchkov.bot.gitlab.context.repository.IssueRepository;
import dev.struchkov.bot.gitlab.context.service.IssueService;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.domain.IssueState.CLOSED;
import static dev.struchkov.haiti.context.exception.NotFoundException.notFoundException;
import static dev.struchkov.haiti.utils.Checker.checkNotEmpty;
/**
* @author Dmitry Sheyko [25.01.2023]
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IssueServiceImpl implements IssueService {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
public static final Set<IssueState> DELETE_STATES = Set.of(CLOSED);
private final PersonInformation personInformation;
private final IssueRepository repository;
private final ProjectService projectService;
private final NotifyService notifyService;
@Override
@Transactional
public Issue create(@NonNull Issue issue) {
final boolean botUserAssignee = isBotUserAssignee(issue);
final boolean botUserAssigneeAndNotAuthor = isBotUserAssigneeAndNotAuthor(issue);
issue.setUserAssignee(botUserAssignee);
issue.setNotification(botUserAssigneeAndNotAuthor);
final Issue savedIssue = repository.save(issue);
if (botUserAssigneeAndNotAuthor) {
final String projectName = projectService.getByIdOrThrow(savedIssue.getProjectId()).getName();
sendNotifyAboutAssignee(issue, projectName);
}
return savedIssue;
}
private boolean isBotUserAssignee(Issue savedIssue) {
final Long gitlabUserId = personInformation.getId();
final List<Person> assignees = savedIssue.getAssignees();
if (checkNotEmpty(assignees)) {
for (Person assignee : assignees) {
if (gitlabUserId.equals(assignee.getId())) {
return true;
}
}
}
return false;
}
private boolean isBotUserAssigneeAndNotAuthor(Issue savedIssue) {
final Long gitlabUserId = personInformation.getId();
final boolean botUserAssignee = isBotUserAssignee(savedIssue);
if (botUserAssignee) {
return !gitlabUserId.equals(savedIssue.getAuthor().getId());
}
return false;
}
private void sendNotifyAboutAssignee(Issue issue, String projectName) {
final Long gitlabUserId = personInformation.getId();
if (!gitlabUserId.equals(issue.getAuthor().getId()) // создатель Issue не является пользователем бота
)
notifyService.send(
NewIssueNotify.builder()
.projectName(projectName)
.title(issue.getTitle())
.url(issue.getWebUrl())
.issueType(issue.getType().name())
.author(issue.getAuthor().getName())
.description(issue.getDescription())
.dueDate(issue.getDueDate().format(DATE_FORMAT))
.labels(issue.getLabels())
.confidential(issue.getConfidential().toString())
.build()
);
}
private void sendNotifyAboutDeleteFromAssignees(Issue issue, String projectName) {
final Long gitlabUserId = personInformation.getId();
if (!gitlabUserId.equals(issue.getAuthor().getId()) // создатель Issue не является пользователем бота
)
notifyService.send(
DeleteFromAssigneesNotify.builder()
.projectName(projectName)
.title(issue.getTitle())
.url(issue.getWebUrl())
.issueType(issue.getType().name())
.updateDate(issue.getUpdatedDate().format(DATE_FORMAT))
.build()
);
}
@Override
@Transactional
public Issue update(@NonNull Issue issue) {
final Issue oldIssue = repository.findById(issue.getId())
.orElseThrow(notFoundException("Issue не найдено"));
issue.setNotification(oldIssue.isNotification());
final Long gitlabUserId = personInformation.getId();
/**
* проверяем изменения списка Assignees: пользователь появился в списке или удален из него или без изменений.
*/
final AssigneesChanged assigneesChanged = AssigneesChanged.valueOf(gitlabUserId, oldIssue.getAssignees(), issue.getAssignees());
issue.setUserAssignee(assigneesChanged.getNewStatus(oldIssue.isUserAssignee()));
final boolean isChangedIssue = !oldIssue.getUpdatedDate().equals(issue.getUpdatedDate());
/**
* Удаление пользователя из assignee не всегда обновляет UpdatedDate, поэтому добавляется
* второе условие assigneesChanged.isChanged()
*/
if (isChangedIssue || assigneesChanged.isChanged()) {
if (assigneesChanged.equals(AssigneesChanged.BECOME) && !gitlabUserId.equals(issue.getAuthor().getId()))
issue.setNotification(true);
if (issue.isNotification()) {
final Project project = projectService.getByIdOrThrow(issue.getProjectId());
notifyAboutStatus(oldIssue, issue, project);
notifyAboutType(oldIssue, issue, project);
notifyAboutTitle(oldIssue, issue, project);
notifyAboutDescription(oldIssue, issue, project);
notifyAboutDueDate(oldIssue, issue, project);
notifyAboutChangeAssignees(assigneesChanged, issue, project);
}
return repository.save(issue);
}
return oldIssue;
}
@Override
public ExistContainer<Issue, Long> existsById(@NonNull Set<Long> issueIds) {
final List<Issue> existsEntity = repository.findAllById(issueIds);
final Set<Long> existsIds = existsEntity.stream().map(Issue::getId).collect(Collectors.toSet());
if (existsIds.containsAll(issueIds)) {
return ExistContainer.allFind(existsEntity);
} else {
final Set<Long> noExistsId = issueIds.stream()
.filter(id -> !existsIds.contains(id))
.collect(Collectors.toSet());
return ExistContainer.notAllFind(existsEntity, noExistsId);
}
}
@Override
public List<Issue> createAll(List<Issue> newIssues) {
return newIssues.stream()
.map(this::create)
.toList();
}
@Override
@Transactional
public List<Issue> updateAll(@NonNull List<Issue> issues) {
return issues.stream()
.map(this::update)
.collect(Collectors.toList());
}
@Override
public Set<IdAndStatusIssue> getAllId(Set<IssueState> statuses) {
return repository.findAllIdByStateIn(statuses);
}
protected void notifyAboutChangeAssignees(AssigneesChanged assigneesChanged, Issue issue, Project project) {
switch (assigneesChanged) {
case BECOME -> sendNotifyAboutAssignee(issue, project.getName());
case DELETED -> {
sendNotifyAboutDeleteFromAssignees(issue, project.getName());
issue.setUserAssignee(false);
issue.setNotification(false);
}
}
}
protected void notifyAboutTitle(Issue oldIssue, Issue newIssue, Project project) {
final String oldTitle = oldIssue.getTitle();
final String newTitle = newIssue.getTitle();
final Long gitlabUserId = personInformation.getId();
if (
!oldTitle.equals(newTitle) // заголовок изменился
&& !gitlabUserId.equals(oldIssue.getAuthor().getId()) // создатель Issue не является пользователем бота
) {
notifyService.send(
TitleIssueNotify.builder()
.projectName(project.getName())
.title(oldIssue.getTitle())
.url(oldIssue.getWebUrl())
.issueType(oldIssue.getType().name())
.newTitle(newTitle)
.build()
);
}
}
protected void notifyAboutDescription(Issue oldIssue, Issue newIssue, Project project) {
final String oldDescription = oldIssue.getDescription();
final String newDescription = newIssue.getDescription();
final Long gitlabUserId = personInformation.getId();
if (
!oldDescription.equals(newDescription) // описание изменилось
&& !gitlabUserId.equals(oldIssue.getAuthor().getId()) // создатель Issue не является пользователем бота
) {
notifyService.send(
DescriptionIssueNotify.builder()
.projectName(project.getName())
.title(oldIssue.getTitle())
.url(oldIssue.getWebUrl())
.issueType(oldIssue.getType().name())
.newDescription(newDescription)
.build()
);
}
}
protected void notifyAboutType(Issue oldIssue, Issue newIssue, Project project) {
final IssueType oldType = oldIssue.getType();
final IssueType newType = newIssue.getType();
final Long gitlabUserId = personInformation.getId();
if (
!oldType.equals(newType) // тип изменился
&& !gitlabUserId.equals(oldIssue.getAuthor().getId()) // создатель Issue не является пользователем бота
) {
notifyService.send(
TypeIssueNotify.builder()
.projectName(project.getName())
.title(oldIssue.getTitle())
.url(oldIssue.getWebUrl())
.issueType(oldIssue.getType().name())
.oldType(oldType)
.newType(newType)
.build()
);
}
}
protected void notifyAboutStatus(Issue oldIssue, Issue newIssue, Project project) {
final IssueState oldStatus = oldIssue.getState();
final IssueState newStatus = newIssue.getState();
final Long gitlabUserId = personInformation.getId();
if (
!oldStatus.equals(newStatus) // статус изменился
&& gitlabUserId.equals(oldIssue.getAuthor().getId()) // создатель Issue является пользователем бота
) {
notifyService.send(
StatusIssueNotify.builder()
.name(newIssue.getTitle())
.url(oldIssue.getWebUrl())
.issueType(oldIssue.getType().name())
.projectName(project.getName())
.newStatus(newStatus)
.oldStatus(oldStatus)
.build()
);
}
}
protected void notifyAboutDueDate(Issue oldIssue, Issue newIssue, Project project) {
final String oldDueDate = oldIssue.getDueDate().format(DATE_FORMAT);
final String newDueDate = newIssue.getDueDate().format(DATE_FORMAT);
final Long gitlabUserId = personInformation.getId();
if (
(!Objects.equals(oldDueDate, newDueDate)) // дата изменилась
&& (!gitlabUserId.equals(oldIssue.getAuthor().getId())) // создатель Issue не является пользователем бота
) {
notifyService.send(
DueDateIssueNotify.builder()
.projectName(project.getName())
.title(oldIssue.getTitle())
.url(oldIssue.getWebUrl())
.issueType(oldIssue.getType().name())
.oldDueDate(oldDueDate)
.newDueDate(newDueDate)
.build()
);
}
}
@Override
public void cleanOld() {
log.debug("Старт очистки старых Issue");
repository.deleteByStates(DELETE_STATES);
log.debug("Конец очистки старых Issue");
}
}

View File

@ -0,0 +1,166 @@
package dev.struchkov.bot.gitlab.core.service.parser;
import dev.struchkov.bot.gitlab.context.domain.*;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
import dev.struchkov.bot.gitlab.context.domain.entity.Person;
import dev.struchkov.bot.gitlab.context.service.IssueService;
import dev.struchkov.bot.gitlab.context.service.ProjectService;
import dev.struchkov.bot.gitlab.core.config.properties.GitlabProperty;
import dev.struchkov.bot.gitlab.core.config.properties.PersonProperty;
import dev.struchkov.bot.gitlab.core.service.parser.forktask.GetAllIssueForProjectTask;
import dev.struchkov.bot.gitlab.core.service.parser.forktask.GetSingleIssueTask;
import dev.struchkov.bot.gitlab.sdk.domain.IssueJson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static dev.struchkov.haiti.utils.Checker.checkNotEmpty;
import static dev.struchkov.haiti.utils.concurrent.ForkJoinUtils.pullTaskResult;
import static dev.struchkov.haiti.utils.concurrent.ForkJoinUtils.pullTaskResults;
/**
* @author Dmotry Sheyko [24.01.2023]
*/
@Slf4j
@Service
public class IssueParser {
private static final Set<IssueState> OLD_STATUSES = Set.of(IssueState.OPENED, IssueState.CLOSED);
private final GitlabProperty gitlabProperty;
private final IssueService issueService;
private final ProjectService projectService;
private final ConversionService conversionService;
private final PersonProperty personProperty;
private final ForkJoinPool forkJoinPool;
public IssueParser(
GitlabProperty gitlabProperty,
IssueService issueService,
ProjectService projectService,
ConversionService conversionService,
PersonProperty personProperty,
@Qualifier("parserPool") ForkJoinPool forkJoinPool
) {
this.gitlabProperty = gitlabProperty;
this.issueService = issueService;
this.projectService = projectService;
this.conversionService = conversionService;
this.personProperty = personProperty;
this.forkJoinPool = forkJoinPool;
}
public void parsingOldIssue(){
log.debug("Старт обработаки старых Issue");
final Set<IdAndStatusIssue> existIds = issueService.getAllId(OLD_STATUSES);
final List<Issue> newIssues = getOldIssues(existIds).stream()
.map(issueJson -> {
final Issue newIssue = conversionService.convert(issueJson, Issue.class);
return newIssue;
})
.collect(Collectors.toList());
if (checkNotEmpty(newIssues)) {
personMapping(newIssues);
issueService.updateAll(newIssues);
}
log.debug("Конец обработки старых Issue");
}
private List<IssueJson> getOldIssues(Set<IdAndStatusIssue> existIds) {
final List<ForkJoinTask<Optional<IssueJson>>> tasks = existIds.stream()
.map(
existId -> new GetSingleIssueTask(
gitlabProperty.getIssueUrl(),
existId.getProjectId(),
existId.getTwoId(),
personProperty.getToken()
)
).map(forkJoinPool::submit)
.collect(Collectors.toList());
return pullTaskResult(tasks).stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
}
public void parsingNewIssue() {
log.debug("Старт обработки новых Issue");
/**
* получаем через репозиторий список id всех проектов хранящихся в БД
*/
final Set<Long> projectIds = projectService.getAllIds();
/**
* На основе id проекта, url для получения issues по id проекта и токена пользователя
* выгружаем из GitLab список всех IssueJson. Получаем в многопоточном режиме.
*/
final List<IssueJson> issueJsons = getIssues(projectIds);
/**
* Получаем id всех IssueJson загруженных из GitLab
*/
if (checkNotEmpty(issueJsons)) {
final Set<Long> jsonIds = issueJsons.stream()
.map(IssueJson::getId)
.collect(Collectors.toSet());
final ExistContainer<Issue, Long> existContainer = issueService.existsById(jsonIds);
log.trace("Из {} полученных MR не найдены в хранилище {}", jsonIds.size(), existContainer.getIdNoFound().size());
if (!existContainer.isAllFound()) {
final List<Issue> newIssues = issueJsons.stream()
.filter(json -> existContainer.getIdNoFound().contains(json.getId()))
.map(json -> {
final Issue issue = conversionService.convert(json, Issue.class);
return issue;
})
.toList();
log.trace("Пачка новых issues обработана и отправлена на сохранение. Количество: {} шт.", newIssues.size());
issueService.createAll(newIssues);
}
}
log.debug("Конец обработки новых MR");
}
private List<IssueJson> getIssues(Set<Long> projectIds) {
final List<ForkJoinTask<List<IssueJson>>> tasks = projectIds.stream()
.map(projectId -> new GetAllIssueForProjectTask(projectId, gitlabProperty.getOpenIssueUrl(), personProperty.getToken()))
.map(forkJoinPool::submit)
.collect(Collectors.toList());
return pullTaskResults(tasks);
}
private static void personMapping(List<Issue> newIssues) {
final Map<Long, Person> personMap = Stream.concat(
newIssues.stream()
.map(Issue::getAuthor),
newIssues.stream()
.flatMap(issue -> issue.getAssignees().stream())
).distinct()
.filter(Objects::nonNull)
.collect(Collectors.toMap(Person::getId, p -> p));
for (Issue newIssue : newIssues) {
newIssue.setAuthor(personMap.get(newIssue.getAuthor().getId()));
newIssue.setAssignees(
newIssue.getAssignees().stream()
.map(reviewer -> personMap.get(reviewer.getId()))
.collect(Collectors.toList())
);
}
}
}

View File

@ -0,0 +1,55 @@
package dev.struchkov.bot.gitlab.core.service.parser.forktask;
import dev.struchkov.bot.gitlab.core.utils.StringUtils;
import dev.struchkov.bot.gitlab.sdk.domain.IssueJson;
import dev.struchkov.haiti.utils.network.HttpParse;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat;
import java.util.List;
import java.util.concurrent.RecursiveTask;
import static dev.struchkov.haiti.utils.Checker.checkNotEmpty;
import static dev.struchkov.haiti.utils.network.HttpParse.ACCEPT;
/**
* @author Dmitry Sheyko [24.01.2023]
*/
@Slf4j
@AllArgsConstructor
@RequiredArgsConstructor
public class GetAllIssueForProjectTask extends RecursiveTask<List<IssueJson>> {
private static final int PAGE_COUNT = 100;
private final long projectId;
private int pageNumber = 1;
private final String urlIssueOpen;
private final String gitlabToken;
@Override
@SneakyThrows
protected List<IssueJson> compute() {
Thread.sleep(200);
final List<IssueJson> issueJson = getIssueJsons();
if (checkNotEmpty(issueJson) && issueJson.size() == PAGE_COUNT) {
final GetAllIssueForProjectTask newTask = new GetAllIssueForProjectTask(projectId, pageNumber + 1, urlIssueOpen, gitlabToken);
newTask.fork();
issueJson.addAll(newTask.join());
}
return issueJson;
}
private List<IssueJson> getIssueJsons() {
final List<IssueJson> jsons = HttpParse.request(MessageFormat.format(urlIssueOpen, projectId, pageNumber, PAGE_COUNT))
.header(StringUtils.H_PRIVATE_TOKEN, gitlabToken)
.header(ACCEPT)
.executeList(IssueJson.class);
log.trace("Получено {} шт потенциально новых Issue для проекта id:'{}' ", jsons.size(), projectId);
return jsons;
}
}

View File

@ -0,0 +1,36 @@
package dev.struchkov.bot.gitlab.core.service.parser.forktask;
import dev.struchkov.bot.gitlab.core.utils.StringUtils;
import dev.struchkov.bot.gitlab.sdk.domain.IssueJson;
import dev.struchkov.haiti.utils.network.HttpParse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat;
import java.util.Optional;
import java.util.concurrent.RecursiveTask;
import static dev.struchkov.haiti.utils.network.HttpParse.ACCEPT;
@Slf4j
@RequiredArgsConstructor
public class GetSingleIssueTask extends RecursiveTask<Optional<IssueJson>> {
private final String urlIssue;
private final long projectId;
private final long issueTwoId;
private final String gitlabToken;
@Override
@SneakyThrows
protected Optional<IssueJson> compute() {
Thread.sleep(200);
final String mrUrl = MessageFormat.format(urlIssue, projectId, issueTwoId);
return HttpParse.request(mrUrl)
.header(ACCEPT)
.header(StringUtils.H_PRIVATE_TOKEN, gitlabToken)
.execute(IssueJson.class);
}
}

View File

@ -0,0 +1,56 @@
package dev.struchkov.bot.gitlab.data.impl;
import dev.struchkov.bot.gitlab.context.domain.IdAndStatusIssue;
import dev.struchkov.bot.gitlab.context.domain.IssueState;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
import dev.struchkov.bot.gitlab.context.repository.IssueRepository;
import dev.struchkov.bot.gitlab.data.jpa.IssueJpaRepository;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* @author Dmitry Sheyko [24.01.2023]
*/
@Repository
@RequiredArgsConstructor
public class IssueRepositoryImpl implements IssueRepository {
private final IssueJpaRepository jpaRepository;
@Override
@Transactional(readOnly = true)
public Set<IdAndStatusIssue> findAllIdByStateIn(@NonNull Set<IssueState> statuses) {
return jpaRepository.findAllIdByStateIn(statuses);
}
@Override
@Transactional
public Issue save(Issue issue) {
return jpaRepository.save(issue);
}
@Override
@Transactional(readOnly = true)
public Optional<Issue> findById(Long issueId) {
return jpaRepository.findById(issueId);
}
@Override
@Transactional(readOnly = true)
public List<Issue> findAllById(Set<Long> issueIds) {
return jpaRepository.findAllById(issueIds);
}
@Override
@Transactional
public void deleteByStates(Set<IssueState> states) {
jpaRepository.deleteAllByStateIn(states);
}
}

View File

@ -0,0 +1,22 @@
package dev.struchkov.bot.gitlab.data.jpa;
import dev.struchkov.bot.gitlab.context.domain.IdAndStatusIssue;
import dev.struchkov.bot.gitlab.context.domain.IssueState;
import dev.struchkov.bot.gitlab.context.domain.entity.Issue;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Set;
/**
* @author Dmitry Sheyko [24.01.2023]
*/
public interface IssueJpaRepository extends JpaRepository<Issue, Long> {
@Query("SELECT new dev.struchkov.bot.gitlab.context.domain.IdAndStatusIssue(i.id, i.twoId, i.projectId, i.state) FROM Issue i WHERE i.state IN :states")
Set<IdAndStatusIssue> findAllIdByStateIn(@Param("states") Set<IssueState> states);
void deleteAllByStateIn(Set<IssueState> states);
}

View File

@ -1,9 +1,11 @@
package dev.struchkov.bot.gitlab.scheduler; package dev.struchkov.bot.gitlab.scheduler;
import dev.struchkov.bot.gitlab.context.service.AppSettingService; import dev.struchkov.bot.gitlab.context.service.AppSettingService;
import dev.struchkov.bot.gitlab.context.service.IssueService;
import dev.struchkov.bot.gitlab.context.service.MergeRequestsService; import dev.struchkov.bot.gitlab.context.service.MergeRequestsService;
import dev.struchkov.bot.gitlab.context.service.PipelineService; import dev.struchkov.bot.gitlab.context.service.PipelineService;
import dev.struchkov.bot.gitlab.core.service.parser.DiscussionParser; import dev.struchkov.bot.gitlab.core.service.parser.DiscussionParser;
import dev.struchkov.bot.gitlab.core.service.parser.IssueParser;
import dev.struchkov.bot.gitlab.core.service.parser.MergeRequestParser; import dev.struchkov.bot.gitlab.core.service.parser.MergeRequestParser;
import dev.struchkov.bot.gitlab.core.service.parser.PipelineParser; import dev.struchkov.bot.gitlab.core.service.parser.PipelineParser;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -28,6 +30,9 @@ public class SchedulerService {
private final PipelineService pipelineService; private final PipelineService pipelineService;
private final MergeRequestsService mergeRequestsService; private final MergeRequestsService mergeRequestsService;
private final IssueParser issueParser;
private final IssueService issueService;
@Scheduled(cron = "0 */1 * * * *") @Scheduled(cron = "0 */1 * * * *")
public void newMergeRequest() { public void newMergeRequest() {
log.debug("Запуск процесса обновления данных"); log.debug("Запуск процесса обновления данных");
@ -40,6 +45,9 @@ public class SchedulerService {
discussionParser.scanNewDiscussion(); discussionParser.scanNewDiscussion();
mergeRequestsService.cleanOld(); mergeRequestsService.cleanOld();
pipelineService.cleanOld(); pipelineService.cleanOld();
issueParser.parsingNewIssue();
issueParser.parsingOldIssue();
issueService.cleanOld();
} else { } else {
log.warn("Процесс обновления данных не был выполнен, так как пользователь не выполнил первичную настройку."); log.warn("Процесс обновления данных не был выполнен, так как пользователь не выполнил первичную настройку.");
} }

View File

@ -16,6 +16,8 @@ spring:
jdbc: jdbc:
lob: lob:
non_contextual_creation: true non_contextual_creation: true
event:
merge.entity_copy_observer: allow
telegram-config: telegram-config:
bot-username: ${TELEGRAM_BOT_USERNAME} bot-username: ${TELEGRAM_BOT_USERNAME}
bot-token: ${TELEGRAM_BOT_TOKEN} bot-token: ${TELEGRAM_BOT_TOKEN}
@ -51,3 +53,5 @@ gitlab-bot:
new-note-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/merge_requests/{1,number,#}/discussions/{2}/notes?body={3}" new-note-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/merge_requests/{1,number,#}/discussions/{2}/notes?body={3}"
discussions-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/merge_requests/{1,number,#}/discussions?&page={2,number,#}&per_page={3,number,#}" discussions-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/merge_requests/{1,number,#}/discussions?&page={2,number,#}&per_page={3,number,#}"
discussion-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/merge_requests/{1,number,#}/discussions/{2}" discussion-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/merge_requests/{1,number,#}/discussions/{2}"
issue-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/issues/{1,number,#}"
open-issue-url: "${GITLAB_URL}/api/v4/projects/{0,number,#}/issues?state=opened&page={1, number, integer}&per_page={2, number, integer}"

View File

@ -77,6 +77,12 @@
<column name="has_tasks" type="boolean"> <column name="has_tasks" type="boolean">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="notification" type="boolean">
<constraints nullable="false"/>
</column>
<column name="is_assignee" type="boolean">
<constraints nullable="false"/>
</column>
</createTable> </createTable>
</changeSet> </changeSet>

View File

@ -3,11 +3,15 @@ package dev.struchkov.bot.gitlab.sdk.domain;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Data; import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -47,7 +51,7 @@ public class IssueJson {
private Set<String> labels; private Set<String> labels;
private MilestoneJson milestone; private MilestoneJson milestone;
private Set<PersonJson> assignees; private List<PersonJson> assignees;
private PersonJson author; private PersonJson author;
private IssueTypeJson type; private IssueTypeJson type;
private PersonJson assignee; private PersonJson assignee;
@ -64,10 +68,10 @@ public class IssueJson {
@JsonProperty("downvotes") @JsonProperty("downvotes")
private Integer downVotes; private Integer downVotes;
@JsonSerialize(using = LocalDateTimeSerializer.class) @JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonDeserialize(using = LocalDateDeserializer.class)
@JsonProperty("due_date") @JsonProperty("due_date")
private LocalDateTime dueDate; private LocalDate dueDate;
private Boolean confidential; private Boolean confidential;
@JsonProperty("discussion_locked") @JsonProperty("discussion_locked")

View File

@ -3,10 +3,13 @@ package dev.struchkov.bot.gitlab.sdk.domain;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Data; import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
@ -36,15 +39,15 @@ public class MilestoneJson {
@JsonProperty("updated_at") @JsonProperty("updated_at")
private LocalDateTime updatedDate; private LocalDateTime updatedDate;
@JsonSerialize(using = LocalDateTimeSerializer.class) @JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonDeserialize(using = LocalDateDeserializer.class)
@JsonProperty("start_date") @JsonProperty("start_date")
private LocalDateTime startDate; private LocalDate startDate;
@JsonSerialize(using = LocalDateTimeSerializer.class) @JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonDeserialize(using = LocalDateDeserializer.class)
@JsonProperty("due_date") @JsonProperty("due_date")
private LocalDateTime dueDate; private LocalDate dueDate;
private boolean expired; private boolean expired;
@JsonProperty("web_url") @JsonProperty("web_url")

View File

@ -0,0 +1,37 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.*;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
/**
* @author Dmitry Sheyko 26.01.2021
*/
@Component
public class DeleteFromAssigneesOfIssueNotifyGenerator implements NotifyBoxAnswerGenerator<DeleteFromAssigneesNotify> {
@Override
public BoxAnswer generate(DeleteFromAssigneesNotify notify) {
final StringBuilder builder = new StringBuilder(Icons.PEN)
.append(String.format(" *You excluded from %s assignees | ", notify.getIssueType()))
.append(notify.getProjectName()).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()))
.append(Icons.HR)
.append(notify.getUpdateDate());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return DeleteFromAssigneesNotify.TYPE;
}
}

View File

@ -0,0 +1,39 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.DescriptionIssueNotify;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
/**
* @author Dmitry Sheyko 26.01.2021
*/
@Component
public class DescriptionIssueNotifyGenerator implements NotifyBoxAnswerGenerator<DescriptionIssueNotify> {
@Override
public BoxAnswer generate(DescriptionIssueNotify notify) {
final StringBuilder builder = new StringBuilder(Icons.PEN)
.append(String.format(" *Description of %s changed | ", notify.getIssueType()))
.append(notify.getProjectName()).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()))
.append(Icons.HR)
.append("new description: ")
.append(notify.getNewDescription());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return DescriptionIssueNotify.TYPE;
}
}

View File

@ -0,0 +1,38 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.DueDateIssueNotify;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
/**
* @author Dmitry Sheyko 26.01.2021
*/
@Component
public class DueDateIssueNotifyGenerator implements NotifyBoxAnswerGenerator<DueDateIssueNotify> {
@Override
public BoxAnswer generate(DueDateIssueNotify notify) {
final StringBuilder builder = new StringBuilder(Icons.PEN)
.append(String.format(" *Due date of %s changed | ", notify.getIssueType()))
.append(notify.getProjectName()).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()))
.append(Icons.HR)
.append(notify.getOldDueDate()).append(Icons.ARROW).append(notify.getNewDueDate());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return DueDateIssueNotify.TYPE;
}
}

View File

@ -0,0 +1,49 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.NewIssueNotify;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import java.util.stream.Collectors;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
import static dev.struchkov.haiti.utils.Strings.escapeMarkdown;
/**
* @author Dmitry Sheyko 24.01.2023
*/
@Component
public class NewIssueNotifyGenerator implements NotifyBoxAnswerGenerator<NewIssueNotify> {
@Override
public BoxAnswer generate(NewIssueNotify notify) {
final String labelText = notify.getLabels().stream()
.map(label -> "#" + label)
.collect(Collectors.joining(" "));
final StringBuilder builder = new StringBuilder(Icons.FUN)
.append(String.format(" *New %s assigned to you | ", notify.getIssueType()))
.append(escapeMarkdown(notify.getProjectName())).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()));
if (!labelText.isEmpty()) {
builder.append("\n\n").append(labelText);
}
builder.append(Icons.HR)
.append(Icons.BELL).append(": ").append(notify.getTitle()).append("\n")
.append(Icons.AUTHOR).append(": ").append(notify.getAuthor());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return NewIssueNotify.TYPE;
}
}

View File

@ -0,0 +1,38 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.StatusIssueNotify;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
/**
* @author Dmitry Sheyko 26.01.2021
*/
@Component
public class StatusIssueNotifyGenerator implements NotifyBoxAnswerGenerator<StatusIssueNotify> {
@Override
public BoxAnswer generate(StatusIssueNotify notify) {
final StringBuilder builder = new StringBuilder(Icons.PEN)
.append(String.format(" *Status of %s changed | ", notify.getIssueType()))
.append(notify.getProjectName()).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()))
.append(Icons.HR)
.append(notify.getOldStatus().name()).append(Icons.ARROW).append(notify.getNewStatus().name());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return StatusIssueNotify.TYPE;
}
}

View File

@ -0,0 +1,39 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.TitleIssueNotify;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
/**
* @author Dmitry Sheyko 26.01.2021
*/
@Component
public class TitleIssueNotifyGenerator implements NotifyBoxAnswerGenerator<TitleIssueNotify> {
@Override
public BoxAnswer generate(TitleIssueNotify notify) {
final StringBuilder builder = new StringBuilder(Icons.PEN)
.append(String.format(" *Title of %s changed | ", notify.getIssueType()))
.append(notify.getProjectName()).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()))
.append(Icons.HR)
.append("new title: ")
.append(notify.getNewTitle());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return TitleIssueNotify.TYPE;
}
}

View File

@ -0,0 +1,38 @@
package dev.struchkov.bot.gitlab.telegram.service.notify;
import dev.struchkov.bot.gitlab.context.domain.notify.issue.TypeIssueNotify;
import dev.struchkov.bot.gitlab.context.utils.Icons;
import dev.struchkov.godfather.main.domain.BoxAnswer;
import org.springframework.stereotype.Component;
import static dev.struchkov.bot.gitlab.context.utils.Icons.link;
import static dev.struchkov.godfather.main.domain.BoxAnswer.boxAnswer;
/**
* @author Dmitry Sheyko 26.01.2021
*/
@Component
public class TypeIssueNotifyGenerator implements NotifyBoxAnswerGenerator<TypeIssueNotify> {
@Override
public BoxAnswer generate(TypeIssueNotify notify) {
final StringBuilder builder = new StringBuilder(Icons.PEN)
.append(String.format(" *Type of %s changed | ", notify.getIssueType()))
.append(notify.getProjectName()).append("*")
.append(Icons.HR)
.append(link(notify.getType(), notify.getUrl()))
.append(Icons.HR)
.append(notify.getOldType().name()).append(Icons.ARROW).append(notify.getNewType().name());
final String notifyMessage = builder.toString();
return boxAnswer(notifyMessage);
}
@Override
public String getNotifyType() {
return TypeIssueNotify.TYPE;
}
}