Compare commits

...
This repository has been archived on 2021-12-05. You can view files and clone it, but cannot push or open issues or pull requests.

26 Commits

Author SHA1 Message Date
Kai S. K. Engelbart 6499a4f698
Merge pull request 'Update Default Chat Pictures on Theme Change' (#115) from b/ui-fixes into develop
zdm/envoy/pipeline/head This commit looks good Details
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/115
Reviewed-by: delvh <leon@kske.dev>
2021-12-04 09:22:53 +01:00
Kai S. K. Engelbart 05ed5da41b
Merge branch 'develop' into b/ui-fixes
zdm/envoy/pipeline/head This commit looks good Details
2021-12-03 21:37:07 +01:00
Kai S. K. Engelbart c5f4969666
Merge pull request 'Add Jenkinsfile' (#114) from feature/jenkinsfile into develop
zdm/envoy/pipeline/head This commit looks good Details
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/114
Reviewed-by: DieGurke <maxi@kske.dev>
Reviewed-by: delvh <leon@kske.dev>
2021-12-03 21:29:22 +01:00
Kai S. K. Engelbart 1a9f9a85ab
Add Jenkinsfile
zdm/envoy/pipeline/head This commit looks good Details
2021-12-03 16:35:43 +01:00
Kai S. K. Engelbart 544210a811
Merge pull request 'Upgrade to Event Bus 1.0.0' (#112) from event-bus-1.0.0 into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/112
Reviewed-by: delvh <leon@kske.dev>
2021-02-20 22:29:38 +01:00
Kai S. K. Engelbart 5ef5d96445
Upgrade to Event Bus 1.0.0 2021-02-19 13:25:12 +01:00
Kai S. K. Engelbart dcf1b0c58d
Send Pending Messages After Successful Handshake (#111)
Instead of caching pending messages during the handshake and relaying
them afterwards, they are now sent after the handshake has been
completed.

This is possible because the relevant processors (messages and status
changes) are now event handlers which are registered at the event bus,
which means that they can immediately react to pending messages even if
Client#initReceiver has not been fully executed yet.

Because Client#initReceiver exists for that very reason, it is no
longer necessary anymore. ID generator initialization, which was its other part,
is now directly handled in Startup#performHandshake, which is a far more
sensible placement.

Fixes #106

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/111
Reviewed-by: DieGurke <maxi@kske.dev>
2020-12-02 21:21:00 +01:00
Leon Hofmeister 10213a0d3d
Link to Documentation (Wiki) in README (#110)
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/110
Reviewed-by: kske <kai@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
2020-11-27 09:36:08 +01:00
Leon Hofmeister b653652f6d
Fix chat default pictures not being updated on theme change 2020-11-22 12:26:08 +01:00
Kai S. K. Engelbart 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
Kai S. K. Engelbart 6d85e337d2
Remove fixed size support from SceneContext 2020-11-22 11:11:48 +01:00
Kai S. K. Engelbart 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
Kai S. K. Engelbart e3052a2133
Reuse the same scene in SceneContext by switching root nodes 2020-11-06 17:27:54 +01:00
Kai S. K. Engelbart 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
Kai S. K. Engelbart 0ce8b0c89d
Move SceneInfo to separate file 2020-11-06 08:58:13 +01:00
Kai S. K. Engelbart 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
Leon Hofmeister e5659c1da1
Remove account deletion on the server 2020-10-31 16:57:26 +01:00
Leon Hofmeister f67ca1d61d
Add option to delete your account 2020-10-31 16:56:31 +01:00
Leon Hofmeister 8bdd201b28
Add Ctrl+U and Ctrl+K shortcuts to ChatScene 2020-10-31 16:54:14 +01:00
Kai S. K. Engelbart 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
Leon Hofmeister 7a883861be
Apply suggestions by @kske 2020-10-30 12:07:56 +01:00
Leon Hofmeister d4c7813c97
Fix unnecessary authentication token being sent in requests 2020-10-23 18:45:40 +02:00
Kai S. K. Engelbart 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
Leon Hofmeister fccd7e70b1
Disable crashing the server when Hibernate panics after oopsing 2020-10-23 00:15:37 +02:00
Leon Hofmeister 2eeb55ed52
Add client side errors in case of data initialization with null values 2020-10-22 23:58:55 +02:00
Leon Hofmeister 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
65 changed files with 711 additions and 414 deletions

37
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,37 @@
pipeline {
agent any
options {
ansiColor('xterm')
}
stages {
stage('Build') {
steps {
sh 'mvn -DskipTests clean package'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit '*/target/surefire-reports/*.xml'
}
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('KSKE SonarQube') {
sh 'mvn org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184:sonar'
}
}
}
}
post {
success {
archiveArtifacts artifacts: 'client/target/envoy-client-*-shaded.jar, server/target/envoy-server-jar-with-dependencies.jar'
}
}
}

View File

@ -17,6 +17,8 @@ If you want to transfer a file to another user, you can attach it to a message.
On the settings page some convenience features can be configured, as well as the color theme.
Additional info on how to use Envoy can be found [here](https://git.kske.dev/zdm/envoy/wiki) in the client section.
### System requirements
To run Envoy, you have to install a Java Runtime Environment (JRE) of at least version 11.
@ -29,7 +31,7 @@ Most major Linux distributions like Debian, Arch and Gentoo have a Noto emoji pa
To set up an Envoy server, download the package from the release page.
Because the project lacks external documentation for the moment, please refer to the Javadoc inside the source code to configure your Envoy instance.
To configure the behavior of Envoy Server, please have a look at the [documentation](https://git.kske.dev/zdm/envoy/wiki), specifically the server part.
### System requirements

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">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<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 {
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.
*/
protected transient long lastWritingEvent;
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
protected int unreadAmount;
protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty();
protected final Contact recipient;
private static final long serialVersionUID = 2L;
/**

View File

@ -13,9 +13,8 @@ import java.util.stream.Stream;
import javafx.application.Platform;
import javafx.collections.*;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.EventListener;
import dev.kske.eventbus.core.*;
import dev.kske.eventbus.core.Event;
import envoy.data.*;
import envoy.data.Message.MessageStatus;
@ -35,7 +34,7 @@ import envoy.client.event.*;
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public final class LocalDB implements EventListener {
public final class LocalDB {
// Data
private User user;
@ -246,8 +245,13 @@ public final class LocalDB implements EventListener {
* @throws IOException if the saving process failed
* @since Envoy Client v0.3-alpha
*/
@Event(eventType = EnvoyCloseEvent.class, priority = 500)
@Event(EnvoyCloseEvent.class)
@Priority(500)
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...");
// Save users
@ -273,37 +277,66 @@ public final class LocalDB implements EventListener {
}
}
@Event(priority = 500)
/**
* 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)
private void onMessage(Message msg) {
if (msg.getStatus() == MessageStatus.SENT)
msg.nextStatus();
}
@Event(priority = 500)
@Event
@Priority(500)
private void onGroupMessage(GroupMessage msg) {
// TODO: Cancel event once EventBus is updated
if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ)
logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
}
@Event(priority = 500)
@Event
@Priority(500)
private void onMessageStatusChange(MessageStatusChange evt) {
getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get()));
}
@Event(priority = 500)
@Event
@Priority(500)
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
this.<GroupMessage>getMessage(evt.getID())
.ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
}
@Event(priority = 500)
@Event
@Priority(500)
private void onUserStatusChange(UserStatusChange evt) {
getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast)
.ifPresent(u -> u.setStatus(evt.get()));
}
@Event(priority = 500)
@Event
@Priority(500)
private void onUserOperation(UserOperation operation) {
final var eventUser = operation.get();
switch (operation.getOperationType()) {
@ -329,13 +362,15 @@ public final class LocalDB implements EventListener {
Platform.runLater(() -> chats.add(new GroupChat(user, newGroup)));
}
@Event(priority = 500)
@Event
@Priority(500)
private void onGroupResize(GroupResize evt) {
getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast)
.ifPresent(evt::apply);
}
@Event(priority = 500)
@Event
@Priority(500)
private void onNameChange(NameChange evt) {
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny()
.ifPresent(c -> c.setName(evt.get()));
@ -357,7 +392,8 @@ public final class LocalDB implements EventListener {
*
* @since Envoy Client v0.2-beta
*/
@Event(eventType = Logout.class, priority = 50)
@Event(Logout.class)
@Priority(50)
private void onLogout() {
autoSaver.cancel();
autoSaveRestart = true;
@ -389,21 +425,33 @@ public final class LocalDB implements EventListener {
});
}
@Event(priority = 500)
@Event
@Priority(500)
private void onOwnStatusChange(OwnStatusChange statusChange) {
user.setStatus(statusChange.get());
}
@Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500)
@Event(ContactsChangedSinceLastLogin.class)
@Priority(500)
private void onContactsChangedSinceLastLogin() {
contactsChanged = true;
}
@Event(priority = 500)
@Event
@Priority(500)
private void onContactDisabled(ContactDisabled event) {
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
* @since Envoy Client v0.2-alpha
@ -461,7 +509,8 @@ public final class LocalDB implements EventListener {
* @param idGenerator the message ID generator to set
* @since Envoy Client v0.3-alpha
*/
@Event(priority = 150)
@Event
@Priority(150)
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
/**

View File

@ -5,8 +5,7 @@ import java.util.*;
import java.util.logging.Level;
import java.util.prefs.Preferences;
import dev.kske.eventbus.*;
import dev.kske.eventbus.EventListener;
import dev.kske.eventbus.core.*;
import envoy.util.*;
@ -21,7 +20,7 @@ import envoy.client.event.EnvoyCloseEvent;
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public final class Settings implements EventListener {
public final class Settings {
// Actual settings accessible by the rest of the application
private Map<String, SettingsItem<?>> items;
@ -69,7 +68,7 @@ public final class Settings implements EventListener {
* @throws IOException if an error occurs while saving the themes
* @since Envoy Client v0.2-alpha
*/
@Event(eventType = EnvoyCloseEvent.class)
@Event(EnvoyCloseEvent.class)
private void save() {
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");

View File

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

View File

@ -4,7 +4,7 @@ import java.util.*;
import javafx.scene.input.KeyCombination;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.SceneInfo;
/**
* 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

@ -1,6 +1,6 @@
package envoy.client.helper;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.core.EventBus;
import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent;

View File

@ -5,14 +5,14 @@ import java.net.Socket;
import java.util.concurrent.TimeoutException;
import java.util.logging.*;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.core.*;
import dev.kske.eventbus.core.Event;
import envoy.data.*;
import envoy.event.*;
import envoy.util.*;
import envoy.client.data.*;
import envoy.client.data.ClientConfig;
import envoy.client.event.EnvoyCloseEvent;
/**
@ -24,7 +24,7 @@ import envoy.client.event.EnvoyCloseEvent;
* @author Leon Hofmeister
* @since Envoy Client v0.1-alpha
*/
public final class Client implements EventListener, Closeable {
public final class Client implements Closeable {
// Connection handling
private Socket socket;
@ -55,13 +55,12 @@ public final class Client implements EventListener, Closeable {
* the handshake does exceed this time limit, an exception is thrown.
*
* @param credentials the login credentials of the user
* @param cacheMap the map of all caches needed
* @throws TimeoutException if the server could not be reached
* @throws IOException if the login credentials could not be written
* @throws InterruptedException if the current thread is interrupted while waiting for the
* handshake response
*/
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap)
public void performHandshake(LoginCredentials credentials)
throws TimeoutException, IOException, InterruptedException {
if (online)
throw new IllegalStateException("Handshake has already been performed successfully");
@ -79,7 +78,6 @@ public final class Client implements EventListener, Closeable {
// Register user creation processor, contact list processor, message cache and
// authentication token
receiver.registerProcessor(User.class, sender -> this.sender = sender);
receiver.registerProcessors(cacheMap.getMap());
// Start receiver
receiver.start();
@ -101,42 +99,18 @@ public final class Client implements EventListener, Closeable {
if (System.currentTimeMillis() - start > 5000) {
rejected = true;
socket.close();
receiver.removeAllProcessors();
throw new TimeoutException("Did not log in after 5 seconds");
}
Thread.sleep(500);
}
online = true;
logger.log(Level.INFO, "Handshake completed.");
}
/**
* Initializes the {@link Receiver} used to process data sent from the server to this client.
*
* @param localDB the local database used to persist the current {@link IDGenerator}
* @param cacheMap the map of all caches needed
* @throws IOException if no {@link IDGenerator} is present and none could be requested from the
* server
* @since Envoy Client v0.2-alpha
*/
public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
checkOnline();
// Remove all processors as they are only used during the handshake
// Remove handshake specific processors
receiver.removeAllProcessors();
// Relay cached messages and message status changes
cacheMap.get(Message.class).setProcessor(eventBus::dispatch);
cacheMap.get(GroupMessage.class).setProcessor(eventBus::dispatch);
cacheMap.get(MessageStatusChange.class).setProcessor(eventBus::dispatch);
cacheMap.get(GroupMessageStatusChange.class).setProcessor(eventBus::dispatch);
// Request a generator if none is present or the existing one is consumed
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext())
requestIDGenerator();
// Relay caches
cacheMap.getMap().values().forEach(Cache::relay);
online = true;
logger.log(Level.INFO, "Handshake completed.");
}
/**
@ -179,13 +153,15 @@ public final class Client implements EventListener, Closeable {
send(new IDGeneratorRequest());
}
@Event(eventType = HandshakeRejection.class, priority = 1000)
@Event(HandshakeRejection.class)
@Priority(1000)
private void onHandshakeRejection() {
rejected = true;
}
@Override
@Event(eventType = EnvoyCloseEvent.class, priority = 50)
@Event(EnvoyCloseEvent.class)
@Priority(50)
public void close() {
if (online) {
logger.log(Level.INFO, "Closing connection...");

View File

@ -6,7 +6,7 @@ import java.util.*;
import java.util.function.Consumer;
import java.util.logging.*;
import dev.kske.eventbus.*;
import dev.kske.eventbus.core.EventBus;
import envoy.util.*;
@ -87,15 +87,17 @@ public final class Receiver extends Thread {
// Dispatch to the processor if present
if (processor != null)
processor.accept(obj);
// Dispatch to the event bus if the object is an event without a processor
else if (obj instanceof IEvent)
eventBus.dispatch((IEvent) obj);
// Notify if no processor could be located
// Dispatch to the event bus if the object has no processor
else
logger.log(Level.WARNING,
String.format(
"The received object has the %s for which no processor is defined.",
obj.getClass()));
eventBus.dispatch(obj);
// TODO: Log DeadEvent from Event Bus 1.1.0
// Notify if no processor could be located
// else
// logger.log(Level.WARNING,
// String.format(
// "The received object has the %s for which no processor is defined.",
// obj.getClass()));
}
} catch (final SocketException | EOFException e) {
// Connection probably closed by client.

View File

@ -4,12 +4,11 @@ import java.io.IOException;
import java.util.Stack;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.*;
import javafx.stage.Stage;
import dev.kske.eventbus.*;
import dev.kske.eventbus.core.*;
import envoy.util.EnvoyLog;
@ -26,53 +25,13 @@ import envoy.client.event.*;
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
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;
}
}
public final class SceneContext {
private final Stage stage;
private final FXMLLoader loader = new FXMLLoader();
private final Stack<Scene> sceneStack = new Stack<>();
private final Stack<Object> controllerStack = new Stack<>();
private final Stack<Parent> roots = new Stack<>();
private final Stack<Object> controllers = new Stack<>();
private static final Settings settings = Settings.getInstance();
private Scene scene;
/**
* Initializes the scene context.
@ -88,44 +47,44 @@ public final class SceneContext implements EventListener {
/**
* 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
* @since Envoy Client v0.1-beta
*/
public void load(SceneInfo sceneInfo) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
loader.setRoot(null);
loader.setController(null);
public void load(SceneInfo info) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + info);
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);
stage.setScene(scene);
// Load root node and controller
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
scene.getAccelerators()
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(sceneInfo));
.putAll(GlobalKeyShortcuts.getInstance().getKeyboardShortcuts(info));
// Supply the scene specific keyboard shortcuts
if (controller instanceof KeyboardMapping)
scene.getAccelerators()
.putAll(((KeyboardMapping) controller).getKeyboardShortcuts());
// 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);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@ -137,42 +96,45 @@ public final class SceneContext implements EventListener {
*/
public void pop() {
// Pop scene and controller
sceneStack.pop();
controllerStack.pop();
// Pop current root node and controller
roots.pop();
controllers.pop();
// Apply new scene if present
if (!sceneStack.isEmpty()) {
final var newScene = sceneStack.peek();
stage.setScene(newScene);
applyCSS();
stage.sizeToScene();
// If the controller implements the Restorable interface,
// the actions to perform on restoration will be executed here
final var controller = controllerStack.peek();
if (!roots.isEmpty()) {
scene.setRoot(roots.peek());
// Invoke restore if controller is restorable
var controller = controllers.peek();
if (controller instanceof Restorable)
((Restorable) controller).onRestore();
} else {
// Remove the current scene entirely
scene = null;
stage.setScene(null);
}
stage.show();
}
private void applyCSS() {
if (!sceneStack.isEmpty()) {
final var styleSheets = stage.getScene().getStylesheets();
final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css";
if (scene != null) {
var styleSheets = scene.getStylesheets();
var themeCSS = "/css/" + Settings.getInstance().getCurrentTheme() + ".css";
styleSheets.clear();
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(),
getClass().getResource(themeCSS).toExternalForm());
}
}
@Event(eventType = Logout.class, priority = 150)
@Event(Logout.class)
@Priority(150)
private void onLogout() {
sceneStack.clear();
controllerStack.clear();
roots.clear();
controllers.clear();
}
@Event(priority = 150, eventType = ThemeChangeEvent.class)
@Event(ThemeChangeEvent.class)
@Priority(150)
private void onThemeChange() {
applyCSS();
}
@ -182,7 +144,7 @@ public final class SceneContext implements EventListener {
* @return the controller used by the current scene
* @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
@ -194,5 +156,5 @@ public final class SceneContext implements EventListener {
* @return whether the scene stack is empty
* @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

@ -12,7 +12,7 @@ import javafx.stage.Stage;
import envoy.data.*;
import envoy.data.User.UserStatus;
import envoy.event.*;
import envoy.event.UserStatusChange;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
@ -20,7 +20,6 @@ import envoy.client.data.*;
import envoy.client.data.shortcuts.EnvoyShortcutConfig;
import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene;
import envoy.client.util.IconUtil;
@ -94,7 +93,7 @@ public final class Startup extends Application {
final var sceneContext = new SceneContext(stage);
context.setSceneContext(sceneContext);
// Authenticate with token if present
// Authenticate with token if present or load login scene
if (localDB.getAuthToken() != null) {
logger.info("Attempting authentication with token...");
localDB.loadUserData();
@ -103,8 +102,9 @@ public final class Startup extends Application {
VERSION, localDB.getLastSync())))
sceneContext.load(SceneInfo.LOGIN_SCENE);
} else
// Load login scene
sceneContext.load(SceneInfo.LOGIN_SCENE);
stage.show();
}
/**
@ -115,21 +115,20 @@ public final class Startup extends Application {
* @since Envoy Client v0.2-beta
*/
public static boolean performHandshake(LoginCredentials credentials) {
final var cacheMap = new CacheMap();
cacheMap.put(Message.class, new Cache<Message>());
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
final var originalStatus =
localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus();
try {
client.performHandshake(credentials, cacheMap);
client.performHandshake(credentials);
if (client.isOnline()) {
// Restore the original status as the server automatically returns status ONLINE
client.getSender().setStatus(originalStatus);
loadChatScene();
client.initReceiver(localDB, cacheMap);
// Request an ID generator if none is present or the existing one is consumed
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext())
client.requestIDGenerator();
return true;
} else
return false;
@ -226,7 +225,7 @@ public final class Startup extends Application {
// Load ChatScene
stage.setMinHeight(400);
stage.setMinWidth(843);
context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE);
context.getSceneContext().load(SceneInfo.CHAT_SCENE);
stage.centerOnScreen();
// Exit or minimize the stage when a close request occurs

View File

@ -9,8 +9,8 @@ import java.awt.image.BufferedImage;
import javafx.application.Platform;
import javafx.stage.Stage;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.core.Event;
import dev.kske.eventbus.core.EventBus;
import envoy.data.Message;
import envoy.data.User.UserStatus;
@ -31,7 +31,7 @@ import envoy.client.util.*;
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public final class StatusTrayIcon implements EventListener {
public final class StatusTrayIcon {
/**
* The {@link TrayIcon} provided by the System Tray API for controlling the system tray. This
@ -136,7 +136,7 @@ public final class StatusTrayIcon implements EventListener {
*
* @since Envoy Client v0.2-beta
*/
@Event(eventType = Logout.class)
@Event(Logout.class)
public void hide() {
SystemTray.getSystemTray().remove(trayIcon);
}

View File

@ -15,7 +15,7 @@ import envoy.util.EnvoyLog;
import envoy.client.data.Context;
import envoy.client.data.commands.*;
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.util.*;
@ -32,7 +32,7 @@ public final class ChatSceneCommands {
private final SystemCommandBuilder builder =
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.";
/**
@ -141,7 +141,7 @@ public final class ChatSceneCommands {
else
useRelativeMessage(command, action, additionalCheck, positionalArgument, false);
}).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,

View File

@ -18,7 +18,7 @@ import envoy.client.util.IconUtil;
*/
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);
/**
@ -60,4 +60,14 @@ public final class ChatControl extends HBox {
}
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;
import static envoy.client.ui.SceneInfo.SETTINGS_SCENE;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.*;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.*;
import javafx.animation.RotateTransition;
@ -18,14 +21,14 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
import dev.kske.eventbus.core.*;
import dev.kske.eventbus.core.Event;
import envoy.data.*;
import envoy.data.Attachment.AttachmentType;
@ -37,6 +40,7 @@ import envoy.util.EnvoyLog;
import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder;
import envoy.client.data.shortcuts.KeyboardMapping;
import envoy.client.event.*;
import envoy.client.net.*;
import envoy.client.ui.*;
@ -51,7 +55,7 @@ import envoy.client.util.*;
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class ChatScene implements EventListener, Restorable {
public final class ChatScene implements Restorable, KeyboardMapping {
@FXML
private ListView<Message> messageList;
@ -216,12 +220,13 @@ public final class ChatScene implements EventListener, Restorable {
});
}
@Event(eventType = BackEvent.class)
@Event(BackEvent.class)
private void onBackEvent() {
tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal());
}
@Event(includeSubtypes = true)
@Event
@Polymorphic
private void onMessage(Message message) {
// The sender of the message is the recipient of the chat
@ -300,7 +305,7 @@ public final class ChatScene implements EventListener, Restorable {
}));
}
@Event(eventType = NoAttachments.class)
@Event(NoAttachments.class)
private void onNoAttachments() {
Platform.runLater(() -> {
attachmentButton.setDisable(true);
@ -313,13 +318,15 @@ public final class ChatScene implements EventListener, Restorable {
});
}
@Event(priority = 150)
@Event
@Priority(150)
private void onGroupCreationResult(GroupCreationResult result) {
Platform.runLater(() -> newGroupButton.setDisable(result.get() == null));
}
@Event(eventType = ThemeChangeEvent.class)
@Event(ThemeChangeEvent.class)
private void onThemeChange() {
ChatControl.reloadDefaultChatIcons();
settingsButton.setGraphic(
new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(
@ -341,11 +348,17 @@ public final class ChatScene implements EventListener, Restorable {
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
}
@Event(eventType = Logout.class, priority = 200)
@Event(Logout.class)
@Priority(200)
private void onLogout() {
eventBus.removeListener(this);
}
@Event(AccountDeletion.class)
private void onAccountDeletion() {
Platform.runLater(chatList::refresh);
}
@Override
public void onRestore() {
updateRemainingCharsLabel();
@ -438,7 +451,7 @@ public final class ChatScene implements EventListener, Restorable {
*/
@FXML
private void settingsButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.load(SETTINGS_SCENE);
}
/**
@ -586,7 +599,7 @@ public final class ChatScene implements EventListener, Restorable {
// IsTyping#millisecondsActive
if (client.isOnline() && currentChat.getLastWritingEvent()
+ IsTyping.millisecondsActive <= System.currentTimeMillis()) {
client.send(new IsTyping(getChatID(), currentChat.getRecipient().getID()));
client.send(new IsTyping(currentChat.getRecipient().getID()));
currentChat.lastWritingEventWasNow();
}
@ -600,19 +613,6 @@ public final class ChatScene implements EventListener, Restorable {
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
* @since Envoy Client v0.1-beta
@ -786,7 +786,8 @@ public final class ChatScene implements EventListener, Restorable {
attachmentView.setVisible(visible);
}
@Event(eventType = OwnStatusChange.class, priority = 50)
@Event(OwnStatusChange.class)
@Priority(50)
private void generateOwnStatusControl() {
// Update the own user status if present
@ -797,7 +798,7 @@ public final class ChatScene implements EventListener, Restorable {
// Else prepend it to the HBox children
final var ownUserControl = new ContactControl(localDB.getUser());
ownUserControl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(ownUserControl, Priority.NEVER);
HBox.setHgrow(ownUserControl, javafx.scene.layout.Priority.NEVER);
ownContactControl.getChildren().add(1, ownUserControl);
}
}
@ -884,4 +885,25 @@ public final class ChatScene implements EventListener, Restorable {
: c -> c.getRecipient().getName().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

@ -7,7 +7,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.*;
import dev.kske.eventbus.core.*;
import envoy.data.User;
import envoy.event.ElementOperation;
@ -34,7 +34,7 @@ import envoy.client.ui.listcell.ListCellFactory;
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class ContactSearchTab implements EventListener {
public class ContactSearchTab {
@FXML
private TextArea searchBar;

View File

@ -10,7 +10,7 @@ import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import dev.kske.eventbus.*;
import dev.kske.eventbus.core.*;
import envoy.data.*;
import envoy.event.GroupCreation;
@ -18,7 +18,7 @@ import envoy.event.contact.UserOperation;
import envoy.util.Bounds;
import envoy.client.data.*;
import envoy.client.event.BackEvent;
import envoy.client.event.*;
import envoy.client.ui.control.*;
import envoy.client.ui.listcell.ListCellFactory;
@ -33,7 +33,7 @@ import envoy.client.ui.listcell.ListCellFactory;
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupCreationTab implements EventListener {
public class GroupCreationTab {
@FXML
private Button createButton;
@ -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

@ -10,7 +10,7 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.ImageView;
import dev.kske.eventbus.*;
import dev.kske.eventbus.core.*;
import envoy.data.LoginCredentials;
import envoy.event.HandshakeRejection;
@ -27,7 +27,7 @@ import envoy.client.util.IconUtil;
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public final class LoginScene implements EventListener {
public final class LoginScene {
@FXML
private TextField userTextField;

View File

@ -2,7 +2,7 @@ package envoy.client.ui.settings;
import javafx.scene.control.*;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.core.EventBus;
import envoy.data.User.UserStatus;

View File

@ -13,14 +13,15 @@ import javafx.scene.image.*;
import javafx.scene.input.InputEvent;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.core.EventBus;
import envoy.event.*;
import envoy.util.*;
import envoy.client.ui.control.ProfilePicImageView;
import envoy.client.util.IconUtil;
import envoy.client.util.*;
/**
* @author Leon Hofmeister
@ -38,6 +39,7 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
private final PasswordField newPasswordField = new PasswordField();
private final PasswordField repeatNewPasswordField = new PasswordField();
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 Logger logger = EnvoyLog.getLogger(UserSettingsPane.class);
@ -112,16 +114,19 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
final PasswordField[] passwordFields =
{ currentPasswordField, newPasswordField, repeatNewPasswordField };
final EventHandler<? super InputEvent> passwordEntered = e -> {
newPassword =
newPasswordField.getText();
validPassword = newPassword
.equals(
repeatNewPasswordField
.getText())
&& !newPasswordField
.getText().isBlank();
};
final EventHandler<? super InputEvent> passwordEntered =
e -> {
newPassword =
newPasswordField
.getText();
validPassword =
newPassword.equals(
repeatNewPasswordField
.getText())
&& !newPasswordField
.getText()
.isBlank();
};
newPasswordField.setOnInputMethodTextChanged(passwordEntered);
newPasswordField.setOnKeyTyped(passwordEntered);
repeatNewPasswordField.setOnInputMethodTextChanged(passwordEntered);
@ -137,9 +142,21 @@ public final class UserSettingsPane extends OnlineOnlySettingsPane {
// Displaying the save button
saveButton
.setOnAction(e -> save(client.getSender().getID(), currentPasswordField.getText()));
.setOnAction(e -> save(currentPasswordField.getText()));
saveButton.setAlignment(Pos.BOTTOM_RIGHT);
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
* @since Envoy Client v0.2-beta
*/
private void save(long userID, String oldPassword) {
private void save(String oldPassword) {
// The profile pic was changed
if (profilePicChanged) {
final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes, userID);
final var profilePicChangeEvent = new ProfilePicChange(currentImageBytes);
eventBus.dispatch(profilePicChangeEvent);
client.send(profilePicChangeEvent);
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
final var validContactName = Bounds.isValidContactName(newUsername);
if (usernameChanged && validContactName) {
final var nameChangeEvent = new NameChange(userID, newUsername);
final var nameChangeEvent = new NameChange(client.getSender().getID(), newUsername);
eventBus.dispatch(nameChangeEvent);
client.send(nameChangeEvent);
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
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!");
} else if (!(validPassword || newPassword.isBlank())) {
final var alert = new Alert(AlertType.ERROR);

View File

@ -7,7 +7,7 @@ import java.util.logging.*;
import javafx.stage.FileChooser;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.core.EventBus;
import envoy.data.Message;
import envoy.util.EnvoyLog;

View File

@ -2,10 +2,10 @@ package envoy.client.util;
import java.util.logging.*;
import javafx.scene.control.Alert;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import dev.kske.eventbus.EventBus;
import dev.kske.eventbus.core.EventBus;
import envoy.data.*;
import envoy.data.User.UserStatus;
@ -16,7 +16,7 @@ import envoy.util.EnvoyLog;
import envoy.client.data.Context;
import envoy.client.event.*;
import envoy.client.helper.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.SceneInfo;
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

@ -16,12 +16,13 @@ module envoy.client {
requires javafx.fxml;
requires javafx.base;
requires javafx.graphics;
requires dev.kske.eventbus.core;
opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus;
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
opens envoy.client.ui.chatscene to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus.core;
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus.core;
opens envoy.client.ui.chatscene to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus.core;
opens envoy.client.ui.control to javafx.graphics, javafx.fxml;
opens envoy.client.ui.settings to envoy.client.util;
opens envoy.client.net to dev.kske.eventbus;
opens envoy.client.data to dev.kske.eventbus;
opens envoy.client.net to dev.kske.eventbus.core;
opens envoy.client.data to dev.kske.eventbus.core;
}

View File

@ -70,6 +70,17 @@
-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 {
-fx-alignment: center-left;
-fx-background-radius: 1.3em;

View File

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

View File

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

View File

@ -7,11 +7,16 @@
<?import javafx.scene.layout.HBox?>
<?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>
<HBox prefHeight="389.0" prefWidth="600.0">
<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>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</opaqueInsets>
@ -22,7 +27,8 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</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>
<Insets bottom="10.0" left="5.0" right="10.0" top="10.0" />
</HBox.margin>
@ -32,7 +38,8 @@
</TitledPane>
</children>
</HBox>
<Button defaultButton="true" mnemonicParsing="true" onMouseClicked="#backButtonClicked" text="_Back">
<Button defaultButton="true" mnemonicParsing="true"
onMouseClicked="#backButtonClicked" text="_Back">
<opaqueInsets>
<Insets />
</opaqueInsets>

View File

@ -12,18 +12,11 @@
<version>0.2-beta</version>
</parent>
<repositories>
<repository>
<id>kske-repo</id>
<url>https://kske.dev/maven-repo</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.0.4</version>
<artifactId>event-bus-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>

View File

@ -1,6 +1,7 @@
package envoy.data;
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.
@ -63,9 +64,9 @@ public final class Attachment implements Serializable {
* @since Envoy Common v0.1-beta
*/
public Attachment(byte[] data, String name, AttachmentType type) {
this.data = data;
this.name = name;
this.type = type;
this.data = Objects.requireNonNull(data);
this.name = Objects.requireNonNull(name);
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) {
this.id = id;
this.name = name;
this.contacts = contacts;
this.name = Objects.requireNonNull(name);
this.contacts = contacts == null ? new HashSet<>() : contacts;
}
/**

View File

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

View File

@ -2,15 +2,13 @@ package envoy.data;
import java.io.Serializable;
import dev.kske.eventbus.IEvent;
/**
* Generates increasing IDs between two numbers.
*
* @author Kai S. K. Engelbart
* @since Envoy Common v0.2-alpha
*/
public final class IDGenerator implements IEvent, Serializable {
public final class IDGenerator implements Serializable {
private final long end;
private long current;

View File

@ -2,6 +2,7 @@ package envoy.data;
import java.io.Serializable;
import java.time.Instant;
import java.util.Objects;
/**
* 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 LoginCredentials(String identifier, String password, boolean registration,
boolean token, boolean requestToken, String clientVersion,
Instant lastSync) {
this.identifier = identifier;
this.password = password;
boolean token, boolean requestToken, String clientVersion, Instant lastSync) {
this.identifier = Objects.requireNonNull(identifier);
this.password = Objects.requireNonNull(password);
this.registration = registration;
this.token = token;
this.requestToken = requestToken;
this.clientVersion = clientVersion;
this.lastSync = lastSync;
this.clientVersion = Objects.requireNonNull(clientVersion);
this.lastSync = lastSync == null ? Instant.EPOCH : lastSync;
}
/**
@ -75,7 +75,8 @@ public final class LoginCredentials implements Serializable {
* @since Envoy Common v0.2-beta
*/
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,
lastSync);
}

View File

@ -2,8 +2,7 @@ package envoy.data;
import java.io.Serializable;
import java.time.Instant;
import dev.kske.eventbus.IEvent;
import java.util.Objects;
/**
* Represents a unique message with a unique, numeric ID. Further metadata includes the sender and
@ -13,7 +12,7 @@ import dev.kske.eventbus.IEvent;
* @author Leon Hofmeister
* @since Envoy Common v0.2-alpha
*/
public class Message implements Serializable, IEvent {
public class Message implements Serializable {
/**
* This enumeration defines all possible statuses a {link Message} can have.
@ -80,9 +79,9 @@ public class Message implements Serializable, IEvent {
this.creationDate = creationDate;
this.receivedDate = receivedDate;
this.readDate = readDate;
this.text = text;
this.text = text == null ? "" : text;
this.attachment = attachment;
this.status = status;
this.status = Objects.requireNonNull(status);
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) {
super(id, name, contacts);
this.status = status;
this.status = Objects.requireNonNull(status);
}
@Override

View File

@ -1,26 +1,33 @@
package envoy.event;
import java.io.Serializable;
import dev.kske.eventbus.IEvent;
import java.util.Objects;
/**
* This class serves as a convenience base class for all events. It implements the {@link IEvent}
* interface and provides a generic value. For events without a value there also is
* {@link envoy.event.Event.Valueless}.
* This class serves as a convenience base class for all events. It provides a generic value. For
* events without a value there also is {@link envoy.event.Event.Valueless}.
*
* @author Kai S. K. Engelbart
* @param <T> the type of the Event
* @since Envoy v0.2-alpha
*/
public abstract class Event<T> implements IEvent, Serializable {
public abstract class Event<T> implements Serializable {
protected final T value;
private static final long serialVersionUID = 0L;
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 +53,7 @@ public abstract class Event<T> implements IEvent, Serializable {
private static final long serialVersionUID = 0L;
protected Valueless() {
super(null);
super(null, true);
}
@Override

View File

@ -20,7 +20,7 @@ public class GroupCreationResult extends Event<Group> {
* @since Envoy Common v0.2-beta
*/
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
*/
public long getMemberID() { return memberID; }

View File

@ -2,6 +2,8 @@ package envoy.event;
import static envoy.event.ElementOperation.*;
import java.util.Objects;
import envoy.data.*;
/**
@ -30,7 +32,7 @@ public final class GroupResize extends Event<User> {
*/
public GroupResize(User user, Group group, ElementOperation operation) {
super(user);
this.operation = operation;
this.operation = Objects.requireNonNull(operation);
final var contained = group.getContacts().contains(user);
if (contained && operation.equals(ADD))
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> {
private final long destinationID;
private static final long serialVersionUID = 1L;
/**
@ -22,20 +20,13 @@ public final class IsTyping extends Event<Long> {
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 destinationID the ID of the contact the user wrote to
* @param id the ID of the recipient (client)/ originator(server)
* @since Envoy Common v0.2-beta
*/
public IsTyping(Long sourceID, long destinationID) {
super(sourceID);
this.destinationID = destinationID;
public IsTyping(long id) {
super(id);
}
/**
* @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) {
super(escape(title));
this.description = sanitizeDescription(description);
this.description = description == null ? "" : sanitizeDescription(description);
bug = isBug;
}
@ -37,8 +37,8 @@ public final class IssueProposal extends Event<String> {
*/
public IssueProposal(String title, String description, String user, boolean isBug) {
super(escape(title));
this.description =
sanitizeDescription(description) + String.format("<br>Submitted by user %s.", user);
this.description = description == null ? ""
: sanitizeDescription(description) + String.format("<br>Submitted by user %s.", user);
bug = isBug;
}

View File

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

View File

@ -1,6 +1,6 @@
package envoy.event;
import envoy.data.Contact;
import java.util.Objects;
/**
* @author Leon Hofmeister
@ -8,29 +8,20 @@ import envoy.data.Contact;
*/
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;
/**
* @param newPassword the new 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
*/
public PasswordChangeRequest(String newPassword, String oldPassword, long userID) {
public PasswordChangeRequest(String newPassword, String oldPassword) {
super(newPassword);
this.oldPassword = oldPassword;
id = userID;
this.oldPassword = Objects.requireNonNull(oldPassword);
}
/**
* @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
* @since Envoy Common v0.2-beta
@ -39,6 +30,6 @@ public final class PasswordChangeRequest extends Event<String> {
@Override
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[]> {
private final long id;
private static final long serialVersionUID = 0L;
/**
* @param value the byte[] of the new image
* @param userID the ID of the user who changed his profile pic
* @param value the byte[] of the new image
* @since Envoy Common v0.2-beta
*/
public ProfilePicChange(byte[] value, long userID) {
public ProfilePicChange(byte[] 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;
import java.util.Objects;
import envoy.data.User;
import envoy.event.*;
@ -24,7 +26,7 @@ public final class UserOperation extends Event<User> {
*/
public UserOperation(User contact, ElementOperation operationType) {
super(contact);
this.operationType = operationType;
this.operationType = Objects.requireNonNull(operationType);
}
/**

View File

@ -16,5 +16,5 @@ module envoy.common {
exports envoy.event.contact;
requires transitive java.logging;
requires transitive dev.kske.eventbus;
requires transitive dev.kske.eventbus.core;
}

View File

@ -18,10 +18,10 @@ import envoy.util.SerializationUtils;
* @author Leon Hofmeister
* @since Envoy Common v0.1-beta
*/
class UserTest {
public class UserTest {
@Test
void test() throws IOException, ClassNotFoundException {
public void test() throws IOException, ClassNotFoundException {
User user2 = new User(2, "kai");
User user3 = new User(3, "ai");
User user4 = new User(4, "ki", Set.of(user2, user3));

View File

@ -28,6 +28,13 @@
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins>
</build>
<modules>

View File

@ -1,7 +1,7 @@
package envoy.server.data;
import java.time.Instant;
import java.util.List;
import java.util.*;
import java.util.logging.Level;
import javax.persistence.*;
@ -121,8 +121,9 @@ public final class PersistenceManager {
transaction(() -> {
// 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);
contact.contacts.clear();
});
remove(contact);
}
@ -223,6 +224,9 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.2-beta
*/
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)
.setParameter("lastSeen", lastSync).getResultList();
}
@ -236,6 +240,9 @@ public final class PersistenceManager {
* @since Envoy Server Standalone v0.2-beta
*/
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)
.setParameter("userId", user.getID())
.setParameter("lastSeen", lastSync)
@ -277,16 +284,18 @@ public final class PersistenceManager {
* @since Envoy Server v0.3-beta
*/
public void addContactBidirectional(Contact contact1, Contact contact2) {
if (!(contact1 == null || contact2 == null)) {
// Add users to each others contact list
contact1.getContacts().add(contact2);
contact2.getContacts().add(contact1);
// Add users to each others contact list
contact1.getContacts().add(contact2);
contact2.getContacts().add(contact1);
// Synchronize changes with the database
transaction(() -> {
entityManager.merge(contact1);
entityManager.merge(contact2);
});
// Synchronize changes with the database
transaction(() -> {
entityManager.merge(contact1);
entityManager.merge(contact2);
});
}
}
/**
@ -308,16 +317,18 @@ public final class PersistenceManager {
* @since Envoy Server v0.3-beta
*/
public void removeContactBidirectional(Contact contact1, Contact contact2) {
if (!(contact1 == null || contact2 == null)) {
// Remove users from each others contact list
contact1.getContacts().remove(contact2);
contact2.getContacts().remove(contact1);
// Remove users from each others contact list
contact1.getContacts().remove(contact2);
contact2.getContacts().remove(contact1);
// Synchronize changes with the database
transaction(() -> {
entityManager.merge(contact1);
entityManager.merge(contact2);
});
// Synchronize changes with the database
transaction(() -> {
entityManager.merge(contact1);
entityManager.merge(contact2);
});
}
}
/**
@ -331,15 +342,36 @@ public final class PersistenceManager {
}
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) {
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) {
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
final envoy.server.data.User user =
PersistenceManager.getInstance().getUserByID(getUserIDBySocketID(socketID));
user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
if (user != null) {
user.setLastSeen(Instant.now());
UserStatusChangeProcessor.updateUserStatus(user, UserStatus.OFFLINE);
}
// Remove the socket
sockets.entrySet().removeIf(e -> e.getValue() == socketID);

View File

@ -33,7 +33,6 @@ public final class ObjectMessageProcessor implements IMessageProcessor {
this.processors = processors;
}
@SuppressWarnings("unchecked")
@Override
public void process(Message message, WriteProxy writeProxy) {
try (ObjectInputStream in =
@ -45,23 +44,34 @@ public final class ObjectMessageProcessor implements IMessageProcessor {
return;
}
logger.fine("Received " + obj);
logger.log(Level.INFO, "Received " + 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), message.socketId, new ObjectWriteProxy(writeProxy));
break;
} catch (IOException e) {
logger.log(Level.SEVERE, "Exception during processor execution: ", e);
}
}
refer(message.socketId, writeProxy, obj);
} 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.util.Collections;
import java.util.logging.Logger;
import java.util.logging.*;
import javax.persistence.EntityExistsException;
@ -15,6 +15,7 @@ import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager;
import envoy.server.net.*;
import envoy.server.util.UserAuthenticationUtil;
/**
* @author Maximilian K&auml;fer
@ -29,6 +30,15 @@ public final class GroupMessageProcessor implements ObjectProcessor<GroupMessage
@Override
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();
// 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.net.*;
import envoy.server.util.UserAuthenticationUtil;
/**
* @author Maximilian K&auml;fer
@ -28,7 +29,17 @@ public final class GroupMessageStatusChangeProcessor
@Override
public void process(GroupMessageStatusChange statusChange, long socketID,
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());
if (gmsg == null)
return;
// Any other status than READ is not supposed to be sent to the server
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 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
switch (groupResize.getOperation()) {
case ADD:

View File

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

View File

@ -40,6 +40,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Cache this write proxy for user-independent notifications
UserStatusChangeProcessor.setWriteProxy(writeProxy);
// Check for compatible versions
if (!VersionUtil.verifyCompatibility(credentials.getClientVersion())) {
logger.info("The client has the wrong version.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_VERSION));
@ -70,10 +71,10 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new HandshakeRejection(INVALID_TOKEN));
return;
}
} else
} else if (!PasswordUtil.validate(credentials.getPassword(),
user.getPasswordHash())) {
// Check the password hash
if (!PasswordUtil.validate(credentials.getPassword(), user.getPasswordHash())) {
// Check the password hash
logger.info(user + " has entered the wrong password.");
writeProxy.write(socketID, new HandshakeRejection(WRONG_PASSWORD_OR_USER));
return;
@ -101,7 +102,8 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new HandshakeRejection(USERNAME_TAKEN));
return;
} catch (final NoResultException e) {
// Creation of a new user
// Create a new user
user = new User();
user.setName(credentials.getIdentifier());
user.setLastSeen(Instant.now());
@ -123,7 +125,6 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
// Process token request
if (credentials.requestToken()) {
String token;
if (user.getAuthToken() != null && user.getAuthTokenExpiration().isAfter(Instant.now()))
// Reuse existing token and delay expiration date
@ -140,6 +141,14 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new NewAuthToken(token));
}
// Notify the user if a contact deletion has happened since he last logged in
if (user.getLatestContactDeletion().isAfter(user.getLastSeen()))
writeProxy.write(socketID, new ContactsChangedSinceLastLogin());
// Complete the handshake
writeProxy.write(socketID, user.toCommon());
// Send pending (group) messages and status changes
final var pendingMessages =
PersistenceManager.getInstance().getPendingMessages(user, credentials.getLastSync());
pendingMessages.removeIf(GroupMessage.class::isInstance);
@ -164,8 +173,9 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(connectionManager.getSocketID(msg.getSender().getID()),
new MessageStatusChange(msgCommon));
}
} else
} else {
writeProxy.write(socketID, new MessageStatusChange(msgCommon));
}
}
final List<GroupMessage> pendingGroupMessages = PersistenceManager.getInstance()
@ -199,10 +209,11 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
}
PersistenceManager.getInstance().updateMessage(gmsg);
} else
} else {
// Just send the message without updating if it was received in the past
writeProxy.write(socketID, gmsgCommon);
}
} else {
// Sending group message status changes
@ -222,11 +233,5 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
writeProxy.write(socketID, new MessageStatusChange(gmsgCommon));
}
}
// Notify the user if a contact deletion has happened since he last logged in
if (user.getLatestContactDeletion().isAfter(user.getLastSeen()))
writeProxy.write(socketID, new ContactsChangedSinceLastLogin());
// Complete the handshake
writeProxy.write(socketID, user.toCommon());
}
}

View File

@ -12,6 +12,7 @@ import envoy.util.EnvoyLog;
import envoy.server.data.PersistenceManager;
import envoy.server.net.*;
import envoy.server.util.UserAuthenticationUtil;
/**
* This {@link ObjectProcessor} handles incoming {@link Message}s.
@ -29,6 +30,15 @@ public final class MessageProcessor implements ObjectProcessor<Message> {
@Override
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();
// Convert to server message

View File

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

View File

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

View File

@ -22,10 +22,16 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
@Override
public void process(UserOperation evt, long socketId, ObjectWriteProxy writeProxy) {
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId);
public void process(UserOperation evt, long socketID, ObjectWriteProxy writeProxy) {
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketID);
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()) {
case ADD:
logger.log(Level.FINE,
@ -45,7 +51,7 @@ public final class UserOperationProcessor implements ObjectProcessor<UserOperati
sender.setLatestContactDeletion(Instant.now());
// 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
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;
}
}