Compare commits

18 Commits

Author SHA1 Message Date
b653652f6d Fix chat default pictures not being updated on theme change 2020-11-22 12:26:08 +01:00
0ff910ebde Merge pull request 'Improve Scene Switching' (#109) from improved-scene-switching into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/109
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2020-11-22 11:19:41 +01:00
6d85e337d2 Remove fixed size support from SceneContext 2020-11-22 11:11:48 +01:00
67ebc6be83 Initialize scene with stage size in SceneContext
This apparently fixes the rendering issues when switching scenes, while
keeping the stage size constant (unless the user resizes the stage).
2020-11-20 14:01:00 +01:00
e3052a2133 Reuse the same scene in SceneContext by switching root nodes 2020-11-06 17:27:54 +01:00
4d4865570d Make resizability a property of SceneInfo
This removes a check hard coded into SceneContext that sets LoginScene
to not resizable.
2020-11-06 09:21:59 +01:00
0ce8b0c89d Move SceneInfo to separate file 2020-11-06 08:58:13 +01:00
cd7793a589 Merge pull request 'Add Local Account Deletion' (#108) from f/account-deletion into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/108
Reviewed-by: kske <kai@kske.dev>
2020-10-31 16:57:09 +01:00
e5659c1da1 Remove account deletion on the server 2020-10-31 16:57:26 +01:00
f67ca1d61d Add option to delete your account 2020-10-31 16:56:31 +01:00
8bdd201b28 Add Ctrl+U and Ctrl+K shortcuts to ChatScene 2020-10-31 16:54:14 +01:00
f6c772a655 Merge pull request 'Made Server Less Error Prone' (#107) from f/secure-server into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/107
Reviewed-by: kske <kai@kske.dev>
2020-10-31 16:30:44 +01:00
7a883861be Apply suggestions by @kske 2020-10-30 12:07:56 +01:00
d4c7813c97 Fix unnecessary authentication token being sent in requests 2020-10-23 18:45:40 +02:00
889e9b186f Merge pull request 'Display Current User Status and Unread Message Amount in Status Tray Icon' (#103) from f/enhanced-status-tray-icon into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/103
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-10-23 17:19:43 +02:00
fccd7e70b1 Disable crashing the server when Hibernate panics after oopsing 2020-10-23 00:15:37 +02:00
2eeb55ed52 Add client side errors in case of data initialization with null values 2020-10-22 23:58:55 +02:00
44d3082958 Fix bug allowing unauthorized access to a client
Additionally token authentication is now used whenever the client is
online
2020-10-22 23:05:51 +02:00
50 changed files with 528 additions and 1053 deletions

View File

@ -21,7 +21,6 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
<attribute name="module" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">

View File

@ -22,20 +22,21 @@ import envoy.client.net.WriteProxy;
*/ */
public class Chat implements Serializable { public class Chat implements Serializable {
protected boolean disabled; protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected final Contact recipient;
protected boolean disabled;
protected boolean underlyingContactDeleted;
/** /**
* Stores the last time an {@link envoy.event.IsTyping} event has been sent. * Stores the last time an {@link envoy.event.IsTyping} event has been sent.
*/ */
protected transient long lastWritingEvent; protected transient long lastWritingEvent;
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount; protected int unreadAmount;
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty(); protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
protected final Contact recipient;
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
/** /**

View File

@ -248,6 +248,10 @@ public final class LocalDB implements EventListener {
*/ */
@Event(eventType = EnvoyCloseEvent.class, priority = 500) @Event(eventType = EnvoyCloseEvent.class, priority = 500)
private synchronized void save() { private synchronized void save() {
// Stop saving if this account has been deleted
if (userFile == null)
return;
EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database..."); EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database...");
// Save users // Save users
@ -273,6 +277,29 @@ public final class LocalDB implements EventListener {
} }
} }
/**
* Deletes any local remnant of this user.
*
* @since Envoy Client v0.3-beta
*/
public void delete() {
try {
// Save ID generator - can be used for other users in that db
if (hasIDGenerator())
SerializationUtils.write(idGeneratorFile, idGenerator);
} catch (final IOException e) {
EnvoyLog.getLogger(LocalDB.class).log(Level.SEVERE, "Unable to save local database: ",
e);
}
if (lastLoginFile != null)
lastLoginFile.delete();
userFile.delete();
users.remove(user.getName());
userFile = null;
onLogout();
}
@Event(priority = 500) @Event(priority = 500)
private void onMessage(Message msg) { private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT) if (msg.getStatus() == MessageStatus.SENT)
@ -404,6 +431,14 @@ public final class LocalDB implements EventListener {
getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true));
} }
@Event(priority = 500)
private void onAccountDeletion(AccountDeletion deletion) {
if (user.getID() == deletion.get())
logger.log(Level.WARNING,
"I have been informed by the server that I have been deleted without even knowing it...");
getChat(deletion.get()).ifPresent(chat -> chat.setDisabled(true));
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their user names as keys * @return a {@code Map<String, User>} of all users stored locally with their user names as keys
* @since Envoy Client v0.2-alpha * @since Envoy Client v0.2-alpha

View File

@ -6,7 +6,7 @@ import envoy.data.User.UserStatus;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneInfo;
import envoy.client.util.UserUtil; import envoy.client.util.UserUtil;
/** /**

View File

@ -4,7 +4,7 @@ import java.util.*;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneInfo;
/** /**
* Contains all keyboard shortcuts used throughout the application. * Contains all keyboard shortcuts used throughout the application.

View File

@ -0,0 +1,22 @@
package envoy.client.event;
import envoy.event.Event;
/**
* Signifies the deletion of an account.
*
* @author Leon Hofmeister
* @since Envoy Common v0.3-beta
*/
public class AccountDeletion extends Event<Long> {
private static final long serialVersionUID = 1L;
/**
* @param value the ID of the contact that was deleted
* @since Envoy Common v0.3-beta
*/
public AccountDeletion(Long value) {
super(value);
}
}

View File

@ -4,7 +4,6 @@ import java.io.IOException;
import java.util.Stack; import java.util.Stack;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.*; import javafx.scene.*;
import javafx.stage.Stage; import javafx.stage.Stage;
@ -28,51 +27,11 @@ import envoy.client.event.*;
*/ */
public final class SceneContext implements EventListener { public final class SceneContext implements EventListener {
/**
* Contains information about different scenes and their FXML resource files.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public enum SceneInfo {
/**
* The main scene in which the chat screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CHAT_SCENE("/fxml/ChatScene.fxml"),
/**
* The scene in which the settings screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the login screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) {
this.path = path;
}
}
private final Stage stage; private final Stage stage;
private final FXMLLoader loader = new FXMLLoader(); private final Stack<Parent> roots = new Stack<>();
private final Stack<Scene> sceneStack = new Stack<>(); private final Stack<Object> controllers = new Stack<>();
private final Stack<Object> controllerStack = new Stack<>();
private static final Settings settings = Settings.getInstance(); private Scene scene;
/** /**
* Initializes the scene context. * Initializes the scene context.
@ -88,44 +47,44 @@ public final class SceneContext implements EventListener {
/** /**
* Loads a new scene specified by a scene info. * Loads a new scene specified by a scene info.
* *
* @param sceneInfo specifies the scene to load * @param info specifies the scene to load
* @throws RuntimeException if the loading process fails * @throws RuntimeException if the loading process fails
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void load(SceneInfo sceneInfo) { public void load(SceneInfo info) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo); EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + info);
loader.setRoot(null);
loader.setController(null);
try { try {
final var rootNode =
(Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode);
final var controller = loader.getController();
controllerStack.push(controller);
sceneStack.push(scene); // Load root node and controller
stage.setScene(scene); var loader = new FXMLLoader();
Parent root = loader.load(getClass().getResourceAsStream(info.path));
Object controller = loader.getController();
roots.push(root);
controllers.push(controller);
if (scene == null) {
// One-time scene initialization
scene = new Scene(root, stage.getWidth(), stage.getHeight());
applyCSS();
stage.setScene(scene);
} else {
scene.setRoot(root);
}
// Remove previous keyboard shortcuts
scene.getAccelerators().clear();
// Supply the global custom keyboard shortcuts for that scene // Supply the global custom keyboard shortcuts for that scene
scene.getAccelerators() scene.getAccelerators()
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo)); .putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(info));
// Supply the scene specific keyboard shortcuts // Supply the scene specific keyboard shortcuts
if (controller instanceof KeyboardMapping) if (controller instanceof KeyboardMapping)
scene.getAccelerators() scene.getAccelerators()
.putAll(((KeyboardMapping) controller).getKeyboardShortcuts()); .putAll(((KeyboardMapping) controller).getKeyboardShortcuts());
} catch (IOException e) {
// The LoginScene is the only scene not intended to be resized
// As strange as it seems, this is needed as otherwise the LoginScene won't be
// displayed on some OS (...Debian...)
stage.sizeToScene();
Platform.runLater(() -> stage.setResizable(sceneInfo != SceneInfo.LOGIN_SCENE));
applyCSS();
stage.show();
} catch (final IOException e) {
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE,
String.format("Could not load scene for %s: ", sceneInfo), e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@ -137,29 +96,30 @@ public final class SceneContext implements EventListener {
*/ */
public void pop() { public void pop() {
// Pop scene and controller // Pop current root node and controller
sceneStack.pop(); roots.pop();
controllerStack.pop(); controllers.pop();
// Apply new scene if present // Apply new scene if present
if (!sceneStack.isEmpty()) { if (!roots.isEmpty()) {
final var newScene = sceneStack.peek(); scene.setRoot(roots.peek());
stage.setScene(newScene);
applyCSS(); // Invoke restore if controller is restorable
stage.sizeToScene(); var controller = controllers.peek();
// If the controller implements the Restorable interface,
// the actions to perform on restoration will be executed here
final var controller = controllerStack.peek();
if (controller instanceof Restorable) if (controller instanceof Restorable)
((Restorable) controller).onRestore(); ((Restorable) controller).onRestore();
} else {
// Remove the current scene entirely
scene = null;
stage.setScene(null);
} }
stage.show();
} }
private void applyCSS() { private void applyCSS() {
if (!sceneStack.isEmpty()) { if (scene != null) {
final var styleSheets = stage.getScene().getStylesheets(); var styleSheets = scene.getStylesheets();
final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css"; var themeCSS = "/css/" + Settings.getInstance().getCurrentTheme() + ".css";
styleSheets.clear(); styleSheets.clear();
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(),
getClass().getResource(themeCSS).toExternalForm()); getClass().getResource(themeCSS).toExternalForm());
@ -168,8 +128,8 @@ public final class SceneContext implements EventListener {
@Event(eventType = Logout.class, priority = 150) @Event(eventType = Logout.class, priority = 150)
private void onLogout() { private void onLogout() {
sceneStack.clear(); roots.clear();
controllerStack.clear(); controllers.clear();
} }
@Event(priority = 150, eventType = ThemeChangeEvent.class) @Event(priority = 150, eventType = ThemeChangeEvent.class)
@ -182,7 +142,7 @@ public final class SceneContext implements EventListener {
* @return the controller used by the current scene * @return the controller used by the current scene
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public <T> T getController() { return (T) controllerStack.peek(); } public <T> T getController() { return (T) controllers.peek(); }
/** /**
* @return the stage in which the scenes are displayed * @return the stage in which the scenes are displayed
@ -194,5 +154,5 @@ public final class SceneContext implements EventListener {
* @return whether the scene stack is empty * @return whether the scene stack is empty
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public boolean isEmpty() { return sceneStack.isEmpty(); } public boolean isEmpty() { return roots.isEmpty(); }
} }

View File

@ -0,0 +1,40 @@
package envoy.client.ui;
/**
* Contains information about different scenes and their FXML resource files.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public enum SceneInfo {
/**
* The main scene in which the chat screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CHAT_SCENE("/fxml/ChatScene.fxml"),
/**
* The scene in which the settings screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the login screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) {
this.path = path;
}
}

View File

@ -20,7 +20,6 @@ import envoy.client.data.*;
import envoy.client.data.shortcuts.EnvoyShortcutConfig; import envoy.client.data.shortcuts.EnvoyShortcutConfig;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene; import envoy.client.ui.controller.LoginScene;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
@ -94,7 +93,7 @@ public final class Startup extends Application {
final var sceneContext = new SceneContext(stage); final var sceneContext = new SceneContext(stage);
context.setSceneContext(sceneContext); context.setSceneContext(sceneContext);
// Authenticate with token if present // Authenticate with token if present or load login scene
if (localDB.getAuthToken() != null) { if (localDB.getAuthToken() != null) {
logger.info("Attempting authentication with token..."); logger.info("Attempting authentication with token...");
localDB.loadUserData(); localDB.loadUserData();
@ -103,8 +102,9 @@ public final class Startup extends Application {
VERSION, localDB.getLastSync()))) VERSION, localDB.getLastSync())))
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
} else } else
// Load login scene
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
stage.show();
} }
/** /**
@ -226,7 +226,7 @@ public final class Startup extends Application {
// Load ChatScene // Load ChatScene
stage.setMinHeight(400); stage.setMinHeight(400);
stage.setMinWidth(843); stage.setMinWidth(843);
context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE); context.getSceneContext().load(SceneInfo.CHAT_SCENE);
stage.centerOnScreen(); stage.centerOnScreen();
// Exit or minimize the stage when a close request occurs // Exit or minimize the stage when a close request occurs

View File

@ -15,7 +15,7 @@ import envoy.util.EnvoyLog;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.data.commands.*; import envoy.client.data.commands.*;
import envoy.client.helper.ShutdownHelper; import envoy.client.helper.ShutdownHelper;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneInfo;
import envoy.client.ui.controller.ChatScene; import envoy.client.ui.controller.ChatScene;
import envoy.client.util.*; import envoy.client.util.*;
@ -32,7 +32,7 @@ public final class ChatSceneCommands {
private final SystemCommandBuilder builder = private final SystemCommandBuilder builder =
new SystemCommandBuilder(messageTextAreaCommands); new SystemCommandBuilder(messageTextAreaCommands);
private static final String messageDependantCommandDescription = private static final String messageDependentCommandDescription =
" the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message."; " the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message.";
/** /**
@ -141,7 +141,7 @@ public final class ChatSceneCommands {
else else
useRelativeMessage(command, action, additionalCheck, positionalArgument, false); useRelativeMessage(command, action, additionalCheck, positionalArgument, false);
}).setDefaults("s").setNumberOfArguments(1) }).setDefaults("s").setNumberOfArguments(1)
.setDescription(description.concat(messageDependantCommandDescription)).build(command); .setDescription(description.concat(messageDependentCommandDescription)).build(command);
} }
private void selectionNeighbor(Consumer<Message> action, Predicate<Message> additionalCheck, private void selectionNeighbor(Consumer<Message> action, Predicate<Message> additionalCheck,

View File

@ -18,7 +18,7 @@ import envoy.client.util.IconUtil;
*/ */
public final class ChatControl extends HBox { public final class ChatControl extends HBox {
private static final Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32), private static Image userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32),
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32); groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
/** /**
@ -60,4 +60,14 @@ public final class ChatControl extends HBox {
} }
getStyleClass().add("list-element"); getStyleClass().add("list-element");
} }
/**
* Reloads the default icons.
*
* @since Envoy Client v0.3-beta
*/
public static void reloadDefaultChatIcons() {
userIcon = IconUtil.loadIconThemeSensitive("user_icon", 32);
groupIcon = IconUtil.loadIconThemeSensitive("group_icon", 32);
}
} }

View File

@ -1,11 +1,14 @@
package envoy.client.ui.controller; package envoy.client.ui.controller;
import static envoy.client.ui.SceneInfo.SETTINGS_SCENE;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.*; import java.util.logging.*;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
@ -37,6 +40,7 @@ import envoy.util.EnvoyLog;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.net.*; import envoy.client.net.*;
import envoy.client.ui.*; import envoy.client.ui.*;
@ -51,7 +55,7 @@ import envoy.client.util.*;
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public final class ChatScene implements EventListener, Restorable { public final class ChatScene implements EventListener, Restorable, KeyboardMapping {
@FXML @FXML
private ListView<Message> messageList; private ListView<Message> messageList;
@ -320,6 +324,7 @@ public final class ChatScene implements EventListener, Restorable {
@Event(eventType = ThemeChangeEvent.class) @Event(eventType = ThemeChangeEvent.class)
private void onThemeChange() { private void onThemeChange() {
ChatControl.reloadDefaultChatIcons();
settingsButton.setGraphic( settingsButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic( voiceButton.setGraphic(
@ -346,6 +351,11 @@ public final class ChatScene implements EventListener, Restorable {
eventBus.removeListener(this); eventBus.removeListener(this);
} }
@Event(eventType = AccountDeletion.class)
private void onAccountDeletion() {
Platform.runLater(chatList::refresh);
}
@Override @Override
public void onRestore() { public void onRestore() {
updateRemainingCharsLabel(); updateRemainingCharsLabel();
@ -438,7 +448,7 @@ public final class ChatScene implements EventListener, Restorable {
*/ */
@FXML @FXML
private void settingsButtonClicked() { private void settingsButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE); sceneContext.load(SETTINGS_SCENE);
} }
/** /**
@ -586,7 +596,7 @@ public final class ChatScene implements EventListener, Restorable {
// IsTyping#millisecondsActive // IsTyping#millisecondsActive
if (client.isOnline() && currentChat.getLastWritingEvent() if (client.isOnline() && currentChat.getLastWritingEvent()
+ IsTyping.millisecondsActive <= System.currentTimeMillis()) { + IsTyping.millisecondsActive <= System.currentTimeMillis()) {
client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID())); client.send(new IsTyping(currentChat.getRecipient().getID()));
currentChat.lastWritingEventWasNow(); currentChat.lastWritingEventWasNow();
} }
@ -600,19 +610,6 @@ public final class ChatScene implements EventListener, Restorable {
checkPostConditions(false); checkPostConditions(false);
} }
/**
* Returns the id that should be used to send things to the server: the id of 'our' {@link User}
* if the recipient of that object is another User, else the id of the {@link Group} 'our' user
* is sending to.
*
* @return an id that can be sent to the server
* @since Envoy Client v0.2-beta
*/
private long getChatID() {
return currentChat.getRecipient() instanceof User ? client.getSender().getID()
: currentChat.getRecipient().getID();
}
/** /**
* @param e the keys that have been pressed * @param e the keys that have been pressed
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
@ -884,4 +881,25 @@ public final class ChatScene implements EventListener, Restorable {
: c -> c.getRecipient().getName().toLowerCase() : c -> c.getRecipient().getName().toLowerCase()
.contains(contactSearch.getText().toLowerCase())); .contains(contactSearch.getText().toLowerCase()));
} }
@Override
public Map<KeyCombination, Runnable> getKeyboardShortcuts() {
return Map.<KeyCombination, Runnable>of(
// Delete text before the caret with "Control" + U
new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea
.setText(
messageTextArea.getText().substring(messageTextArea.getCaretPosition()));
checkPostConditions(false);
// Delete text after the caret with "Control" + K
}, new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN), () -> {
messageTextArea
.setText(
messageTextArea.getText().substring(0, messageTextArea.getCaretPosition()));
checkPostConditions(false);
messageTextArea.positionCaret(messageTextArea.getText().length());
});
}
} }

View File

@ -14,11 +14,11 @@ import dev.kske.eventbus.*;
import envoy.data.*; import envoy.data.*;
import envoy.event.GroupCreation; import envoy.event.GroupCreation;
import envoy.event.contact.UserOperation; import envoy.event.contact.*;
import envoy.util.Bounds; import envoy.util.Bounds;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.BackEvent; import envoy.client.event.*;
import envoy.client.ui.control.*; import envoy.client.ui.control.*;
import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.ListCellFactory;
@ -252,4 +252,10 @@ public class GroupCreationTab implements EventListener {
} }
}); });
} }
@Event
private void onAccountDeletion(AccountDeletion deletion) {
final var deletedID = deletion.get();
Platform.runLater(() -> userList.getItems().removeIf(user -> (user.getID() == deletedID)));
}
} }

View File

@ -13,6 +13,7 @@ import javafx.scene.image.*;
import javafx.scene.input.InputEvent; import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -20,7 +21,7 @@ import envoy.event.*;
import envoy.util.*; import envoy.util.*;
import envoy.client.ui.control.ProfilePicImageView; import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil; import envoy.client.util.*;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -38,6 +39,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
private final PasswordField newPasswordField = new PasswordField(); private final PasswordField newPasswordField = new PasswordField();
private final PasswordField repeatNewPasswordField = new PasswordField(); private final PasswordField repeatNewPasswordField = new PasswordField();
private final Button saveButton = new Button("Save"); private final Button saveButton = new Button("Save");
private final Button deleteAccountButton = new Button("Delete Account (Locally)");
private static final EventBus eventBus = EventBus.getInstance(); private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class); private static final Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
@ -112,16 +114,19 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
final PasswordField[] passwordFields = final PasswordField[] passwordFields =
{ currentPasswordField, newPasswordField, repeatNewPasswordField }; { currentPasswordField, newPasswordField, repeatNewPasswordField };
final EventHandler<? super InputEvent> passwordEntered = e -> { final EventHandler<? super InputEvent> passwordEntered =
newPassword = e -> {
newPasswordField.getText(); newPassword =
validPassword = newPassword newPasswordField
.equals( .getText();
repeatNewPasswordField validPassword =
.getText()) newPassword.equals(
&& !newPasswordField repeatNewPasswordField
.getText().isBlank(); .getText())
}; && !newPasswordField
.getText()
.isBlank();
};
newPasswordField.setOnInputMethodTextChanged(passwordEntered); newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered); newPasswordField.setOnKeyTyped(passwordEntered);
repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered); repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered);
@ -137,9 +142,21 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// Displaying the save button // Displaying the save button
saveButton saveButton
.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText())); .setOnAction(e -> save(currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT); saveButton.setAlignment(Pos.BOTTOM_RIGHT);
getChildren().add(saveButton); getChildren().add(saveButton);
// Displaying the delete account button
deleteAccountButton.setAlignment(Pos.BASELINE_CENTER);
deleteAccountButton.setOnAction(e -> UserUtil.deleteAccount());
deleteAccountButton.setText("Delete Account (locally)");
deleteAccountButton.setPrefHeight(25);
deleteAccountButton.getStyleClass().clear();
deleteAccountButton.getStyleClass().add("danger-button");
final var tooltip = new Tooltip("Remote deletion is currently unsupported.");
tooltip.setShowDelay(Duration.millis(100));
deleteAccountButton.setTooltip(tooltip);
getChildren().add(deleteAccountButton);
} }
/** /**
@ -148,11 +165,11 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
* @param username the new username * @param username the new username
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private void save(long userID, String oldPassword) { private void save(String oldPassword) {
// The profile pic was changed // The profile pic was changed
if (profilePicChanged) { if (profilePicChanged) {
final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes, userID); final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes);
eventBus.dispatch(profilePicChangeEvent); eventBus.dispatch(profilePicChangeEvent);
client.send(profilePicChangeEvent); client.send(profilePicChangeEvent);
logger.log(Level.INFO, "The user just changed his profile pic."); logger.log(Level.INFO, "The user just changed his profile pic.");
@ -161,7 +178,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// The username was changed // The username was changed
final var validContactName = Bounds.isValidContactName(newUsername); final var validContactName = Bounds.isValidContactName(newUsername);
if (usernameChanged && validContactName) { if (usernameChanged && validContactName) {
final var nameChangeEvent = new NameChange(userID, newUsername); final var nameChangeEvent = new NameChange(client.getSender().getID(), newUsername);
eventBus.dispatch(nameChangeEvent); eventBus.dispatch(nameChangeEvent);
client.send(nameChangeEvent); client.send(nameChangeEvent);
logger.log(Level.INFO, "The user just changed his name to " + newUsername + "."); logger.log(Level.INFO, "The user just changed his name to " + newUsername + ".");
@ -178,7 +195,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// The password was changed // The password was changed
if (validPassword) { if (validPassword) {
client.send(new PasswordChangeRequest(newPassword, oldPassword, userID)); client.send(new PasswordChangeRequest(newPassword, oldPassword));
logger.log(Level.INFO, "The user just tried to change his password!"); logger.log(Level.INFO, "The user just tried to change his password!");
} else if (!(validPassword || newPassword.isBlank())) { } else if (!(validPassword || newPassword.isBlank())) {
final var alert = new Alert(AlertType.ERROR); final var alert = new Alert(AlertType.ERROR);

View File

@ -2,7 +2,7 @@ package envoy.client.util;
import java.util.logging.*; import java.util.logging.*;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -16,7 +16,7 @@ import envoy.util.EnvoyLog;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.helper.*; import envoy.client.helper.*;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneInfo;
import envoy.client.ui.controller.ChatScene; import envoy.client.ui.controller.ChatScene;
/** /**
@ -121,4 +121,35 @@ public final class UserUtil {
}); });
} }
} }
/**
* Deletes anything pointing to this user, independent of client or server. Will do nothing if
* the client is currently offline.
*
* @since Envoy Client v0.3-beta
*/
public static void deleteAccount() {
// Show the first wall of defense, if not disabled by the user
final var outerAlert = new Alert(AlertType.CONFIRMATION);
outerAlert.setContentText(
"Are you sure you want to delete your account entirely? This action can seriously not be undone.");
outerAlert.setTitle("Delete Account?");
AlertHelper.confirmAction(outerAlert, () -> {
// Show the final wall of defense in every case
final var lastAlert = new Alert(AlertType.WARNING,
"Do you REALLY want to delete your account? Last Warning. Proceed?",
ButtonType.CANCEL, ButtonType.OK);
lastAlert.setTitle("Delete Account?");
lastAlert.showAndWait().filter(ButtonType.OK::equals).ifPresent(b2 -> {
// Delete the account
// TODO: Notify server of account deletion
context.getLocalDB().delete();
logger.log(Level.INFO, "The user just deleted his account. Goodbye.");
ShutdownHelper.exit(true);
});
});
}
} }

View File

@ -70,6 +70,17 @@
-fx-text-fill: gray; -fx-text-fill: gray;
} }
.danger-button {
-fx-background-color: red;
-fx-text-fill: white;
-fx-background-radius: 0.2em;
}
.danger-button:hover {
-fx-scale-x: 1.05;
-fx-scale-y: 1.05;
}
.received-message { .received-message {
-fx-alignment: center-left; -fx-alignment: center-left;
-fx-background-radius: 1.3em; -fx-background-radius: 1.3em;

View File

@ -30,6 +30,10 @@
-fx-background-color: black; -fx-background-color: black;
} }
.tooltip {
-fx-text-fill: black;
}
#login-input-field { #login-input-field {
-fx-border-color: black; -fx-border-color: black;
} }

View File

@ -24,8 +24,8 @@
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" <GridPane maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="400.0" minWidth="500.0" minHeight="400.0" minWidth="500.0"
prefHeight="${screen.visualBounds.height}"
prefWidth="${screen.visualBounds.width}" prefWidth="${screen.visualBounds.width}"
prefHeight="${screen.visualBounds.height}"
xmlns="http://javafx.com/javafx/11.0.1" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.ChatScene"> fx:controller="envoy.client.ui.controller.ChatScene">
@ -57,8 +57,7 @@
<content> <content>
<AnchorPane minHeight="0.0" minWidth="0.0"> <AnchorPane minHeight="0.0" minWidth="0.0">
<children> <children>
<VBox prefHeight="3000.0" <VBox prefHeight="3000.0" prefWidth="316.0">
prefWidth="316.0">
<children> <children>
<VBox id="search-panel" maxHeight="-Infinity" <VBox id="search-panel" maxHeight="-Infinity"
minHeight="-Infinity" prefHeight="80.0" prefWidth="316.0"> minHeight="-Infinity" prefHeight="80.0" prefWidth="316.0">
@ -156,8 +155,7 @@
<Insets left="15.0" top="5.0" right="10.0" /> <Insets left="15.0" top="5.0" right="10.0" />
</HBox.margin> </HBox.margin>
</ImageView> </ImageView>
<Region id="transparent-background" <Region id="transparent-background" HBox.hgrow="ALWAYS" />
HBox.hgrow="ALWAYS" />
<Button fx:id="settingsButton" mnemonicParsing="false" <Button fx:id="settingsButton" mnemonicParsing="false"
onAction="#settingsButtonClicked" prefHeight="30.0" onAction="#settingsButtonClicked" prefHeight="30.0"
prefWidth="30.0" alignment="CENTER_RIGHT"> prefWidth="30.0" alignment="CENTER_RIGHT">
@ -165,7 +163,7 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
<HBox.margin> <HBox.margin>
<Insets bottom="35.0" left="5.0" top="35.0" right="10.0"/> <Insets bottom="35.0" left="5.0" top="35.0" right="10.0" />
</HBox.margin> </HBox.margin>
</Button> </Button>
</children> </children>

View File

@ -7,11 +7,16 @@
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox alignment="TOP_RIGHT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="envoy.client.ui.controller.SettingsScene"> <VBox alignment="TOP_RIGHT" maxHeight="-Infinity" minHeight="400.0"
minWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.SettingsScene">
<children> <children>
<HBox prefHeight="389.0" prefWidth="600.0"> <HBox prefHeight="389.0" prefWidth="600.0">
<children> <children>
<ListView id="message-list" fx:id="settingsList" onMouseClicked="#settingsListClicked" prefHeight="200.0" prefWidth="200.0"> <ListView id="message-list" fx:id="settingsList"
onMouseClicked="#settingsListClicked" prefHeight="200.0"
prefWidth="200.0">
<opaqueInsets> <opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</opaqueInsets> </opaqueInsets>
@ -22,7 +27,8 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding> </padding>
</ListView> </ListView>
<TitledPane fx:id="titledPane" collapsible="false" prefHeight="400.0" prefWidth="400.0"> <TitledPane fx:id="titledPane" collapsible="false"
prefHeight="400.0" prefWidth="400.0">
<HBox.margin> <HBox.margin>
<Insets bottom="10.0" left="5.0" right="10.0" top="10.0" /> <Insets bottom="10.0" left="5.0" right="10.0" top="10.0" />
</HBox.margin> </HBox.margin>
@ -32,7 +38,8 @@
</TitledPane> </TitledPane>
</children> </children>
</HBox> </HBox>
<Button defaultButton="true" mnemonicParsing="true" onMouseClicked="#backButtonClicked" text="_Back"> <Button defaultButton="true" mnemonicParsing="true"
onMouseClicked="#backButtonClicked" text="_Back">
<opaqueInsets> <opaqueInsets>
<Insets /> <Insets />
</opaqueInsets> </opaqueInsets>

View File

@ -1,6 +1,7 @@
package envoy.data; package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
/** /**
* This interface should be used for any type supposed to be a {@link Message} attachment (i.e. * This interface should be used for any type supposed to be a {@link Message} attachment (i.e.
@ -63,9 +64,9 @@ public final class Attachment implements Serializable {
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public Attachment(byte[] data, String name, AttachmentType type) { public Attachment(byte[] data, String name, AttachmentType type) {
this.data = data; this.data = Objects.requireNonNull(data);
this.name = name; this.name = Objects.requireNonNull(name);
this.type = type; this.type = Objects.requireNonNull(type);
} }
/** /**

View File

@ -29,8 +29,8 @@ public abstract class Contact implements Serializable {
*/ */
public Contact(long id, String name, Set<? extends Contact> contacts) { public Contact(long id, String name, Set<? extends Contact> contacts) {
this.id = id; this.id = id;
this.name = name; this.name = Objects.requireNonNull(name);
this.contacts = contacts; this.contacts = contacts == null ? new HashSet<>() : contacts;
} }
/** /**

View File

@ -38,7 +38,8 @@ public final class GroupMessage extends Message {
Map<Long, MessageStatus> memberStatuses) { Map<Long, MessageStatus> memberStatuses) {
super(id, senderID, groupID, creationDate, receivedDate, readDate, text, attachment, status, super(id, senderID, groupID, creationDate, receivedDate, readDate, text, attachment, status,
forwarded); forwarded);
this.memberStatuses = memberStatuses; this.memberStatuses =
memberStatuses == null ? new HashMap<>() : memberStatuses;
} }
/** /**

View File

@ -2,6 +2,7 @@ package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
/** /**
* Contains a {@link User}'s login / registration information as well as the client version. * Contains a {@link User}'s login / registration information as well as the client version.
@ -20,15 +21,14 @@ public final class LoginCredentials implements Serializable {
private static final long serialVersionUID = 4; private static final long serialVersionUID = 4;
private LoginCredentials(String identifier, String password, boolean registration, private LoginCredentials(String identifier, String password, boolean registration,
boolean token, boolean requestToken, String clientVersion, boolean token, boolean requestToken, String clientVersion, Instant lastSync) {
Instant lastSync) { this.identifier = Objects.requireNonNull(identifier);
this.identifier = identifier; this.password = Objects.requireNonNull(password);
this.password = password;
this.registration = registration; this.registration = registration;
this.token = token; this.token = token;
this.requestToken = requestToken; this.requestToken = requestToken;
this.clientVersion = clientVersion; this.clientVersion = Objects.requireNonNull(clientVersion);
this.lastSync = lastSync; this.lastSync = lastSync == null ? Instant.EPOCH : lastSync;
} }
/** /**
@ -75,7 +75,8 @@ public final class LoginCredentials implements Serializable {
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public static LoginCredentials registration(String identifier, String password, public static LoginCredentials registration(String identifier, String password,
boolean requestToken, String clientVersion, Instant lastSync) { boolean requestToken,
String clientVersion, Instant lastSync) {
return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion, return new LoginCredentials(identifier, password, true, false, requestToken, clientVersion,
lastSync); lastSync);
} }

View File

@ -2,6 +2,7 @@ package envoy.data;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import dev.kske.eventbus.IEvent; import dev.kske.eventbus.IEvent;
@ -80,9 +81,9 @@ public class Message implements Serializable, IEvent {
this.creationDate = creationDate; this.creationDate = creationDate;
this.receivedDate = receivedDate; this.receivedDate = receivedDate;
this.readDate = readDate; this.readDate = readDate;
this.text = text; this.text = text == null ? "" : text;
this.attachment = attachment; this.attachment = attachment;
this.status = status; this.status = Objects.requireNonNull(status);
this.forwarded = forwarded; this.forwarded = forwarded;
} }

View File

@ -86,7 +86,7 @@ public final class User extends Contact {
*/ */
public User(long id, String name, UserStatus status, Set<Contact> contacts) { public User(long id, String name, UserStatus status, Set<Contact> contacts) {
super(id, name, contacts); super(id, name, contacts);
this.status = status; this.status = Objects.requireNonNull(status);
} }
@Override @Override

View File

@ -1,6 +1,7 @@
package envoy.event; package envoy.event;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
import dev.kske.eventbus.IEvent; import dev.kske.eventbus.IEvent;
@ -20,7 +21,16 @@ public abstract class Event<T> implements IEvent, Serializable {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
protected Event(T value) { protected Event(T value) {
this.value = value; this(value, false);
}
/**
* This constructor is reserved for {@link Valueless} events. No other event should contain null
* values. Only use if really necessary. Using this constructor with {@code true} implies that
* the user has to manually check if the value of the event is null.
*/
protected Event(T value, boolean canBeNull) {
this.value = canBeNull ? value : Objects.requireNonNull(value);
} }
/** /**
@ -46,7 +56,7 @@ public abstract class Event<T> implements IEvent, Serializable {
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
protected Valueless() { protected Valueless() {
super(null); super(null, true);
} }
@Override @Override

View File

@ -20,7 +20,7 @@ public class GroupCreationResult extends Event<Group> {
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public GroupCreationResult() { public GroupCreationResult() {
super(null); super(null, true);
} }
/** /**

View File

@ -30,7 +30,7 @@ public final class GroupMessageStatusChange extends MessageStatusChange {
} }
/** /**
* @return the memberID which the user who sends this event has * @return the ID of the sender of this event
* @since Envoy Common v0.1-beta * @since Envoy Common v0.1-beta
*/ */
public long getMemberID() { return memberID; } public long getMemberID() { return memberID; }

View File

@ -2,6 +2,8 @@ package envoy.event;
import static envoy.event.ElementOperation.*; import static envoy.event.ElementOperation.*;
import java.util.Objects;
import envoy.data.*; import envoy.data.*;
/** /**
@ -30,7 +32,7 @@ public final class GroupResize extends Event<User> {
*/ */
public GroupResize(User user, Group group, ElementOperation operation) { public GroupResize(User user, Group group, ElementOperation operation) {
super(user); super(user);
this.operation = operation; this.operation = Objects.requireNonNull(operation);
final var contained = group.getContacts().contains(user); final var contained = group.getContacts().contains(user);
if (contained && operation.equals(ADD)) if (contained && operation.equals(ADD))
throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group)); throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group));

View File

@ -8,8 +8,6 @@ package envoy.event;
*/ */
public final class IsTyping extends Event<Long> { public final class IsTyping extends Event<Long> {
private final long destinationID;
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
@ -22,20 +20,13 @@ public final class IsTyping extends Event<Long> {
public static final int millisecondsActive = 3500; public static final int millisecondsActive = 3500;
/** /**
* Creates a new {@code IsTyping} event with originator and recipient. * Creates a new {@code IsTyping}. The client will only send the contact that should receive
* this event. The server will send the id of the contact who sent this event.
* *
* @param sourceID the ID of the originator * @param id the ID of the recipient (client)/ originator(server)
* @param destinationID the ID of the contact the user wrote to
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public IsTyping(Long sourceID, long destinationID) { public IsTyping(long id) {
super(sourceID); super(id);
this.destinationID = destinationID;
} }
/**
* @return the ID of the contact in whose chat the user typed something
* @since Envoy Common v0.2-beta
*/
public long getDestinationID() { return destinationID; }
} }

View File

@ -23,7 +23,7 @@ public final class IssueProposal extends Event<String> {
*/ */
public IssueProposal(String title, String description, boolean isBug) { public IssueProposal(String title, String description, boolean isBug) {
super(escape(title)); super(escape(title));
this.description = sanitizeDescription(description); this.description = description == null ? "" : sanitizeDescription(description);
bug = isBug; bug = isBug;
} }
@ -37,8 +37,8 @@ public final class IssueProposal extends Event<String> {
*/ */
public IssueProposal(String title, String description, String user, boolean isBug) { public IssueProposal(String title, String description, String user, boolean isBug) {
super(escape(title)); super(escape(title));
this.description = this.description = description == null ? ""
sanitizeDescription(description) + String.format("<br>Submitted by user %s.", user); : sanitizeDescription(description) + String.format("<br>Submitted by user %s.", user);
bug = isBug; bug = isBug;
} }

View File

@ -1,6 +1,7 @@
package envoy.event; package envoy.event;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import envoy.data.Message; import envoy.data.Message;
@ -26,7 +27,7 @@ public class MessageStatusChange extends Event<Message.MessageStatus> {
public MessageStatusChange(long id, Message.MessageStatus status, Instant date) { public MessageStatusChange(long id, Message.MessageStatus status, Instant date) {
super(status); super(status);
this.id = id; this.id = id;
this.date = date; this.date = Objects.requireNonNull(date);
} }
/** /**

View File

@ -1,6 +1,6 @@
package envoy.event; package envoy.event;
import envoy.data.Contact; import java.util.Objects;
/** /**
* @author Leon Hofmeister * @author Leon Hofmeister
@ -8,29 +8,20 @@ import envoy.data.Contact;
*/ */
public final class PasswordChangeRequest extends Event<String> { public final class PasswordChangeRequest extends Event<String> {
private final long id; private final String oldPassword;
private final String oldPassword;
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
/** /**
* @param newPassword the new password of that user * @param newPassword the new password of that user
* @param oldPassword the old password of that user * @param oldPassword the old password of that user
* @param userID the ID of the user who wants to change his password
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public PasswordChangeRequest(String newPassword, String oldPassword, long userID) { public PasswordChangeRequest(String newPassword, String oldPassword) {
super(newPassword); super(newPassword);
this.oldPassword = oldPassword; this.oldPassword = Objects.requireNonNull(oldPassword);
id = userID;
} }
/**
* @return the ID of the {@link Contact} this event is related to
* @since Envoy Common v0.2-alpha
*/
public long getID() { return id; }
/** /**
* @return the old password of the underlying user * @return the old password of the underlying user
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
@ -39,6 +30,6 @@ public final class PasswordChangeRequest extends Event<String> {
@Override @Override
public String toString() { public String toString() {
return "PasswordChangeRequest[id=" + id + "]"; return "PasswordChangeRequest[]";
} }
} }

View File

@ -6,23 +6,13 @@ package envoy.event;
*/ */
public final class ProfilePicChange extends Event<byte[]> { public final class ProfilePicChange extends Event<byte[]> {
private final long id;
private static final long serialVersionUID = 0L; private static final long serialVersionUID = 0L;
/** /**
* @param value the byte[] of the new image * @param value the byte[] of the new image
* @param userID the ID of the user who changed his profile pic
* @since Envoy Common v0.2-beta * @since Envoy Common v0.2-beta
*/ */
public ProfilePicChange(byte[] value, long userID) { public ProfilePicChange(byte[] value) {
super(value); super(value);
id = userID;
} }
/**
* @return the ID of the user changing his profile pic
* @since Envoy Common v0.2-beta
*/
public long getId() { return id; }
} }

View File

@ -1,5 +1,7 @@
package envoy.event.contact; package envoy.event.contact;
import java.util.Objects;
import envoy.data.User; import envoy.data.User;
import envoy.event.*; import envoy.event.*;
@ -24,7 +26,7 @@ public final class UserOperation extends Event<User> {
*/ */
public UserOperation(User contact, ElementOperation operationType) { public UserOperation(User contact, ElementOperation operationType) {
super(contact); super(contact);
this.operationType = operationType; this.operationType = Objects.requireNonNull(operationType);
} }
/** /**

View File

@ -1,285 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="14.3.0">
<zoom_level>10</zoom_level>
<element>
<id>UMLClass</id>
<coordinates>
<x>50</x>
<y>60</y>
<w>210</w>
<h>240</h>
</coordinates>
<panel_attributes>DBUser
--
id: Long
nick: String
name: String
chats: Set&lt;DBChat&gt;
status: UserStatus
created: Instant
lastSeen: Instant
deleted: Boolean
avatar: DBAvatar
passwordHash: String
authToken: String
authTokenExpiration: Instant
--
+toCommon(): User</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>380</x>
<y>330</y>
<w>290</w>
<h>160</h>
</coordinates>
<panel_attributes>DBMessage
--
id: Long
sender: DBUser
forwarded: Boolean
deleted: Boolean
statuses: Map&lt;DBUser, DBMessageStatus&gt;
text: String
attachment: DBAttachment
--
+toCommon(): Message</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>770</x>
<y>320</y>
<w>210</w>
<h>100</h>
</coordinates>
<panel_attributes>DBMessageStatus
--
id: Long
status: MessageStatus
timestamp: Instant
--
+toCommon(): MessageStatus</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>770</x>
<y>440</y>
<w>180</w>
<h>120</h>
</coordinates>
<panel_attributes>DBAttachment
--
id: Long
name: String
type: AttachmentType
url: String
--
+toCommon(): Attachment</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>510</x>
<y>70</y>
<w>200</w>
<h>180</h>
</coordinates>
<panel_attributes>DBChat
--
id: Long
name: String
members: Set&lt;DBUser&gt;
admins: Set&lt;DBUser&gt;
messages: Set&lt;DBMessage&gt;
avatar: DBAvatar
allowJoining: Boolean
allowAttachments: Boolean
--
+toCommon(): Chat</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>250</x>
<y>100</y>
<w>280</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;-&gt;
m1=0..n
m2=0..n</panel_attributes>
<additional_attributes>10.0;10.0;260.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>600</x>
<y>240</y>
<w>50</w>
<h>110</h>
</coordinates>
<panel_attributes>lt=-&gt;
m2=0..n</panel_attributes>
<additional_attributes>10.0;10.0;10.0;90.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>660</x>
<y>360</y>
<w>130</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=-&gt;
m2=0..n</panel_attributes>
<additional_attributes>10.0;10.0;110.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>660</x>
<y>470</y>
<w>130</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=-&gt;
m2=0..1</panel_attributes>
<additional_attributes>10.0;10.0;110.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>140</x>
<y>290</y>
<w>260</w>
<h>150</h>
</coordinates>
<panel_attributes>lt=&lt;-
m1=0..n</panel_attributes>
<additional_attributes>10.0;10.0;10.0;130.0;240.0;130.0</additional_attributes>
</element>
<element>
<id>UMLPackage</id>
<coordinates>
<x>10</x>
<y>0</y>
<w>1010</w>
<h>590</h>
</coordinates>
<panel_attributes>envoy.server.data
--
bg=#CCCCCC</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>90</x>
<y>680</y>
<w>170</w>
<h>130</h>
</coordinates>
<panel_attributes>User
--
id: long
nick: String
name: String
status: UserStatus
created: Instant
deleted: boolean</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>340</x>
<y>680</y>
<w>190</w>
<h>100</h>
</coordinates>
<panel_attributes>UserStatus
--
_+ONLINE: UserStatus_
_+AWAY: UserStatus_
_+BUSY: UserStatus_
_+OFFLINE: UserStatus_</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>250</x>
<y>730</y>
<w>110</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=&lt;-</panel_attributes>
<additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>340</x>
<y>150</y>
<w>100</w>
<h>80</h>
</coordinates>
<panel_attributes>DBAvatar
--
id: Long
edited: Instant
url: String
layer=1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>250</x>
<y>190</y>
<w>110</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;-
m1=0..1</panel_attributes>
<additional_attributes>90.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>430</x>
<y>190</y>
<w>100</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;-
m1=0..1</panel_attributes>
<additional_attributes>10.0;10.0;80.0;10.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>610</x>
<y>680</y>
<w>180</w>
<h>130</h>
</coordinates>
<panel_attributes>Chat
--
id: long
name: String
deceased: boolean
members: Set&lt;User&gt;
admins: Set&lt;User&gt;
messages: List&lt;Message&gt;</panel_attributes>
<additional_attributes/>
</element>
</diagram>

View File

@ -1,501 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="14.3.0">
<zoom_level>9</zoom_level>
<element>
<id>UMLActor</id>
<coordinates>
<x>72</x>
<y>540</y>
<w>54</w>
<h>99</h>
</coordinates>
<panel_attributes>Client1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>2196</x>
<y>504</y>
<w>198</w>
<h>171</h>
</coordinates>
<panel_attributes>Database
halign=center
bg=orange
--</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>351</x>
<y>513</y>
<w>90</w>
<h>117</h>
</coordinates>
<panel_attributes>Main</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>531</x>
<y>513</y>
<w>90</w>
<h>117</h>
</coordinates>
<panel_attributes>Startup
bg=orange</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>702</x>
<y>513</y>
<w>90</w>
<h>117</h>
</coordinates>
<panel_attributes>Client
bg=yellow</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>873</x>
<y>513</y>
<w>90</w>
<h>117</h>
</coordinates>
<panel_attributes>LocalDB
bg=pink</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>1548</x>
<y>522</y>
<w>189</w>
<h>117</h>
</coordinates>
<panel_attributes>LoginCredentialsProcessor
bg=cyan</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>1809</x>
<y>522</y>
<w>135</w>
<h>117</h>
</coordinates>
<panel_attributes>PersistenceManager
bg=gray</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>396</x>
<y>729</y>
<w>189</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
start javaFX app</panel_attributes>
<additional_attributes>190.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>387</x>
<y>720</y>
<w>18</w>
<h>54</h>
</coordinates>
<panel_attributes/>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>387</x>
<y>621</y>
<w>27</w>
<h>117</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;110.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>567</x>
<y>621</y>
<w>27</w>
<h>117</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;110.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>738</x>
<y>621</y>
<w>27</w>
<h>216</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;220.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>909</x>
<y>621</y>
<w>27</w>
<h>153</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;150.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>567</x>
<y>720</y>
<w>18</w>
<h>396</h>
</coordinates>
<panel_attributes>bg=orange</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>738</x>
<y>819</y>
<w>18</w>
<h>558</h>
</coordinates>
<panel_attributes>bg=yellow</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>909</x>
<y>756</y>
<w>18</w>
<h>621</h>
</coordinates>
<panel_attributes>bg=pink</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>576</x>
<y>756</y>
<w>351</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
initialize</panel_attributes>
<additional_attributes>370.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>1062</x>
<y>513</y>
<w>90</w>
<h>117</h>
</coordinates>
<panel_attributes>Receiver
bg=red</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1098</x>
<y>621</y>
<w>27</w>
<h>270</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;280.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>1098</x>
<y>873</y>
<w>18</w>
<h>504</h>
</coordinates>
<panel_attributes>bg=red</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>576</x>
<y>1071</y>
<w>180</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
init receivers
layer=0</panel_attributes>
<additional_attributes>180.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>576</x>
<y>828</y>
<w>180</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
perform handshake</panel_attributes>
<additional_attributes>180.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>747</x>
<y>873</y>
<w>369</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
init handhake receivers
layer=0</panel_attributes>
<additional_attributes>390.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>747</x>
<y>1071</y>
<w>369</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
init permanent receivers
layer=0</panel_attributes>
<additional_attributes>390.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1638</x>
<y>630</y>
<w>27</w>
<h>306</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;320.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>1638</x>
<y>918</y>
<w>18</w>
<h>558</h>
</coordinates>
<panel_attributes>bg=cyan</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>747</x>
<y>918</y>
<w>909</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
send login credentials
layer=0</panel_attributes>
<additional_attributes>990.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1872</x>
<y>630</y>
<w>27</w>
<h>306</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;320.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>1872</x>
<y>918</y>
<w>18</w>
<h>558</h>
</coordinates>
<panel_attributes>bg=gray</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1881</x>
<y>918</y>
<w>423</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
use queries
layer=0</panel_attributes>
<additional_attributes>450.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>2286</x>
<y>666</y>
<w>27</w>
<h>270</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;280.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>2286</x>
<y>918</y>
<w>18</w>
<h>558</h>
</coordinates>
<panel_attributes>bg=orange</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1647</x>
<y>918</y>
<w>243</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;-
aquire data
layer=0</panel_attributes>
<additional_attributes>250.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1881</x>
<y>954</y>
<w>423</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=-&gt;&gt;
send query responses
layer=0</panel_attributes>
<additional_attributes>450.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1647</x>
<y>954</y>
<w>243</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=-&gt;&gt;
send aquired data
layer=0</panel_attributes>
<additional_attributes>250.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>1107</x>
<y>990</y>
<w>549</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=-&gt;&gt;
send user, chats, pending messages, etc.
layer=0</panel_attributes>
<additional_attributes>590.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>918</x>
<y>990</y>
<w>198</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=-&gt;&gt;
store data
layer=0</panel_attributes>
<additional_attributes>200.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>99</x>
<y>729</y>
<w>306</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=-&gt;
start app</panel_attributes>
<additional_attributes>10.0;20.0;320.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>90</x>
<y>621</y>
<w>27</w>
<h>117</h>
</coordinates>
<panel_attributes>lt=.</panel_attributes>
<additional_attributes>10.0;10.0;10.0;110.0</additional_attributes>
</element>
<element>
<id>UMLGeneric</id>
<coordinates>
<x>90</x>
<y>720</y>
<w>18</w>
<h>54</h>
</coordinates>
<panel_attributes/>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>747</x>
<y>1026</y>
<w>369</w>
<h>36</h>
</coordinates>
<panel_attributes>lt=-&gt;&gt;
online
layer=0</panel_attributes>
<additional_attributes>390.0;20.0;10.0;20.0</additional_attributes>
</element>
</diagram>

View File

@ -1,7 +1,7 @@
package envoy.server.data; package envoy.server.data;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import javax.persistence.*; import javax.persistence.*;
@ -121,8 +121,9 @@ public final class PersistenceManager {
transaction(() -> { transaction(() -> {
// Remove this contact from the contact list of his contacts // Remove this contact from the contact list of his contacts
for (final var remainingContact : contact.getContacts()) for (final var remainingContact : contact.contacts)
remainingContact.getContacts().remove(contact); remainingContact.getContacts().remove(contact);
contact.contacts.clear();
}); });
remove(contact); remove(contact);
} }
@ -223,6 +224,9 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.2-beta * @since Envoy Server Standalone v0.2-beta
*/ */
public List<Message> getPendingMessages(User user, Instant lastSync) { public List<Message> getPendingMessages(User user, Instant lastSync) {
if (user == null)
return new ArrayList<>();
lastSync = Objects.requireNonNullElse(lastSync, Instant.EPOCH);
return entityManager.createNamedQuery(Message.getPending).setParameter("user", user) return entityManager.createNamedQuery(Message.getPending).setParameter("user", user)
.setParameter("lastSeen", lastSync).getResultList(); .setParameter("lastSeen", lastSync).getResultList();
} }
@ -236,6 +240,9 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.2-beta * @since Envoy Server Standalone v0.2-beta
*/ */
public List<GroupMessage> getPendingGroupMessages(User user, Instant lastSync) { public List<GroupMessage> getPendingGroupMessages(User user, Instant lastSync) {
if (user == null)
return new ArrayList<>();
lastSync = Objects.requireNonNullElse(lastSync, Instant.EPOCH);
return entityManager.createNamedQuery(GroupMessage.getPendingGroupMsg) return entityManager.createNamedQuery(GroupMessage.getPendingGroupMsg)
.setParameter("userId", user.getID()) .setParameter("userId", user.getID())
.setParameter("lastSeen", lastSync) .setParameter("lastSeen", lastSync)
@ -277,16 +284,18 @@ public final class PersistenceManager {
* @since Envoy Server v0.3-beta * @since Envoy Server v0.3-beta
*/ */
public void addContactBidirectional(Contact contact1, Contact contact2) { public void addContactBidirectional(Contact contact1, Contact contact2) {
if (!(contact1 == null || contact2 == null)) {
// Add users to each others contact list // Add users to each others contact list
contact1.getContacts().add(contact2); contact1.getContacts().add(contact2);
contact2.getContacts().add(contact1); contact2.getContacts().add(contact1);
// Synchronize changes with the database // Synchronize changes with the database
transaction(() -> { transaction(() -> {
entityManager.merge(contact1); entityManager.merge(contact1);
entityManager.merge(contact2); entityManager.merge(contact2);
}); });
}
} }
/** /**
@ -308,16 +317,18 @@ public final class PersistenceManager {
* @since Envoy Server v0.3-beta * @since Envoy Server v0.3-beta
*/ */
public void removeContactBidirectional(Contact contact1, Contact contact2) { public void removeContactBidirectional(Contact contact1, Contact contact2) {
if (!(contact1 == null || contact2 == null)) {
// Remove users from each others contact list // Remove users from each others contact list
contact1.getContacts().remove(contact2); contact1.getContacts().remove(contact2);
contact2.getContacts().remove(contact1); contact2.getContacts().remove(contact1);
// Synchronize changes with the database // Synchronize changes with the database
transaction(() -> { transaction(() -> {
entityManager.merge(contact1); entityManager.merge(contact1);
entityManager.merge(contact2); entityManager.merge(contact2);
}); });
}
} }
/** /**
@ -331,15 +342,36 @@ public final class PersistenceManager {
} }
private void persist(Object obj) { private void persist(Object obj) {
transaction(() -> entityManager.persist(obj)); try {
transaction(() -> entityManager.persist(obj));
} catch (EntityExistsException e) {
if (transaction.isActive())
transaction.rollback();
EnvoyLog.getLogger(PersistenceManager.class).log(Level.WARNING,
String.format("Could not persist %s: entity exists already.", obj));
}
} }
private void merge(Object obj) { private void merge(Object obj) {
transaction(() -> entityManager.merge(obj)); try {
transaction(() -> entityManager.merge(obj));
} catch (IllegalArgumentException e) {
if (transaction.isActive())
transaction.rollback();
EnvoyLog.getLogger(PersistenceManager.class).log(Level.WARNING,
String.format("Could not merge %s: entity doesn't exist.", obj));
}
} }
private void remove(Object obj) { private void remove(Object obj) {
transaction(() -> entityManager.remove(obj)); try {
transaction(() -> entityManager.remove(obj));
} catch (IllegalArgumentException e) {
if (transaction.isActive())
transaction.rollback();
EnvoyLog.getLogger(PersistenceManager.class).log(Level.WARNING,
String.format("Could not remove %s: entity didn't exist (for the database).", obj));
}
} }
/** /**

View File

@ -49,8 +49,10 @@ public final class ConnectionManager implements ISocketIdListener {
// Notify contacts of this users offline-going // Notify contacts of this users offline-going
final envoy.server.data.User user = final envoy.server.data.User user =
PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID)); PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
user.setLastSeen(Instant.now()); if (user != null) {
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE); user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
}
// Remove the socket // Remove the socket
sockets.entrySet().removeIf(e -> e.getValue() == socketID); sockets.entrySet().removeIf(e -> e.getValue() == socketID);

View File

@ -33,7 +33,6 @@ public final class ObjectMessageProcessor implements IMessageProcessor {
this.processors = processors; this.processors = processors;
} }
@SuppressWarnings("unchecked")
@Override @Override
public void process(Message message, WriteProxy writeProxy) { public void process(Message message, WriteProxy writeProxy) {
try (ObjectInputStream in = try (ObjectInputStream in =
@ -45,23 +44,34 @@ public final class ObjectMessageProcessor implements IMessageProcessor {
return; return;
} }
logger.fine("Received " + obj); logger.log(Level.INFO, "Received " + obj);
// Get processor and input class and process object refer(message.socketId, writeProxy, obj);
for (@SuppressWarnings("rawtypes")
ObjectProcessor p : processors) {
Class<?> c = (Class<?>) ((ParameterizedType) p.getClass().getGenericInterfaces()[0])
.getActualTypeArguments()[0];
if (c.equals(obj.getClass()))
try {
p.process(c.cast(obj), message.socketId, new ObjectWriteProxy(writeProxy));
break;
} catch (IOException e) {
logger.log(Level.SEVERE, "Exception during processor execution: ", e);
}
}
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
e.printStackTrace(); logger.log(Level.WARNING,
"An exception occurred when reading in an object: " + e);
}
}
/**
* Executes the appropriate {@link ObjectProcessor} for the given input ({@code obj}), if any is
* present.
*/
@SuppressWarnings("unchecked")
private void refer(long socketID, WriteProxy writeProxy, Object obj) {
// Get processor and input class and process object
for (@SuppressWarnings("rawtypes")
ObjectProcessor p : processors) {
Class<?> c = (Class<?>) ((ParameterizedType) p.getClass().getGenericInterfaces()[0])
.getActualTypeArguments()[0];
if (c.equals(obj.getClass()))
try {
p.process(c.cast(obj), socketID, new ObjectWriteProxy(writeProxy));
break;
} catch (IOException e) {
logger.log(Level.SEVERE, "Exception during processor execution: ", e);
}
} }
} }
} }

View File

@ -5,7 +5,7 @@ import static envoy.server.Startup.config;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.logging.Logger; import java.util.logging.*;
import javax.persistence.EntityExistsException; import javax.persistence.EntityExistsException;
@ -15,6 +15,7 @@ import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.server.util.UserAuthenticationUtil;
/** /**
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -29,6 +30,15 @@ public final class GroupMessageProcessor implements ObjectProcessor<GroupMessage
@Override @Override
public void process(GroupMessage groupMessage, long socketID, ObjectWriteProxy writeProxy) { public void process(GroupMessage groupMessage, long socketID, ObjectWriteProxy writeProxy) {
// Check whether the message has the expected parameters
if (!UserAuthenticationUtil.isExpectedUser(groupMessage.getSenderID(), socketID)
|| persistenceManager.getContactByID(groupMessage.getRecipientID()) == null) {
logger.log(Level.INFO,
"Received a group message with invalid parameters");
return;
}
groupMessage.nextStatus(); groupMessage.nextStatus();
// Update statuses to SENT / RECEIVED depending on online status // Update statuses to SENT / RECEIVED depending on online status

View File

@ -12,6 +12,7 @@ import envoy.util.EnvoyLog;
import envoy.server.data.*; import envoy.server.data.*;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.server.util.UserAuthenticationUtil;
/** /**
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -28,7 +29,17 @@ public final class GroupMessageStatusChangeProcessor
@Override @Override
public void process(GroupMessageStatusChange statusChange, long socketID, public void process(GroupMessageStatusChange statusChange, long socketID,
ObjectWriteProxy writeProxy) { ObjectWriteProxy writeProxy) {
// Check whether the message has the expected parameters
if (!UserAuthenticationUtil.isExpectedUser(statusChange.getMemberID(), socketID)) {
logger.log(Level.INFO,
"Received a group message with invalid parameters");
return;
}
GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID()); GroupMessage gmsg = (GroupMessage) persistenceManager.getMessageByID(statusChange.getID());
if (gmsg == null)
return;
// Any other status than READ is not supposed to be sent to the server // Any other status than READ is not supposed to be sent to the server
if (statusChange.get() != MessageStatus.READ) { if (statusChange.get() != MessageStatus.READ) {

View File

@ -24,6 +24,10 @@ public final class GroupResizeProcessor implements ObjectProcessor<GroupResize>
final var group = persistenceManager.getGroupByID(groupResize.getGroupID()); final var group = persistenceManager.getGroupByID(groupResize.getGroupID());
final var sender = persistenceManager.getUserByID(groupResize.get().getID()); final var sender = persistenceManager.getUserByID(groupResize.get().getID());
// TODO: Inform the sender that this group has already been deleted
if (group == null)
return;
// Perform the desired operation // Perform the desired operation
switch (groupResize.getOperation()) { switch (groupResize.getOperation()) {
case ADD: case ADD:

View File

@ -23,10 +23,11 @@ public final class IsTypingProcessor implements ObjectProcessor<IsTyping> {
throws IOException { throws IOException {
final var contact = persistenceManager.getContactByID(event.get()); final var contact = persistenceManager.getContactByID(event.get());
if (contact instanceof User) { if (contact instanceof User) {
final var destinationID = event.getDestinationID(); if (connectionManager.isOnline(event.get()))
if (connectionManager.isOnline(destinationID)) writeProxy.write(connectionManager.getSocketID(event.get()),
writeProxy.write(connectionManager.getSocketID(destinationID), event); new IsTyping(connectionManager.getUserIDBySocketID(socketID)));
} else } else
writeProxy.writeToOnlineContacts(contact.getContacts(), event); writeProxy.writeToOnlineContacts(contact.getContacts(),
new IsTyping(connectionManager.getUserIDBySocketID(socketID)));
} }
} }

View File

@ -123,7 +123,6 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Process token request // Process token request
if (credentials.requestToken()) { if (credentials.requestToken()) {
String token; String token;
if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now())) if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now()))
// Reuse existing token and delay expiration date // Reuse existing token and delay expiration date
@ -139,7 +138,6 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
persistenceManager.updateContact(user); persistenceManager.updateContact(user);
writeProxy.write(socketID, new NewAuthToken(token)); writeProxy.write(socketID, new NewAuthToken(token));
} }
final var pendingMessages = final var pendingMessages =
PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync()); PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync());
pendingMessages.removeIf(GroupMessage.class::isInstance); pendingMessages.removeIf(GroupMessage.class::isInstance);

View File

@ -12,6 +12,7 @@ import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.*; import envoy.server.net.*;
import envoy.server.util.UserAuthenticationUtil;
/** /**
* This {@link ObjectProcessor} handles incoming {@link Message}s. * This {@link ObjectProcessor} handles incoming {@link Message}s.
@ -29,6 +30,15 @@ public final class MessageProcessor implements ObjectProcessor<Message> {
@Override @Override
public void process(Message message, long socketID, ObjectWriteProxy writeProxy) { public void process(Message message, long socketID, ObjectWriteProxy writeProxy) {
// Check whether the message has the expected parameters
if (!UserAuthenticationUtil.isExpectedUser(message.getSenderID(), socketID)
|| persistenceManager.getContactByID(message.getRecipientID()) == null) {
logger.log(Level.INFO,
"Received a message with invalid parameters");
return;
}
message.nextStatus(); message.nextStatus();
// Convert to server message // Convert to server message

View File

@ -32,6 +32,8 @@ public final class MessageStatusChangeProcessor implements ObjectProcessor<Messa
} }
final var msg = persistenceManager.getMessageByID(statusChange.getID()); final var msg = persistenceManager.getMessageByID(statusChange.getID());
if (msg == null)
return;
msg.read(); msg.read();
persistenceManager.updateMessage(msg); persistenceManager.updateMessage(msg);

View File

@ -7,7 +7,7 @@ import envoy.event.*;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager; import envoy.server.data.PersistenceManager;
import envoy.server.net.ObjectWriteProxy; import envoy.server.net.*;
import envoy.server.util.PasswordUtil; import envoy.server.util.PasswordUtil;
/** /**
@ -21,7 +21,8 @@ public final class PasswordChangeRequestProcessor
public void process(PasswordChangeRequest event, long socketID, ObjectWriteProxy writeProxy) public void process(PasswordChangeRequest event, long socketID, ObjectWriteProxy writeProxy)
throws IOException { throws IOException {
final var persistenceManager = PersistenceManager.getInstance(); final var persistenceManager = PersistenceManager.getInstance();
final var user = persistenceManager.getUserByID(event.getID()); final var user = persistenceManager
.getUserByID(ConnectionManager.getInstance().getUserIDBySocketID(socketID));
final var logger = final var logger =
EnvoyLog.getLogger(PasswordChangeRequestProcessor.class); EnvoyLog.getLogger(PasswordChangeRequestProcessor.class);
final var correctAuthentication = final var correctAuthentication =

View File

@ -22,10 +22,16 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance(); private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override @Override
public void process(UserOperation evt, long socketId, ObjectWriteProxy writeProxy) { public void process(UserOperation evt, long socketID, ObjectWriteProxy writeProxy) {
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId); final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketID);
final long contactID = evt.get().getID(); final long contactID = evt.get().getID();
final var sender = persistenceManager.getUserByID(userID); final var recipient = persistenceManager.getUserByID(contactID);
// TODO: Inform the sender if the requested contact has already been deleted
if (recipient == null)
return;
final var sender = persistenceManager.getUserByID(userID);
switch (evt.getOperationType()) { switch (evt.getOperationType()) {
case ADD: case ADD:
logger.log(Level.FINE, logger.log(Level.FINE,
@ -45,7 +51,7 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
sender.setLatestContactDeletion(Instant.now()); sender.setLatestContactDeletion(Instant.now());
// Notify the removed contact on next startup(s) of this deletion // Notify the removed contact on next startup(s) of this deletion
persistenceManager.getUserByID(contactID).setLatestContactDeletion(Instant.now()); recipient.setLatestContactDeletion(Instant.now());
// Notify the removed contact if online // Notify the removed contact if online
if (connectionManager.isOnline(contactID)) if (connectionManager.isOnline(contactID))

View File

@ -0,0 +1,24 @@
package envoy.server.util;
import envoy.server.net.ConnectionManager;
/**
* @author Leon Hofmeister
* @since Envoy Server v0.3-beta
*/
public final class UserAuthenticationUtil {
private UserAuthenticationUtil() {}
/**
* Checks whether a user is really who he claims to be.
*
* @param expectedID the expected user ID
* @param socketID the socket ID of the user making a request
* @return whether this user is who he claims to be
* @since Envoy Server v0.3-beta
*/
public static boolean isExpectedUser(long expectedID, long socketID) {
return ConnectionManager.getInstance().getUserIDBySocketID(socketID) == expectedID;
}
}