preparing message system:

copied model from previous java implementation
This commit is contained in:
2025-07-04 14:21:14 +02:00
parent 3c898f36de
commit c934e19837
14 changed files with 538 additions and 6 deletions
@@ -0,0 +1,37 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
import static de.srsoftware.umbrella.core.Constants.*;
import de.srsoftware.umbrella.core.UmbrellaException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
import java.util.Set;
import org.json.JSONObject;
public record Attachment(String name, String mime, byte[] content) {
private static final Base64.Decoder BASE64 = Base64.getDecoder();
public static <T> Attachment of(JSONObject json) throws UmbrellaException {
for (var key : Set.of(NAME, MIME, DATA)) {
if (!json.has(key)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,key);
}
if (!(json.get(NAME) instanceof String name)) throw new UmbrellaException(400,ERROR_INVALID_FIELD, NAME,STRING);
if (!(json.get(MIME) instanceof String mime)) throw new UmbrellaException(400,ERROR_INVALID_FIELD, MIME,STRING);
if (!(json.get(DATA) instanceof String data)) throw new UmbrellaException(400,ERROR_INVALID_FIELD, DATA,STRING);
return new Attachment(name,mime, BASE64.decode(data));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Attachment that)) return false;
return Objects.equals(name, that.name) && Objects.equals(mime, that.mime) && Objects.deepEquals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(name, mime, Arrays.hashCode(content));
}
}
@@ -0,0 +1,70 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
public class CombinedMessage {
private static final System.Logger LOG = System.getLogger(CombinedMessage.class.getSimpleName());
private final Set<Attachment> attachments = new HashSet<>();
private final StringBuilder body = new StringBuilder();
private final List<Message> mergedMessages = new ArrayList<>();
private final Function<String, String> translate;
private UmbrellaUser sender = null;
private String subject = null;
public CombinedMessage(Function<String,String> translate){
LOG.log(DEBUG,"Creating combined message…");
this.translate = translate;
}
public void merge(Message message) {
LOG.log(TRACE,"Merging {0} into combined message…",message);
switch (mergedMessages.size()){
case 0:
body.append(message.body());
sender = message.sender();
subject = message.subject();
break;
case 1:
body.insert(0,format("# {0}:\n# {1}:\n\n",sender,subject)); // insert sender and subject of first message right before the body of the first message
subject = translate.apply("Collected messages");
// no break here, we need to append the subject and content
default:
body.append("\n\n# ").append(message.sender()).append(":\n");
body.append("# ").append(message.subject()).append(":\n\n");
body.append(message.body());
}
attachments.addAll(message.attachments());
mergedMessages.add(message);
}
public Set<Attachment> attachments() {
return attachments;
}
public String body() {
return body.toString();
}
public UmbrellaUser sender() {
return sender;
}
public String subject() {
return subject;
}
public List<Message> messages() {
return mergedMessages;
}
}
@@ -0,0 +1,57 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
import static de.srsoftware.umbrella.core.Constants.ERROR_INVALID_FIELD;
import static de.srsoftware.umbrella.core.Constants.ERROR_MISSING_FIELD;
import static de.srsoftware.umbrella.message.Constants.*;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.user.model.User;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.json.JSONArray;
import org.json.JSONObject;
public class Envelope {
private Message message;
private Set<User> receivers;
public Envelope(Message message, HashSet<User> receivers) {
this.message = message;
this.receivers = receivers;
}
public static Envelope from(JSONObject json) throws UmbrellaException {
if (!json.has(RECEIVERS)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,RECEIVERS);
var message = Message.from(json);
var obj = json.get(RECEIVERS);
if (obj instanceof JSONObject) obj = new JSONArray(List.of(obj));
if (!(obj instanceof JSONArray receiverList)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,RECEIVERS,JSONARRAY);
var receivers = new HashSet<User>();
for (var o : receiverList){
if (!(o instanceof JSONObject receiverData)) throw new UmbrellaException(400,ERROR_INVALID_FIELD,"entries of "+RECEIVERS,JSONOBJECT);
receivers.add(User.of(receiverData));
}
return new Envelope(message,receivers);
}
public boolean isFor(User receiver) {
return receivers.contains(receiver);
}
public Message message(){
return message;
}
public Set<User> receivers(){
return receivers;
}
@Override
public String toString() {
return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(User::email).collect(Collectors.joining(", ")),message.subject());
}
}
@@ -0,0 +1,53 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
import de.srsoftware.umbrella.user.model.User;
import java.util.*;
/**
* This maps recipient email addresses to the pending messages of that recipient
*/
public class MailQueue extends ArrayList<Envelope>{
private record Receiver(User user, Message message){}
private final HashMap<Receiver,List<Exception>> exceptions = new HashMap<>();
public interface Listener{
public void messagesAdded();
public void setQueue(MailQueue queue);
}
private final Set<Listener> listeners = new HashSet<>();
public void addListener(Listener listener) {
listeners.add(listener);
listener.setQueue(this);
}
public void commit() {
listeners.forEach(Listener::messagesAdded);
}
public List<Envelope> envelopesFor(User recv) {
return stream().filter(env -> env.isFor(recv)).toList();
}
public void failedAt(User receiver, CombinedMessage combined, Exception ex) {
for (var message : combined.messages()) exceptions.computeIfAbsent(new Receiver(receiver,message), k -> new ArrayList<>()).add(ex);
}
/**
* return the email addresses of the recipients of all messages in the queue
*
* @return a list of email addresses
*/
public List<User> receivers() {
return stream().map(Envelope::receivers)
.flatMap(Set::stream)
.filter(Objects::nonNull)
.distinct()
.toList();
}
}
@@ -0,0 +1,60 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.message.Constants.*;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.UmbrellaException;
import de.srsoftware.umbrella.user.model.UmbrellaUser;
import de.srsoftware.umbrella.user.model.User;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONObject;
public record Message(UmbrellaUser sender, String subject, String body, List<Attachment> attachments) {
public static Message from(JSONObject json) throws UmbrellaException {
for (var key : Set.of(SENDER, SUBJECT, BODY)) {
if (!json.has(key)) throw new UmbrellaException(400,ERROR_MISSING_FIELD,key);
}
if (!(json.get(SENDER) instanceof JSONObject senderObject)) throw new UmbrellaException(400,ERROR_INVALID_FIELD, SENDER,JSONOBJECT);
if (!(json.get(SUBJECT) instanceof String subject && isSet(subject))) throw new UmbrellaException(400,ERROR_INVALID_FIELD, SUBJECT,STRING);
if (!(json.get(BODY) instanceof String body && isSet(body))) throw new UmbrellaException(400,ERROR_INVALID_FIELD, BODY,STRING);
var user = User.of(senderObject);
if (!(user instanceof UmbrellaUser sender)) throw new UmbrellaException(400,"Sender is not an umbrella user!");
var attachments = new ArrayList<Attachment>();
if (json.has(ATTACHMENTS)){
var jsonAttachments = json.get(ATTACHMENTS);
if (jsonAttachments instanceof JSONObject obj) jsonAttachments = new JSONArray(List.of(obj));
if (jsonAttachments instanceof JSONArray arr){
for (var att : arr){
if (!(att instanceof JSONObject o)) throw new UmbrellaException(400,"Attachments contains entry that is not an object: {}",att);
var attachment = Attachment.of(o);
attachments.add(attachment);
}
}
}
return new Message(sender,subject,body,attachments);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Message message)) return false;
return Objects.equals(sender, message.sender) && Objects.equals(subject, message.subject) && Objects.equals(body, message.body) && Objects.equals(attachments, message.attachments);
}
@Override
public int hashCode() {
return Objects.hash(subject, body, attachments);
}
@Override
public String toString() {
return format("{0}(from: {1}), subject: {2}",getClass().getSimpleName(),sender,subject);
}
}
@@ -0,0 +1,38 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
import static de.srsoftware.umbrella.message.Constants.SUBMISSION;
import de.srsoftware.tools.Mappable;
import java.util.Map;
import java.util.Set;
public record Settings(Set<Times> times) implements Mappable {
public enum Times{
INSTANTLY,
AT8,
AT10,
AT12,
AT14,
AT16,
AT18,
AT20;
public boolean matches(int hour){
if (this == INSTANTLY) return false;
return Integer.parseInt(toString().substring(2)) == hour;
}
}
public boolean sendAt(Integer scheduledHour) {
return times.contains(Times.INSTANTLY) || (scheduledHour != null && times.stream().anyMatch(time -> time.matches(scheduledHour)));
}
@Override
public Map<String, Object> toMap() {
return Map.of(SUBMISSION,times);
}
}