14 changed files with 538 additions and 6 deletions
@ -0,0 +1,14 @@ |
|||||||
|
description = "Umbrella : Message subsystem" |
||||||
|
|
||||||
|
dependencies{ |
||||||
|
implementation(project(":core")) |
||||||
|
implementation(project(":user")) |
||||||
|
implementation("de.srsoftware:configuration.api:1.0.2") |
||||||
|
implementation("de.srsoftware:tools.jdbc:1.3.2") |
||||||
|
implementation("de.srsoftware:tools.mime:1.1.2") |
||||||
|
implementation("de.srsoftware:tools.optionals:1.0.0") |
||||||
|
implementation("de.srsoftware:tools.util:2.0.3") |
||||||
|
implementation("org.bitbucket.b_c:jose4j:0.9.6") |
||||||
|
implementation("org.json:json:20240303") |
||||||
|
implementation("org.xerial:sqlite-jdbc:3.49.0.0") |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -1,9 +1,9 @@ |
|||||||
rootProject.name = "Umbrella25" |
rootProject.name = "Umbrella25" |
||||||
|
|
||||||
include("backend") |
include("backend") |
||||||
|
include("core") |
||||||
|
include("legacy") |
||||||
|
include("messages") |
||||||
include("translations") |
include("translations") |
||||||
include("user") |
include("user") |
||||||
include("web") |
include("web") |
||||||
|
|
||||||
include("core") |
|
||||||
include("legacy") |
|
||||||
Loading…
Reference in new issue