diff --git a/apps/backend/src/main/java/no/nav/data/etterlevelse/krav/TilbakemeldingController.java b/apps/backend/src/main/java/no/nav/data/etterlevelse/krav/TilbakemeldingController.java index d2fc6ef77..368006675 100644 --- a/apps/backend/src/main/java/no/nav/data/etterlevelse/krav/TilbakemeldingController.java +++ b/apps/backend/src/main/java/no/nav/data/etterlevelse/krav/TilbakemeldingController.java @@ -6,12 +6,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import no.nav.data.common.rest.RestResponsePage; +import no.nav.data.common.security.SecurityUtils; import no.nav.data.etterlevelse.krav.domain.Tilbakemelding; import no.nav.data.etterlevelse.krav.domain.TilbakemeldingStatus; import no.nav.data.etterlevelse.krav.dto.CreateTilbakemeldingRequest; import no.nav.data.etterlevelse.krav.dto.EditTilbakemeldingRequest; import no.nav.data.etterlevelse.krav.dto.TilbakemeldingNewMeldingRequest; import no.nav.data.etterlevelse.krav.dto.TilbakemeldingResponse; +import no.nav.data.integration.slack.SlackService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -33,6 +35,7 @@ public class TilbakemeldingController { private final TilbakemeldingService tilbakemeldingService; private final TilbakemeldingValidator validator; + private final SlackService slackService; // Tilbakemelding @@ -99,6 +102,17 @@ public ResponseEntity updateTilbakemeldingStatusAndEndre var tilbakemelding = tilbakemeldingService.updateTilbakemeldingStatusAndEndretKrav(tilbakeMeldingId, status, endretkrav); return ResponseEntity.ok(tilbakemelding.toResponse()); } + + @Operation(summary = "Initiates sending of all pending slack messages (admin use only)") + @ApiResponse(description = "Pending slack messages sent") + @GetMapping("/flushSlack") + public void flushSlack() { + if (!SecurityUtils.isAdmin()) { + log.info("Ignoring request to flush slack messages: user not admin"); + } + log.info("Requested to flush pending slack messages"); + slackService.sendAll(); + } static class TilbakemeldingPage extends RestResponsePage { diff --git a/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/VarselService.java b/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/VarselService.java index 38dad2ccc..e40ad67c5 100644 --- a/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/VarselService.java +++ b/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/VarselService.java @@ -6,6 +6,7 @@ import no.nav.data.common.security.SecurityProperties; import no.nav.data.etterlevelse.varsel.domain.Varsel; import no.nav.data.etterlevelse.varsel.domain.Varslingsadresse; +import no.nav.data.integration.slack.SlackMelding; import no.nav.data.integration.slack.SlackService; import org.apache.commons.lang3.NotImplementedException; import org.springframework.stereotype.Service; @@ -24,17 +25,18 @@ public void varsle(List recipients, Varsel varsel) { for (Varslingsadresse varslingsadresse : recipients) { switch (varslingsadresse.getType()) { case EPOST -> emailService.scheduleMail(MailTask.builder().to(varslingsadresse.getAdresse()).subject(varsel.getTitle()).body(varsel.toHtml()).build()); - case SLACK -> slackService.sendMessageToChannel(varslingsadresse.getAdresse(), varsel.toSlack()); + case SLACK -> slackService.sendMessageToChannel(varslingsadresse.getAdresse(), varsel.toSlack(), SlackMelding.PRIORITY_LOW); case SLACK_USER -> { if (securityProperties.isDev()) { - slackService.sendMessageToChannel(varslingsadresse.getAdresse(), varsel.toSlack()); + slackService.sendMessageToChannel(varslingsadresse.getAdresse(), varsel.toSlack(), SlackMelding.PRIORITY_LOW); } else { - slackService.sendMessageToUser(varslingsadresse.getAdresse(), varsel.toSlack()); + slackService.sendMessageToUser(varslingsadresse.getAdresse(), varsel.toSlack(), SlackMelding.PRIORITY_LOW); } } default -> throw new NotImplementedException("%s is not an implemented varsel type".formatted(varslingsadresse.getType())); } }; + } } diff --git a/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/domain/Varsel.java b/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/domain/Varsel.java index d8cd1128b..694d78910 100644 --- a/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/domain/Varsel.java +++ b/apps/backend/src/main/java/no/nav/data/etterlevelse/varsel/domain/Varsel.java @@ -6,7 +6,7 @@ import lombok.NoArgsConstructor; import lombok.Singular; import lombok.Value; -import no.nav.data.integration.slack.dto.SlackDtos.PostMessageRequest.Block; +import no.nav.data.integration.slack.dto.SlackDtos.Block; import java.util.ArrayList; import java.util.Arrays; diff --git a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackClient.java b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackClient.java index 161f39690..2c366eadc 100644 --- a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackClient.java +++ b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackClient.java @@ -12,12 +12,12 @@ import no.nav.data.common.web.TraceHeaderRequestInterceptor; import no.nav.data.etterlevelse.varsel.domain.SlackChannel; import no.nav.data.etterlevelse.varsel.domain.SlackUser; +import no.nav.data.integration.slack.dto.SlackDtos.Block; import no.nav.data.integration.slack.dto.SlackDtos.Channel; import no.nav.data.integration.slack.dto.SlackDtos.CreateConversationRequest; import no.nav.data.integration.slack.dto.SlackDtos.CreateConversationResponse; import no.nav.data.integration.slack.dto.SlackDtos.ListChannelResponse; import no.nav.data.integration.slack.dto.SlackDtos.PostMessageRequest; -import no.nav.data.integration.slack.dto.SlackDtos.PostMessageRequest.Block; import no.nav.data.integration.slack.dto.SlackDtos.PostMessageResponse; import no.nav.data.integration.slack.dto.SlackDtos.Response; import no.nav.data.integration.slack.dto.SlackDtos.UserResponse; diff --git a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMelding.java b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMelding.java index 61eac17c2..6869a0fdc 100644 --- a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMelding.java +++ b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMelding.java @@ -11,9 +11,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import no.nav.data.integration.slack.dto.SlackDtos.PostMessageRequest.Block; +import no.nav.data.integration.slack.dto.SlackDtos.Block; import org.hibernate.annotations.Type; import java.util.List; @@ -30,6 +29,9 @@ @Entity @Table(name = "SLACK_MELDING") public class SlackMelding { + + public static final int PRIORITY_LOW = 0; + public static final int PRIORITY_HIGH = 10; @Id @SequenceGenerator(name = "slack_melding_id_seq", sequenceName = "slack_melding_id_seq", allocationSize = 1) @@ -42,9 +44,11 @@ public class SlackMelding { @Builder.Default private SlackMeldingData data = new SlackMeldingData(); - public SlackMelding(String mottager, boolean tilKanal, List blocks) { + public SlackMelding(String mottager, boolean tilKanal, int prioritet, List blocks) { + data = new SlackMeldingData(); data.mottager = mottager; data.sendTilKanal = tilKanal; + data.prioritet = prioritet; data.blocks = blocks; } @@ -53,8 +57,18 @@ public SlackMelding(String mottager, boolean tilKanal, List blocks) { @NoArgsConstructor @AllArgsConstructor public static class SlackMeldingData { + + /* + * OBS!!! Instanser av denne klassen vil bli serialisert til en arbeidstabell, og der kan de være i flere dager. Derfor: + * Ikke gjør endringer her som medfører at eksisterende rader i arbeidstabellen ikke lar seg deserialisere! + * Ikke gjør endringer her som medfører at eksisterende rader i arbeidstabellen ikke lar seg deserialisere! + * Ikke gjør endringer her som medfører at eksisterende rader i arbeidstabellen ikke lar seg deserialisere! + * Se no.nav.data.etterlevelse.krav.TilbakemeldingController.flushSlack() for manuell tømmin av arbeidstabellen. + */ + private String mottager; private boolean sendTilKanal; + private int prioritet; // == 0 → send bulk, >= 10 → send ASAP private List blocks; } diff --git a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMeldingRepo.java b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMeldingRepo.java index 12a2b2646..06762d098 100644 --- a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMeldingRepo.java +++ b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackMeldingRepo.java @@ -3,9 +3,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.Optional; + public interface SlackMeldingRepo extends JpaRepository { - @Query(value = "select * from slack_melding limit 1", nativeQuery = true) - public SlackMelding getOne(); + @Query(value = "select * from slack_melding where data ->> prioritet >= ?1 limit 1", nativeQuery = true) + public Optional getOneWithPriority(int priorityThreshold); } diff --git a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackService.java b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackService.java index cb2cd07f0..76f00d238 100644 --- a/apps/backend/src/main/java/no/nav/data/integration/slack/SlackService.java +++ b/apps/backend/src/main/java/no/nav/data/integration/slack/SlackService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; -import no.nav.data.integration.slack.dto.SlackDtos.PostMessageRequest.Block; +import no.nav.data.integration.slack.dto.SlackDtos.Block; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; @@ -20,38 +20,50 @@ public class SlackService { private final SlackClient slackClient; @Transactional(propagation = Propagation.REQUIRED) - public void sendMessageToChannel(String mottager, List blocks) { - SlackMelding sm = new SlackMelding(mottager, false, blocks); + public void sendMessageToChannel(String mottager, List blocks, int priority) { + SlackMelding sm = new SlackMelding(mottager, false, priority, blocks); repo.save(sm); } @Transactional(propagation = Propagation.REQUIRED) - public void sendMessageToUser(String mottager, List blocks) { - SlackMelding sm = new SlackMelding(mottager, true, blocks); + public void sendMessageToUser(String mottager, List blocks, int priority) { + SlackMelding sm = new SlackMelding(mottager, true, priority, blocks); repo.save(sm); } - @SchedulerLock(name = "sendSlack") - @Scheduled(cron = "0 0 13 * * *") // Happens every day at 1300 + @SchedulerLock(name = "sendSlackEnGros") + @Scheduled(cron = "0 55 12 * * *") // Happens every day at 12:55:00 public void sendAll() { log.info("Sending all pending slack messages..."); int sendCount = 0; - while (repo.count() > 0) { - sendOneMessage(); + while (sendOneMessage(SlackMelding.PRIORITY_LOW)) { sendCount++; } log.info("Done sending {} pending slack messages", sendCount); } + @SchedulerLock(name = "sendSlackPriority") + @Scheduled(cron = "10 * * * * *") // Happens every minute at 10 seconds. + public void sendHighPriority() { + int sendCount = 0; + while (sendOneMessage(SlackMelding.PRIORITY_HIGH)) { + sendCount++; + } + log.info("Done sending {} high priority slack messages", sendCount); + } + @Transactional() - private void sendOneMessage() { - SlackMelding sm = repo.getOne(); + // Returns true if there was a pending message to send, false otherwise. + private boolean sendOneMessage(int priority) { + SlackMelding sm = repo.getOneWithPriority(priority).orElse(null); + if (sm == null) return false; if (sm.getSendTilKanal()) { slackClient.sendMessageToChannel(sm.getMottager(), sm.getBlocks()); } else { slackClient.sendMessageToUserId(sm.getMottager(), sm.getBlocks()); } repo.delete(sm); + return true; } } diff --git a/apps/backend/src/main/java/no/nav/data/integration/slack/dto/SlackDtos.java b/apps/backend/src/main/java/no/nav/data/integration/slack/dto/SlackDtos.java index af2b8e21f..e7366b03b 100644 --- a/apps/backend/src/main/java/no/nav/data/integration/slack/dto/SlackDtos.java +++ b/apps/backend/src/main/java/no/nav/data/integration/slack/dto/SlackDtos.java @@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.Value; import no.nav.data.common.utils.JsonUtils; import no.nav.data.etterlevelse.varsel.domain.SlackChannel; @@ -13,62 +15,79 @@ import java.util.List; -public class SlackDtos { +public final class SlackDtos { + + // FIXME + + /* + * OBS!!! Instanser av noen av klassen her (Block m/ innhold) vil bli serialisert til en arbeidstabell, og der kan + * de være i flere dager. Derfor: + * Ikke gjør endringer her som medfører at eksisterende rader i arbeidstabellen ikke lar seg deserialisere! + * Ikke gjør endringer her som medfører at eksisterende rader i arbeidstabellen ikke lar seg deserialisere! + * Ikke gjør endringer her som medfører at eksisterende rader i arbeidstabellen ikke lar seg deserialisere! + * Se no.nav.data.etterlevelse.krav.TilbakemeldingController.flushSlack() for manuell tømmin av arbeidstabellen. + */ - @Value + @Data + @AllArgsConstructor + @NoArgsConstructor public static class PostMessageRequest { + private String channel = null; + private List blocks = null; + } - String channel; - List blocks; - - @Value - public static class Block { - - public static Block header(String text) { - return new Block(BlockType.header, Text.plain(text)); - } + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Block { - public static Block text(String text) { - return new Block(BlockType.section, Text.markdown(text)); - } + private BlockType type = null; + @JsonInclude(Include.NON_NULL) + private Text text = null; - public static Block divider() { - return new Block(BlockType.divider, null); - } + public static Block header(String text) { + return new Block(BlockType.header, Text.plain(text)); + } - /** - * Create Block with text, keeping other properties - */ - public Block withText(String newText) { - Assert.isTrue(text != null, "this is not a text block"); - return new Block(type, new Text(text.type, newText)); - } + public static Block text(String text) { + return new Block(BlockType.section, Text.markdown(text)); + } - public enum BlockType { - header, section, divider - } + public static Block divider() { + return new Block(BlockType.divider, null); + } - BlockType type; - @JsonInclude(Include.NON_NULL) - Text text; + /** + * Create Block with text, keeping other properties + */ + public Block withText(String newText) { + Assert.isTrue(text != null, "this is not a text block"); + return new Block(type, new Text(text.type, newText)); + } + } - @Value - public static class Text { + public enum BlockType { + header, section, divider + } - enum TextType {mrkdwn, plain_text} + public enum TextType { + mrkdwn, plain_text + } - TextType type; - String text; + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Text { - public static Text plain(String text) { - return new Text(TextType.plain_text, text); - } + private TextType type; + private String text; - public static Text markdown(String text) { - return new Text(TextType.mrkdwn, text); + public static Text plain(String text) { + return new Text(TextType.plain_text, text); + } - } - } + public static Text markdown(String text) { + return new Text(TextType.mrkdwn, text); } }