Merge branch 'feature/notifications' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m21s
Build Docker Image / Clean-Registry (push) Successful in -2s

This commit is contained in:
2026-01-27 00:20:13 +01:00
9 changed files with 35 additions and 36 deletions

View File

@@ -7,7 +7,6 @@ import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED; import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED;
import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;

View File

@@ -7,6 +7,7 @@ import static de.srsoftware.tools.PathHandler.GET;
import static de.srsoftware.tools.PathHandler.POST; import static de.srsoftware.tools.PathHandler.POST;
import static de.srsoftware.tools.Strings.hex; import static de.srsoftware.tools.Strings.hex;
import static de.srsoftware.umbrella.core.Errors.INVALID_URL; import static de.srsoftware.umbrella.core.Errors.INVALID_URL;
import static de.srsoftware.umbrella.core.constants.Constants.TIME_FORMATTER;
import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.serverError; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.serverError;
import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.*;
@@ -200,7 +201,7 @@ public class Util {
plantumlJar = file; plantumlJar = file;
} }
public static LocalDateTime dateTimeOf(long epocSecs){ public static String dateTimeOf(long epochMilis){
return LocalDateTime.ofInstant(Instant.ofEpochSecond(epocSecs), ZoneId.systemDefault()); return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilis), ZoneId.systemDefault()).format(TIME_FORMATTER);
} }
} }

View File

@@ -21,7 +21,7 @@ public class Constants {
public static final String NO_CACHE = "no-cache"; public static final String NO_CACHE = "no-cache";
public static final String NONE = "none"; public static final String NONE = "none";
public static final String TABLE_SETTINGS = "settings"; public static final String TABLE_SETTINGS = "settings";
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final String UMBRELLA = "Umbrella"; public static final String UMBRELLA = "Umbrella";
public static final String UTF8 = UTF_8.displayName(); public static final String UTF8 = UTF_8.displayName();

View File

@@ -10,7 +10,6 @@ import static de.srsoftware.umbrella.core.model.Translatable.t;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -22,7 +21,6 @@ import org.json.JSONObject;
public class Envelope<T extends Message<?>> { public class Envelope<T extends Message<?>> {
private final T message; private final T message;
private final Set<User> receivers; private final Set<User> receivers;
private LocalDateTime time;
private final long id; private final long id;
public Envelope(long id, T message, User receiver){ public Envelope(long id, T message, User receiver){
@@ -32,7 +30,6 @@ public class Envelope<T extends Message<?>> {
public Envelope(long id, T message, Collection<? extends User> receivers) { public Envelope(long id, T message, Collection<? extends User> receivers) {
this.message = message; this.message = message;
this.receivers = new HashSet<>(receivers); this.receivers = new HashSet<>(receivers);
time = LocalDateTime.now();
this.id = id; this.id = id;
} }
@@ -60,12 +57,12 @@ public class Envelope<T extends Message<?>> {
@Override @Override
public final boolean equals(Object o) { public final boolean equals(Object o) {
if (!(o instanceof Envelope<?> envelope)) return false; if (!(o instanceof Envelope<?> envelope)) return false;
return message.equals(envelope.message) && time.equals(envelope.time); return message.equals(envelope.message) && id == envelope.id;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return 31 * message.hashCode() + time.hashCode(); return message.hashCode();
} }
public long id(){ public long id(){
@@ -84,15 +81,6 @@ public class Envelope<T extends Message<?>> {
return receivers; return receivers;
} }
public Envelope<T> time(LocalDateTime timestamp){
this.time = timestamp;
return this;
}
public LocalDateTime time(){
return time;
}
@Override @Override
public String toString() { public String toString() {
return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(User::email).map(EmailAddress::toString).collect(Collectors.joining(", ")),message.subject()); return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(User::email).map(EmailAddress::toString).collect(Collectors.joining(", ")),message.subject());

View File

@@ -10,12 +10,14 @@ public abstract class Message<T> {
private final Collection<Attachment> attachments; private final Collection<Attachment> attachments;
private final T body, subject; private final T body, subject;
private final UmbrellaUser sender; private final UmbrellaUser sender;
private long utcTime;
public Message(UmbrellaUser sender, T subject, T body, Collection<Attachment> attachments){ public Message(UmbrellaUser sender, T subject, T body, Collection<Attachment> attachments){
this.sender = sender; this.sender = sender;
this.subject = subject; this.subject = subject;
this.body = body; this.body = body;
this.attachments = attachments; this.attachments = attachments;
this.utcTime = System.currentTimeMillis();
} }
public Collection<Attachment> attachments(){ public Collection<Attachment> attachments(){
@@ -49,4 +51,13 @@ public abstract class Message<T> {
public String toString() { public String toString() {
return format("{0}(from: {1}), subject: {2}",getClass().getSimpleName(),sender,subject); return format("{0}(from: {1}), subject: {2}",getClass().getSimpleName(),sender,subject);
} }
public long utcTime() {
return utcTime;
}
public Message<T> utcTime(long newValue){
utcTime = newValue;
return this;
}
} }

View File

@@ -9,6 +9,8 @@ public class TranslatableMessage extends Message<Translatable> {
} }
public TranslatedMessage translate(String lang){ public TranslatedMessage translate(String lang){
return new TranslatedMessage(sender(),subject().translate(lang),body().translate(lang),attachments()); var translated = new TranslatedMessage(sender(),subject().translate(lang),body().translate(lang),attachments());
translated.utcTime(this.utcTime());
return translated;
} }
} }

View File

@@ -39,6 +39,8 @@ import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart; import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.util.ByteArrayDataSource; import jakarta.mail.util.ByteArrayDataSource;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.*; import java.util.*;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -189,12 +191,12 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
private boolean markRead(HttpExchange ex, UmbrellaUser user, String path) { private boolean markRead(HttpExchange ex, UmbrellaUser user, String path) {
try { try {
var hash = Integer.parseInt(path); var id = Integer.parseInt(path);
var envelope = queue.markRead(hash, user); var envelope = queue.markRead(id, user);
if (envelope.isPresent()) return sendMessage(ex,user,envelope.get()); if (envelope.isPresent()) return sendMessage(ex,user,envelope.get());
return notFound(ex); return notFound(ex);
} catch (NumberFormatException | IOException e) { } catch (NumberFormatException | IOException e) {
throw invalidField(HASH,LONG); throw invalidField(ID,LONG);
} }
} }
@@ -221,9 +223,9 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
try { try {
var envelopes = queue.getEnvelopesFor(receiver); var envelopes = queue.getEnvelopesFor(receiver);
envelopes.stream().forEach(combined::merge); envelopes.stream().map(Envelope::message).forEach(combined::merge);
send(combined,date); send(combined,date);
envelopes.forEach(env -> queue.markRead(env.hashCode(),receiver)); envelopes.forEach(env -> queue.markRead(env.id(),receiver));
} catch (Exception ex){ } catch (Exception ex){
LOG.log(WARNING,"Failed to deliver mail ({0}) to {1}.",combined.subject(),receiver,ex); LOG.log(WARNING,"Failed to deliver mail ({0}) to {1}.",combined.subject(),receiver,ex);
for (var message : combined.messages()) exceptions.computeIfAbsent(new Receiver(receiver,message), k -> new ArrayList<>()).add(ex); for (var message : combined.messages()) exceptions.computeIfAbsent(new Receiver(receiver,message), k -> new ArrayList<>()).add(ex);
@@ -328,7 +330,7 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
var sender = message.sender().name(); var sender = message.sender().name();
var subject = message.subject(); var subject = message.subject();
var time = envelope.time().format(TIME_FORMATTER); var time = Instant.ofEpochMilli(message.utcTime()).atZone(ZoneId.systemDefault()).toLocalDateTime().format(TIME_FORMATTER);
var id = envelope.id(); var id = envelope.id();
return new JSONObject(Map.of(SENDER,sender,SUBJECT,subject,TIMESTAMP,time,ID,id)); return new JSONObject(Map.of(SENDER,sender,SUBJECT,subject,TIMESTAMP,time,ID,id));
} }

View File

@@ -14,11 +14,9 @@ import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.message.Constants.*; import static de.srsoftware.umbrella.message.Constants.*;
import static de.srsoftware.umbrella.message.model.Schedule.schedule; import static de.srsoftware.umbrella.message.model.Schedule.schedule;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.jdbc.Query; import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
@@ -162,7 +160,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
var messageId = rs.getLong(ID); var messageId = rs.getLong(ID);
var sender = userService().loadUser(rs.getLong(SENDER_USER_ID)); var sender = userService().loadUser(rs.getLong(SENDER_USER_ID));
var msg = new TranslatedMessage(sender,rs.getString(SUBJECT),rs.getString(BODY), attachments.get(messageId)); var msg = new TranslatedMessage(sender,rs.getString(SUBJECT),rs.getString(BODY), attachments.get(messageId));
var envelope = new Envelope<>(messageId, msg, user).time(Util.dateTimeOf(rs.getLong(TIMESTAMP))); var envelope = new Envelope<>(messageId, msg, user);
envelopes.add(envelope); envelopes.add(envelope);
} }
rs.close(); rs.close();
@@ -224,8 +222,8 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
@Override @Override
public void push(Envelope<TranslatedMessage> envelope) { public void push(Envelope<TranslatedMessage> envelope) {
var timestamp = envelope.time().toEpochSecond(UTC);
var message = envelope.message(); var message = envelope.message();
var timestamp = message.utcTime();
var sender = message.sender().id(); var sender = message.sender().id();
var subject = message.subject(); var subject = message.subject();
var body = message.body(); var body = message.body();

View File

@@ -1,12 +1,12 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model; package de.srsoftware.umbrella.message.model;
import static de.srsoftware.umbrella.core.Util.dateTimeOf;
import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE; import static java.lang.System.Logger.Level.TRACE;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.model.*; import de.srsoftware.umbrella.core.model.*;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
public class CombinedMessage { public class CombinedMessage {
@@ -20,7 +20,6 @@ public class CombinedMessage {
private final List<Message<?>> mergedMessages = new ArrayList<>(); private final List<Message<?>> mergedMessages = new ArrayList<>();
private final Translatable subjectForCombinedMessage; private final Translatable subjectForCombinedMessage;
private UmbrellaUser sender = null; private UmbrellaUser sender = null;
private static DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public CombinedMessage(Translatable subjectForCombinedMessage, User receiver){ public CombinedMessage(Translatable subjectForCombinedMessage, User receiver){
LOG.log(DEBUG,"Creating combined message for {0}…",receiver); LOG.log(DEBUG,"Creating combined message for {0}…",receiver);
@@ -29,9 +28,8 @@ public class CombinedMessage {
this.lang = receiver.language(); this.lang = receiver.language();
} }
public void merge(Envelope<?> envelope) { public void merge(Message<?> message) {
LOG.log(TRACE,"Merging {0} into combined message…",envelope); LOG.log(TRACE,"Merging {0} into combined message…",message);
var message = envelope.message();
if (message instanceof TranslatableMessage tm) message = tm.translate(lang); if (message instanceof TranslatableMessage tm) message = tm.translate(lang);
var body = message.body(); var body = message.body();
@@ -43,11 +41,11 @@ public class CombinedMessage {
combinedSubject = subject; combinedSubject = subject;
break; break;
case 1: case 1:
combinedBody.insert(0,format("# {0} @ {1}\n→ {2}:\n\n",sender,envelope.time().format(DT_FORMAT),subject)); // insert sender and subject of first message right before the body of the first message combinedBody.insert(0,format("# {0} @ {1}\n→ {2}:\n\n",sender, dateTimeOf(message.utcTime()),subject)); // insert sender and subject of first message right before the body of the first message
combinedSubject = subjectForCombinedMessage.translate(lang); combinedSubject = subjectForCombinedMessage.translate(lang);
// no break here, we need to append the subject and content // no break here, we need to append the subject and content
default: default:
combinedBody.append("\n\n━━━━━━━━━━━━━━━━━━━━━\n\n# ").append(message.sender()).append(" @ ").append(envelope.time().format(DT_FORMAT)).append("\n→ ").append(subject).append(":\n\n"); combinedBody.append("\n\n━━━━━━━━━━━━━━━━━━━━━\n\n# ").append(message.sender()).append(" @ ").append(dateTimeOf(message.utcTime())).append("\n→ ").append(subject).append(":\n\n");
combinedBody.append(body); combinedBody.append(body);
} }
if (message.attachments() != null) attachments.addAll(message.attachments()); if (message.attachments() != null) attachments.addAll(message.attachments());