Typing indicator + Metrics
This commit is contained in:
@@ -4,9 +4,11 @@ import dev.wiing.gossip.lib.data.AuthSecret;
|
||||
import dev.wiing.gossip.lib.models.SecretUser;
|
||||
import dev.wiing.gossip.lib.models.Topic;
|
||||
import dev.wiing.gossip.lib.models.User;
|
||||
import dev.wiing.gossip.server.customs.TypingTopic;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
@@ -15,14 +17,14 @@ public class Database {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(Database.class);
|
||||
|
||||
private int userIdCounter = 1;
|
||||
private int channelIDCounter = 1;
|
||||
private final Map<Long, User> users = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<String, User> usersBySecret = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<Long, Socket> userSockets = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Set<String> usedUsernames = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private int topicIdCounter = 1;
|
||||
private final Map<Long, Topic> topics = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<Socket, User> connectedUsers = Collections.synchronizedMap(new HashMap<>());
|
||||
private final Map<Long, TypingTopic> topics = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private Database() {
|
||||
|
||||
@@ -41,7 +43,7 @@ public class Database {
|
||||
random.nextBytes(secretBytes);
|
||||
AuthSecret secret = new AuthSecret(secretBytes);
|
||||
|
||||
long userID = userIdCounter++;
|
||||
long userID = channelIDCounter++;
|
||||
|
||||
User user = new User(username, iconID, userID);
|
||||
|
||||
@@ -49,6 +51,7 @@ public class Database {
|
||||
userSockets.put(userID, socket);
|
||||
usersBySecret.put(secret.getString(), user);
|
||||
usedUsernames.add(username);
|
||||
connectedUsers.put(socket, user);
|
||||
|
||||
logger.info("User created: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||
|
||||
@@ -79,12 +82,7 @@ public class Database {
|
||||
}
|
||||
|
||||
public User getUserOfSocket(Socket socket) {
|
||||
return userSockets.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().equals(socket))
|
||||
.findFirst()
|
||||
.map(Map.Entry::getKey)
|
||||
.map(this::getUserByID)
|
||||
.orElse(null);
|
||||
return connectedUsers.get(socket);
|
||||
}
|
||||
|
||||
public void removeUser(User user) {
|
||||
@@ -96,22 +94,43 @@ public class Database {
|
||||
.findFirst()
|
||||
.ifPresent(usersBySecret::remove);
|
||||
|
||||
userSockets.remove(user.getUserID());
|
||||
|
||||
usedUsernames.remove(user.getUsername());
|
||||
|
||||
disconnectUser(user);
|
||||
|
||||
logger.info("User removed: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||
}
|
||||
|
||||
public Topic createTopic(AuthSecret userSecret, String topicName, String topicDescription) {
|
||||
public void disconnectUser(User user) {
|
||||
Socket socket = null;
|
||||
|
||||
if (userSockets.containsKey(user.getUserID())) {
|
||||
socket = userSockets.remove(user.getUserID());
|
||||
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (socket != null) {
|
||||
connectedUsers.remove(socket);
|
||||
|
||||
logger.info("User disconnected: \"{}\" (#{})", user.getUsername(), user.getUserID());
|
||||
}
|
||||
}
|
||||
|
||||
public TypingTopic createTopic(AuthSecret userSecret, String topicName, String topicDescription) {
|
||||
if (!usersBySecret.containsKey(userSecret.getString())) return null;
|
||||
|
||||
User user = usersBySecret.get(userSecret.getString());
|
||||
|
||||
short colorHue = (short)Math.abs((new Random().nextInt(360)));
|
||||
|
||||
Topic topic = new Topic(
|
||||
topicIdCounter++,
|
||||
TypingTopic topic = new TypingTopic(
|
||||
channelIDCounter++,
|
||||
topicName,
|
||||
topicDescription,
|
||||
user,
|
||||
@@ -125,17 +144,17 @@ public class Database {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public Topic getTopic(long topicID) {
|
||||
public TypingTopic getTopic(long topicID) {
|
||||
return topics.getOrDefault(topicID, null);
|
||||
}
|
||||
|
||||
public void removeTopic(long topicID) {
|
||||
Topic topic = topics.remove(topicID);
|
||||
TypingTopic topic = topics.remove(topicID);
|
||||
|
||||
logger.info("Topic removed: \"{}\" (#{})", topic.getName(), topic.getId());
|
||||
}
|
||||
|
||||
public List<Topic> getAllTopicsReadOnly() {
|
||||
return Collections.unmodifiableList(topics.values().stream().toList());
|
||||
public List<TypingTopic> getAllTopicsReadOnly() {
|
||||
return topics.values().stream().toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package dev.wiing.gossip.server;
|
||||
import dev.wiing.gossip.lib.data.LongData;
|
||||
import dev.wiing.gossip.lib.models.*;
|
||||
import dev.wiing.gossip.lib.packets.*;
|
||||
import dev.wiing.gossip.server.customs.TypingTopic;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
|
||||
public record UserSocket(Socket socket) implements Runnable {
|
||||
|
||||
@@ -72,6 +72,10 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
case MessageFetchPacket.TYPE:
|
||||
onFetchMessage((MessageFetchPacket) packet);
|
||||
break;
|
||||
|
||||
case TypingPingPacket.TYPE:
|
||||
onTypingUser((TypingPingPacket) packet);
|
||||
break;
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
loop = false;
|
||||
@@ -82,7 +86,7 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
|
||||
// Connection lost
|
||||
User user = Database.getInstance().getUserOfSocket(socket);
|
||||
Database.getInstance().removeUser(user);
|
||||
Database.getInstance().disconnectUser(user);
|
||||
}
|
||||
|
||||
private void onRegisterUser(RegisterRequestPacket packet) {
|
||||
@@ -118,6 +122,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
for (User user : Database.getInstance().getUsers()) {
|
||||
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
||||
|
||||
if (socket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writePacket(socket.getOutputStream(), added);
|
||||
} catch (IOException e) {
|
||||
@@ -162,6 +168,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
for (User user : Database.getInstance().getUsers()) {
|
||||
Socket socket = Database.getInstance().getUserSocket(user.getUserID());
|
||||
|
||||
if (socket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writeAllPackets(socket.getOutputStream(), updatePacket, systemPacket);
|
||||
} catch (IOException e) {
|
||||
@@ -259,6 +267,8 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
for (User babbler : topic.getUsersReadOnly().values()) {
|
||||
Socket userSocket = Database.getInstance().getUserSocket(babbler.getUserID());
|
||||
|
||||
if (userSocket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writePacket(userSocket.getOutputStream(), messageCreated);
|
||||
} catch (IOException e) {
|
||||
@@ -306,6 +316,7 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
resp.setMessageType(MessageDataPacket.MessageType.USER);
|
||||
resp.setUserAuthorID(userMessage.getAuthor().getUserID());
|
||||
resp.setUserContents(userMessage.getContents());
|
||||
|
||||
} else if (message instanceof SystemMessage systemMessage) {
|
||||
resp.setMessageType(MessageDataPacket.MessageType.SYSTEM);
|
||||
resp.setSystemType(systemMessage.getType());
|
||||
@@ -321,4 +332,68 @@ public record UserSocket(Socket socket) implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private void onTypingUser(TypingPingPacket packet) {
|
||||
User user = Database.getInstance().getUserBySecret(packet.getAuth());
|
||||
|
||||
if (user == null) return;
|
||||
|
||||
TypingTopic topic = Database.getInstance().getTopic(packet.getTopicID());
|
||||
|
||||
if (topic == null || !topic.hasUser(user)) return;
|
||||
|
||||
if (packet.isTyping()) {
|
||||
topic.addTypingUser(user);
|
||||
|
||||
if (topic.getTypingExpiry().containsKey(user)) topic.getTypingExpiry().get(user).interrupt();
|
||||
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(20_000);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
topic.removeTypingUser(user);
|
||||
|
||||
announceTypingList(topic);
|
||||
|
||||
topic.getTypingExpiry().remove(user);
|
||||
});
|
||||
thread.start();
|
||||
|
||||
topic.getTypingExpiry().put(user, thread);
|
||||
|
||||
} else {
|
||||
topic.removeTypingUser(user);
|
||||
|
||||
Thread thread = topic.getTypingExpiry().remove(user);
|
||||
if (thread != null && thread.isAlive()) thread.interrupt();
|
||||
}
|
||||
|
||||
info("User #{} typing ping on #{}", user.getUserID(), topic.getId());
|
||||
|
||||
announceTypingList(topic);
|
||||
|
||||
}
|
||||
|
||||
private static void announceTypingList(TypingTopic topic) {
|
||||
TypingListUpdatePacket resp = new TypingListUpdatePacket();
|
||||
resp.setTopicID(topic.getId());
|
||||
resp.getTypingMembers().addAll(topic.getTypingUsersReadOnly().stream()
|
||||
.map(typing -> new LongData().setValue(typing.getUserID()))
|
||||
.toList());
|
||||
|
||||
for (User babbler : topic.getUsersReadOnly().values()) {
|
||||
Socket userSocket = Database.getInstance().getUserSocket(babbler.getUserID());
|
||||
|
||||
if (userSocket == null) continue;
|
||||
|
||||
try {
|
||||
Globals.getPacketManager().writePacket(userSocket.getOutputStream(), resp);
|
||||
} catch (IOException e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.wiing.gossip.server.customs;
|
||||
|
||||
import dev.wiing.gossip.lib.models.Topic;
|
||||
import dev.wiing.gossip.lib.models.User;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class TypingTopic extends Topic {
|
||||
|
||||
private final Map<User, Thread> typingExpiry = new ConcurrentHashMap<>();
|
||||
|
||||
public TypingTopic(long id, String name, String description, User host, short color) {
|
||||
super(id, name, description, host, color);
|
||||
}
|
||||
|
||||
public Map<User, Thread> getTypingExpiry() {
|
||||
return typingExpiry;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user