completed message persistence

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2026-01-26 17:08:09 +01:00
parent c12786971f
commit 21ae4024dc
7 changed files with 57 additions and 38 deletions

View File

@@ -10,9 +10,11 @@ import java.util.LinkedList;
public class EventQueue extends LinkedList<Event<?>> implements AutoCloseable, EventListener {
private final InetSocketAddress addr;
private final System.Logger log;
public EventQueue(InetSocketAddress addr){
this.addr = addr;
log = System.getLogger(addr.toString());
messageBus().register(this);
}
@@ -30,7 +32,7 @@ public class EventQueue extends LinkedList<Event<?>> implements AutoCloseable, E
@Override
public void onEvent(Event<?> event) {
System.getLogger(addr.toString()).log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr);
log.log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr);
add(event);
}
}

View File

@@ -22,7 +22,7 @@ import org.json.JSONObject;
public class Envelope<T extends Message<?>> {
private final T message;
private final Set<User> receivers;
private final LocalDateTime time;
private LocalDateTime time;
private final long id;
public Envelope(long id, T message, User receiver){
@@ -73,7 +73,7 @@ public class Envelope<T extends Message<?>> {
}
public boolean isFor(User receiver) {
return receivers.contains(receiver);
return receivers.stream().anyMatch(r -> r.equals(receiver));
}
public T message(){
@@ -84,6 +84,11 @@ public class Envelope<T extends Message<?>> {
return receivers;
}
public Envelope<T> time(LocalDateTime timestamp){
this.time = timestamp;
return this;
}
public LocalDateTime time(){
return time;
}

View File

@@ -25,11 +25,14 @@ public class UmbrellaUser extends User implements Mappable, Owner {
@Override
public boolean equals(Object o) {
if (!(o instanceof UmbrellaUser user)) return false;
return Objects.equals(email(), user.email())
&& Objects.equals(name(), user.name())
&& Objects.equals(id, user.id)
&& Objects.equals(language(), user.language());
if (o instanceof UmbrellaUser user) {
return Objects.equals(email(), user.email())
&& Objects.equals(name(), user.name())
&& Objects.equals(id, user.id)
&& Objects.equals(language(), user.language());
}
if (o instanceof User user) return user.equals(this);
return false;
}
@Override

View File

@@ -9,17 +9,17 @@
let messages = [];
let timer = null;
async function display(hash){
const url = api(`message/${hash}`);
async function display(id){
const url = api(`message/${id}`);
const res = await get(url);
if (res.ok){
yikes();
const json = await res.json();
if (timer) clearTimeout(timer);
for (let i in messages){
if (messages[i].hash == hash){
if (messages[i].id == id){
messages[i].body = json.body;
timer = setTimeout(() => {mark_read(hash)}, 2000);
timer = setTimeout(() => {mark_read(id)}, 2000);
} else {
delete messages[i].body;
}
@@ -40,13 +40,13 @@
}
}
async function mark_read(hash){
async function mark_read(id){
timer = null;
const url = api(`message/read/${hash}`);
const url = api(`message/read/${id}`);
const res = await patch(url);
if (res.ok) {
for (let i in messages){
if (messages[i].hash == hash) messages[i].read = true;
if (messages[i].id == id) messages[i].read = true;
}
yikes();
} else {
@@ -73,7 +73,7 @@
</thead>
<tbody>
{#each messages as message}
<tr class="message-{message.hash}" onclick={ev => display(message.hash)}>
<tr class="message-{message.id}" onclick={ev => display(message.id)}>
<td>
{#if message.read}{/if}
{message.timestamp}
@@ -82,7 +82,7 @@
<td>{message.subject}</td>
</tr>
{#if message.body}
<tr class="message-{message.hash} body">
<tr class="message-{message.id} body">
<td></td>
<td colspan="2">
<pre>{message.body.trim()}</pre>

View File

@@ -112,13 +112,13 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = ModuleRegistry.userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head){
var id = path.pop();
return switch (id){
case null -> listMessages(ex,user.get());
case SETTINGS -> getSettings(ex,user.get());
default -> {
try {
yield getMessage(ex,user.get(),Integer.parseInt(head));
yield getMessage(ex,user.get(),Integer.parseInt(id));
} catch (NumberFormatException ignored) {
}
@@ -152,8 +152,8 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
}
}
private boolean getMessage(HttpExchange ex, UmbrellaUser user, int hash) throws IOException {
var envelope = queue.getEnvelope(hash).filter(env -> env.isFor(user));
private boolean getMessage(HttpExchange ex, UmbrellaUser user, long id) throws IOException {
var envelope = queue.getEnvelope(id).filter(env -> env.isFor(user));
if (envelope.isPresent()) return sendMessage(ex, user, envelope.get());
return notFound(ex);
}
@@ -221,7 +221,7 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
try {
var envelopes = queue.getEnvelopesFor(receiver);
envelopes.stream().map(Envelope::message).forEach(combined::merge);
envelopes.stream().forEach(combined::merge);
send(combined,date);
envelopes.forEach(env -> queue.markRead(env.hashCode(),receiver));
} catch (Exception ex){
@@ -329,8 +329,8 @@ public class MessageSystem extends BaseHandler implements PostBox, EventListener
var sender = message.sender().name();
var subject = message.subject();
var time = envelope.time().format(TIME_FORMATTER);
var hash = envelope.hashCode();
return new JSONObject(Map.of(SENDER,sender,SUBJECT,subject,TIMESTAMP,time,HASH,hash));
var id = envelope.id();
return new JSONObject(Map.of(SENDER,sender,SUBJECT,subject,TIMESTAMP,time,ID,id));
}
}

View File

@@ -7,7 +7,6 @@ import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Errors.*;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR;
import static de.srsoftware.umbrella.core.constants.Constants.COUNT;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
@@ -19,6 +18,7 @@ import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
@@ -120,7 +120,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
rs.close();
var receivers = new ArrayList<User>();
rs = select(ALL).from(TABLE_ATTACHMENTS).where(MESSAGE_ID,equal(messageId)).exec(db);
rs = select(ALL).from(TABLE_RECEIVERS).where(MESSAGE_ID,equal(messageId)).exec(db);
while (rs.next()) receivers.add(new User(rs.getString(NAME),new EmailAddress(rs.getString(EMAIL)),null));
rs.close();
@@ -135,9 +135,8 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
if (envelope != null) return Optional.of(envelope);
return Optional.empty();
} catch (SQLException e) {
throw failedToLoadObject(Text.MESSAGE,id);
throw failedToLoadObject(Text.MESSAGE,messageId);
}
throw new UmbrellaException(HTTP_SERVER_ERROR,"{class}.getEnvelope({id}) not implemented!","class",getClass().getSimpleName(), ID ,id); // TODO
}
@Override
@@ -163,7 +162,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
var messageId = rs.getLong(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 envelope = new Envelope<>(messageId, msg, user);
var envelope = new Envelope<>(messageId, msg, user).time(Util.dateTimeOf(rs.getLong(TIMESTAMP)));
envelopes.add(envelope);
}
rs.close();
@@ -206,6 +205,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
@Override
public Optional<Envelope<TranslatedMessage>> markRead(long messageId, User user) {
try {
var envelope = getEnvelope(messageId);
Query.delete().from(TABLE_RECEIVERS).where(MESSAGE_ID,equal(messageId)).where(EMAIL,equal(user.email().toString())).execute(db);
var rs = select(COUNT).from(TABLE_RECEIVERS).where(MESSAGE_ID,equal(messageId)).exec(db);
Integer count = null;
@@ -216,7 +216,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
delete().from(TABLE_MESSAGES).where(ID,equal(messageId)).execute(db);
}
// TODO load message or fail
return envelope;
} catch (SQLException e) {
throw failedToDropObject(Text.RECEIVER);
}
@@ -239,8 +239,10 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} INTEGER PRIMARY KEY, {2} VARCHAR(255) NOT N
for (var receiver : envelope.receivers()){
insertInto(TABLE_RECEIVERS,MESSAGE_ID,EMAIL,NAME).values(messageId,receiver.email(),receiver.name()).execute(db).close();
}
for (var attachment : envelope.message().attachments()){
insertInto(TABLE_ATTACHMENTS,MESSAGE_ID,NAME,MIME,DATA).values(messageId,attachment.name(),attachment.mime(),attachment.content()).execute(db).close();
if (envelope.message().attachments() != null) {
for (var attachment : envelope.message().attachments()) {
insertInto(TABLE_ATTACHMENTS, MESSAGE_ID, NAME, MIME, DATA).values(messageId, attachment.name(), attachment.mime(), attachment.content()).execute(db).close();
}
}
} catch (SQLException e) {
throw failedToStoreObject(envelope).causedBy(e);

View File

@@ -6,6 +6,9 @@ import static java.lang.System.Logger.Level.TRACE;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.model.*;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class CombinedMessage {
@@ -14,20 +17,24 @@ public class CombinedMessage {
private final Set<Attachment> attachments = new HashSet<>();
private final StringBuilder combinedBody = new StringBuilder();
private final User receiver;
private final String lang;
private String combinedSubject = null;
private final List<Message<?>> mergedMessages = new ArrayList<>();
private final Translatable subjectForCombinedMessage;
private UmbrellaUser sender = null;
private static DateTimeFormatter DT_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public CombinedMessage(Translatable subjectForCombinedMessage, User receiver){
LOG.log(DEBUG,"Creating combined message for {0}…",receiver);
this.subjectForCombinedMessage = subjectForCombinedMessage;
this.receiver = receiver;
this.lang = receiver.language();
}
public void merge(Message<?> message) {
LOG.log(TRACE,"Merging {0} into combined message…",message);
var lang = receiver.language();
public void merge(Envelope<?> envelope) {
LOG.log(TRACE,"Merging {0} into combined message…",envelope);
var message = envelope.message();
if (message instanceof TranslatableMessage tm) message = tm.translate(lang);
var body = message.body();
var subject = message.subject().toString();
@@ -38,11 +45,11 @@ public class CombinedMessage {
combinedSubject = subject;
break;
case 1:
combinedBody.insert(0,format("# {0} / {1}:\n\n",sender,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,envelope.time().format(DT_FORMAT),subject)); // insert sender and subject of first message right before the body of the first message
combinedSubject = subjectForCombinedMessage.translate(lang);
// no break here, we need to append the subject and content
default:
combinedBody.append("\n\n━━━━━━━━━━━━━━━━━━━━━\n\n# ").append(message.sender()).append(" / ").append(subject).append(":\n\n");
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(body);
}
if (message.attachments() != null) attachments.addAll(message.attachments());