preparing message system:
copied model from previous java implementation
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.message;
|
||||
|
||||
public class Constants {
|
||||
public static final String AUTH = "mail.smtp.auth";
|
||||
public static final String ENVELOPE_FROM = "mail.smtp.from";
|
||||
public static final String FIELD_MESSAGES = "messages";
|
||||
public static final String FIELD_HOST = "host";
|
||||
public static final String FIELD_PORT = "port";
|
||||
public static final String HOST = "mail.smtp.host";
|
||||
public static final String JSONARRAY = "json array";
|
||||
public static final String JSONOBJECT = "json object";
|
||||
public static final String PORT = "mail.smtp.port";
|
||||
public static final String RECEIVERS = "receivers";
|
||||
public static final String SSL = "mail.smtp.ssl.enable";
|
||||
public static final String SUBMISSION = "submission";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.message;
|
||||
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.message.model.Settings;
|
||||
import de.srsoftware.umbrella.user.model.UmbrellaUser;
|
||||
|
||||
public interface MessageDb {
|
||||
public Settings getSettings(UmbrellaUser user) throws UmbrellaException;
|
||||
public Settings update(UmbrellaUser user, Settings settings) throws UmbrellaException;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.message;
|
||||
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.message.model.Settings;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static de.srsoftware.tools.jdbc.Condition.equal;
|
||||
import static de.srsoftware.tools.jdbc.Query.*;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.message.model.Settings.Times;
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
public class SqliteDb implements MessageDb{
|
||||
private static final System.Logger LOG = System.getLogger(SqliteDb.class.getSimpleName());
|
||||
private final Connection db;
|
||||
private static final String DB_VERSION = "message_db_version";
|
||||
private static final int INITIAL_DB_VERSION = 1;
|
||||
private static final String TABLE_SUBMISSIONS = "message_submission";
|
||||
|
||||
public SqliteDb(Connection conn){
|
||||
db = conn;
|
||||
init();
|
||||
}
|
||||
|
||||
private void createSubmissionTable() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} ( {1} Integer PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
|
||||
""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_SUBMISSIONS, USER_ID, VALUE));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SUBMISSIONS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int createSettingsTable() {
|
||||
var createTable = """
|
||||
CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL);
|
||||
""";
|
||||
try {
|
||||
var stmt = db.prepareStatement(format(createTable,TABLE_SETTINGS, KEY, VALUE));
|
||||
stmt.execute();
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Integer version = null;
|
||||
try {
|
||||
var rs = select(VALUE).from(TABLE_SETTINGS).where(KEY, equal(DB_VERSION)).exec(db);
|
||||
if (rs.next()) version = rs.getInt(VALUE);
|
||||
rs.close();
|
||||
if (version == null) {
|
||||
version = INITIAL_DB_VERSION;
|
||||
insertInto(TABLE_SETTINGS, KEY, VALUE).values(DB_VERSION,version).execute(db).close();
|
||||
}
|
||||
|
||||
return version;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,DB_VERSION,TABLE_SETTINGS,e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private int createTables() {
|
||||
createSubmissionTable();
|
||||
return createSettingsTable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings getSettings(UmbrellaUser user) throws UmbrellaException {
|
||||
try {
|
||||
Settings settings = null;
|
||||
var rs = select(VALUE).from(TABLE_SUBMISSIONS).where(USER_ID,equal(user.id())).exec(db);
|
||||
if (rs.next()) settings = toSettings(rs);
|
||||
rs.close();
|
||||
if (settings != null) return settings;
|
||||
throw new UmbrellaException(500,"No submission settings stored for {0}",user);
|
||||
} catch (SQLException e) {
|
||||
LOG.log(ERROR,"Failed to read settings for {0} from {1} table",user,TABLE_SUBMISSIONS,e);
|
||||
throw new UmbrellaException(500,"Failed to read settings for {0} from {1} table",user,TABLE_SUBMISSIONS).causedBy(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void init() {
|
||||
var version = createTables();
|
||||
}
|
||||
|
||||
private Settings toSettings(ResultSet rs) throws SQLException {
|
||||
var submission = rs.getString(VALUE);
|
||||
var parts = submission.split(",");
|
||||
var times = new HashSet<Times>();
|
||||
for (var part : parts) try {
|
||||
times.add(Times.valueOf(part));
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.log(WARNING,"encountered {0}, which is not a valid Times enumeration value!",part);
|
||||
}
|
||||
return new Settings(times);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings update(UmbrellaUser user, Settings settings) throws UmbrellaException {
|
||||
var times = settings.times().stream().map(Times::toString).collect(Collectors.joining(","));
|
||||
try {
|
||||
replaceInto(TABLE_SUBMISSIONS, USER_ID, VALUE).values(user.id(),times).execute(db).close();
|
||||
return settings;
|
||||
} catch (SQLException e) {
|
||||
LOG.log(WARNING,"Failed to store submission data for {0}!",user.name(),e);
|
||||
throw new UmbrellaException(500,"Failed to store submission data for {0}!",user.name()).causedBy(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user